homebridge-unifi-protect 6.19.0 → 6.21.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.
- package/README.md +35 -49
- package/dist/devices/protect-camera-package.js +1 -1
- package/dist/devices/protect-camera-package.js.map +1 -1
- package/dist/devices/protect-camera.d.ts +6 -4
- package/dist/devices/protect-camera.js +144 -218
- package/dist/devices/protect-camera.js.map +1 -1
- package/dist/devices/protect-chime.d.ts +3 -0
- package/dist/devices/protect-chime.js +100 -28
- package/dist/devices/protect-chime.js.map +1 -1
- package/dist/devices/protect-device.d.ts +9 -2
- package/dist/devices/protect-device.js +162 -105
- package/dist/devices/protect-device.js.map +1 -1
- package/dist/devices/protect-doorbell.js +48 -81
- package/dist/devices/protect-doorbell.js.map +1 -1
- package/dist/devices/protect-light.js +33 -43
- package/dist/devices/protect-light.js.map +1 -1
- package/dist/devices/protect-liveviews.js +10 -20
- package/dist/devices/protect-liveviews.js.map +1 -1
- package/dist/devices/protect-nvr-systeminfo.js +3 -9
- package/dist/devices/protect-nvr-systeminfo.js.map +1 -1
- package/dist/devices/protect-securitysystem.d.ts +1 -2
- package/dist/devices/protect-securitysystem.js +31 -58
- package/dist/devices/protect-securitysystem.js.map +1 -1
- package/dist/devices/protect-sensor.d.ts +2 -3
- package/dist/devices/protect-sensor.js +154 -204
- package/dist/devices/protect-sensor.js.map +1 -1
- package/dist/devices/protect-viewer.js +21 -28
- package/dist/devices/protect-viewer.js.map +1 -1
- package/dist/ffmpeg/protect-ffmpeg-options.d.ts +14 -2
- package/dist/ffmpeg/protect-ffmpeg-options.js +61 -46
- package/dist/ffmpeg/protect-ffmpeg-options.js.map +1 -1
- package/dist/ffmpeg/protect-ffmpeg-record.js +11 -2
- package/dist/ffmpeg/protect-ffmpeg-record.js.map +1 -1
- package/dist/ffmpeg/protect-ffmpeg-stream.js +2 -2
- package/dist/ffmpeg/protect-ffmpeg-stream.js.map +1 -1
- package/dist/ffmpeg/protect-ffmpeg.js +2 -2
- package/dist/ffmpeg/protect-ffmpeg.js.map +1 -1
- package/dist/protect-events.js +25 -20
- package/dist/protect-events.js.map +1 -1
- package/dist/protect-mqtt.d.ts +2 -2
- package/dist/protect-mqtt.js +17 -3
- package/dist/protect-mqtt.js.map +1 -1
- package/dist/protect-nvr.d.ts +5 -0
- package/dist/protect-nvr.js +46 -24
- package/dist/protect-nvr.js.map +1 -1
- package/dist/protect-options.d.ts +7 -2
- package/dist/protect-options.js +24 -12
- package/dist/protect-options.js.map +1 -1
- package/dist/protect-record.d.ts +3 -1
- package/dist/protect-record.js +29 -15
- package/dist/protect-record.js.map +1 -1
- package/dist/protect-rtp.d.ts +2 -2
- package/dist/protect-rtp.js +8 -9
- package/dist/protect-rtp.js.map +1 -1
- package/dist/protect-snapshot.js +13 -1
- package/dist/protect-snapshot.js.map +1 -1
- package/dist/protect-stream.js +26 -8
- package/dist/protect-stream.js.map +1 -1
- package/dist/protect-types.d.ts +10 -0
- package/dist/protect-types.js +6 -1
- package/dist/protect-types.js.map +1 -1
- package/dist/settings.d.ts +1 -0
- package/dist/settings.js +2 -0
- package/dist/settings.js.map +1 -1
- package/homebridge-ui/public/index.html +11 -12
- package/package.json +12 -12
|
@@ -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
|
-
//
|
|
52
|
-
|
|
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.
|
|
@@ -157,8 +138,7 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
157
138
|
// - name change.
|
|
158
139
|
// - camera status light.
|
|
159
140
|
// - camera recording settings.
|
|
160
|
-
if (payload.state || payload.name ||
|
|
161
|
-
(payload.recordingSettings && ("mode" in payload.recordingSettings))) {
|
|
141
|
+
if (payload.state || payload.name || payload.ledSettings?.isEnabled || payload.recordingSettings?.mode) {
|
|
162
142
|
this.updateDevice();
|
|
163
143
|
}
|
|
164
144
|
}
|
|
@@ -166,11 +146,11 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
166
146
|
smartMotionEventHandler(packet) {
|
|
167
147
|
const payload = packet.payload;
|
|
168
148
|
// We're only interested in smart motion detection events.
|
|
169
|
-
if ((packet.header.modelKey !== "event") ||
|
|
149
|
+
if ((packet.header.modelKey !== "event") || !["smartDetectLine", "smartDetectZone"].includes(payload.type) || !payload.smartDetectTypes.length) {
|
|
170
150
|
return;
|
|
171
151
|
}
|
|
172
152
|
// Process the motion event.
|
|
173
|
-
this.nvr.events.motionEventHandler(this, payload.smartDetectTypes,
|
|
153
|
+
this.nvr.events.motionEventHandler(this, payload.smartDetectTypes, payload.metadata);
|
|
174
154
|
}
|
|
175
155
|
// Configure discrete smart motion contact sensors for HomeKit.
|
|
176
156
|
configureMotionSmartSensor() {
|
|
@@ -202,30 +182,22 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
202
182
|
}
|
|
203
183
|
// A utility for us to add contact sensors.
|
|
204
184
|
const addSmartDetectContactSensor = (name, serviceId, errorMessage) => {
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
if (!
|
|
209
|
-
|
|
210
|
-
|
|
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);
|
|
185
|
+
// Acquire the service.
|
|
186
|
+
const service = this.acquireService(this.hap.Service.ContactSensor, name, serviceId);
|
|
187
|
+
// Fail gracefully.
|
|
188
|
+
if (!service) {
|
|
189
|
+
this.log.error(errorMessage);
|
|
190
|
+
return false;
|
|
218
191
|
}
|
|
219
192
|
// Initialize the sensor.
|
|
220
|
-
|
|
221
|
-
contactService.updateCharacteristic(this.hap.Characteristic.ContactSensorState, false);
|
|
193
|
+
service.updateCharacteristic(this.hap.Characteristic.ContactSensorState, false);
|
|
222
194
|
return true;
|
|
223
195
|
};
|
|
224
196
|
let enabledContactSensors = [];
|
|
225
197
|
// Add individual contact sensors for each object detection type, if needed.
|
|
226
198
|
if (this.hasFeature("Motion.SmartDetect.ObjectSensors")) {
|
|
227
199
|
for (const smartDetectType of this.ufp.featureFlags.smartDetectTypes) {
|
|
228
|
-
if (addSmartDetectContactSensor(this.accessoryName + " " +
|
|
200
|
+
if (addSmartDetectContactSensor(this.accessoryName + " " + toCamelCase(smartDetectType), ProtectReservedNames.CONTACT_MOTION_SMARTDETECT + "." + smartDetectType, "Unable to add smart motion contact sensor for " + smartDetectType + " detection.")) {
|
|
229
201
|
enabledContactSensors.push(smartDetectType);
|
|
230
202
|
}
|
|
231
203
|
}
|
|
@@ -248,17 +220,17 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
248
220
|
}
|
|
249
221
|
// Configure a switch to manually trigger a doorbell ring event for HomeKit.
|
|
250
222
|
configureDoorbellTrigger() {
|
|
251
|
-
// Find the switch service, if it exists.
|
|
252
|
-
let triggerService = this.accessory.getServiceById(this.hap.Service.Switch, ProtectReservedNames.SWITCH_DOORBELL_TRIGGER);
|
|
253
223
|
// See if we have a doorbell service configured.
|
|
254
224
|
let doorbellService = this.accessory.getService(this.hap.Service.Doorbell);
|
|
255
|
-
//
|
|
256
|
-
if (!this.
|
|
257
|
-
|
|
258
|
-
|
|
225
|
+
// Validate whether we should have this service enabled.
|
|
226
|
+
if (!this.validService(this.hap.Service.Switch, () => {
|
|
227
|
+
// Doorbell switches are disabled by default and primarily exist for automation purposes.
|
|
228
|
+
if (!this.hasFeature("Doorbell.Trigger")) {
|
|
229
|
+
return false;
|
|
259
230
|
}
|
|
260
|
-
|
|
261
|
-
|
|
231
|
+
return true;
|
|
232
|
+
}, ProtectReservedNames.SWITCH_DOORBELL_TRIGGER)) {
|
|
233
|
+
// 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
234
|
if (!this.ufp.featureFlags.isDoorbell && doorbellService) {
|
|
263
235
|
this.accessory.removeService(doorbellService);
|
|
264
236
|
}
|
|
@@ -276,16 +248,12 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
276
248
|
return false;
|
|
277
249
|
}
|
|
278
250
|
}
|
|
279
|
-
const triggerName = this.accessoryName + " Doorbell Trigger";
|
|
280
251
|
// Add the switch to the camera, if needed.
|
|
252
|
+
const triggerService = this.acquireService(this.hap.Service.Switch, this.accessoryName + " Doorbell Trigger", ProtectReservedNames.SWITCH_DOORBELL_TRIGGER);
|
|
253
|
+
// Fail gracefully.
|
|
281
254
|
if (!triggerService) {
|
|
282
|
-
|
|
283
|
-
|
|
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);
|
|
255
|
+
this.log.error("Unable to add the doorbell trigger.");
|
|
256
|
+
return false;
|
|
289
257
|
}
|
|
290
258
|
// Trigger the doorbell.
|
|
291
259
|
triggerService.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => {
|
|
@@ -307,31 +275,21 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
307
275
|
}
|
|
308
276
|
});
|
|
309
277
|
// Initialize the switch.
|
|
310
|
-
triggerService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, triggerName);
|
|
311
278
|
triggerService.updateCharacteristic(this.hap.Characteristic.On, false);
|
|
312
279
|
this.log.info("Enabling doorbell automation trigger.");
|
|
313
280
|
return true;
|
|
314
281
|
}
|
|
315
282
|
// Configure the doorbell service for HomeKit.
|
|
316
283
|
configureVideoDoorbell() {
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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);
|
|
284
|
+
// Acquire the service.
|
|
285
|
+
const service = this.acquireService(this.hap.Service.Doorbell);
|
|
286
|
+
// Fail gracefully.
|
|
287
|
+
if (!service) {
|
|
288
|
+
this.log.error("Unable to add doorbell.");
|
|
289
|
+
return false;
|
|
332
290
|
}
|
|
333
|
-
|
|
334
|
-
|
|
291
|
+
// Add the doorbell service to this Protect doorbell. HomeKit requires the doorbell service to be marked as the primary service on the accessory.
|
|
292
|
+
service.setPrimaryService(true);
|
|
335
293
|
return true;
|
|
336
294
|
}
|
|
337
295
|
// Configure additional camera-specific characteristics for HomeKit.
|
|
@@ -414,18 +372,15 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
414
372
|
// Figure out which camera channels are RTSP-enabled, and user-enabled.
|
|
415
373
|
let cameraChannels = this.ufp.channels.filter(x => x.isRtspEnabled && this.hasFeature("Video.Stream." + x.name, true));
|
|
416
374
|
// Make sure we've got a HomeKit compatible IDR frame interval. If not, let's take care of that.
|
|
417
|
-
|
|
375
|
+
const idrChannels = cameraChannels.filter(x => x.idrInterval !== PROTECT_HOMEKIT_IDR_INTERVAL);
|
|
418
376
|
if (idrChannels.length) {
|
|
419
|
-
// Edit the channel map.
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
return x;
|
|
423
|
-
});
|
|
424
|
-
this.ufp = await this.nvr.ufpApi.updateDevice(this.ufp, { channels: idrChannels }) ?? this.ufp;
|
|
377
|
+
// Edit the channel map and update the Protect controller.
|
|
378
|
+
this.ufp = await this.nvr.ufpApi.updateDevice(this.ufp, { channels: idrChannels.map(x => Object.assign(x, { idrInterval: PROTECT_HOMEKIT_IDR_INTERVAL })) }) ??
|
|
379
|
+
this.ufp;
|
|
425
380
|
}
|
|
426
381
|
// Set the camera and shapshot URLs.
|
|
427
382
|
const cameraUrl = "rtsps://" + (this.nvr.config.overrideAddress ?? this.ufp.connectionHost) + ":" + this.nvr.ufp.ports.rtsps.toString() + "/";
|
|
428
|
-
// Filter out any package camera entries.
|
|
383
|
+
// Filter out any package camera entries. We deal with those independently in the package camera class.
|
|
429
384
|
cameraChannels = cameraChannels.filter(x => x.name !== "Package Camera");
|
|
430
385
|
// No RTSP streams are available that meet our criteria - we're done.
|
|
431
386
|
if (!cameraChannels.length) {
|
|
@@ -478,7 +433,7 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
478
433
|
continue;
|
|
479
434
|
}
|
|
480
435
|
// Find the closest RTSP match for this resolution.
|
|
481
|
-
const foundRtsp = this.findRtsp(entry[0], entry[1], rtspEntries);
|
|
436
|
+
const foundRtsp = this.findRtsp(entry[0], entry[1], { rtspEntries: rtspEntries });
|
|
482
437
|
if (!foundRtsp) {
|
|
483
438
|
continue;
|
|
484
439
|
}
|
|
@@ -491,9 +446,9 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
491
446
|
// Since we added resolutions to the list, resort resolutions, from high to low.
|
|
492
447
|
rtspEntries.sort(this.sortByResolutions.bind(this));
|
|
493
448
|
}
|
|
494
|
-
// Ensure we've got at least one entry that can be used for HomeKit Secure Video. Some Protect cameras (e.g. G3 Flex) don't have a native frame rate that
|
|
495
|
-
//
|
|
496
|
-
//
|
|
449
|
+
// Ensure we've got at least one entry that can be used for HomeKit Secure Video. Some Protect cameras (e.g. G3 Flex) don't have a native frame rate that maps to
|
|
450
|
+
// HomeKit's specific requirements for event recording, so we ensure there's at least one. This doesn't directly affect which stream is used to actually record
|
|
451
|
+
// something, but it does determine whether HomeKit even attempts to use the camera for HomeKit Secure Video.
|
|
497
452
|
if (![15, 24, 30].includes(rtspEntries[0].resolution[2])) {
|
|
498
453
|
// Iterate through the list of RTSP entries we're providing to HomeKit and ensure we have at least one that will meet HomeKit's requirements for frame rate.
|
|
499
454
|
for (let i = 0; i < rtspEntries.length; i++) {
|
|
@@ -516,8 +471,8 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
516
471
|
}
|
|
517
472
|
// Publish our updated list of supported resolutions and their URLs.
|
|
518
473
|
this.rtspEntries = rtspEntries;
|
|
519
|
-
// If we'
|
|
520
|
-
if (this.
|
|
474
|
+
// If we've already configured the HomeKit video streaming delegate, we're done here.
|
|
475
|
+
if (this.stream) {
|
|
521
476
|
return true;
|
|
522
477
|
}
|
|
523
478
|
// Inform users about our RTSP entry mapping, if we're debugging.
|
|
@@ -539,17 +494,20 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
539
494
|
}
|
|
540
495
|
// Inform the user if we've set a streaming default.
|
|
541
496
|
if (this.rtspQuality.StreamingDefault) {
|
|
542
|
-
this.log.info("Video streaming configured to use only: %s.",
|
|
497
|
+
this.log.info("Video streaming configured to use only: %s.", toCamelCase(this.rtspQuality.StreamingDefault.toLowerCase()));
|
|
498
|
+
}
|
|
499
|
+
// Inform the user if they've selected the legacy snapshot API.
|
|
500
|
+
if (!this.hints.highResSnapshots) {
|
|
501
|
+
this.log.info("Disabling the use of higher quality snapshots.");
|
|
543
502
|
}
|
|
544
503
|
// Inform the user if we've set a recording default.
|
|
545
504
|
if (this.rtspQuality.RecordingDefault) {
|
|
546
|
-
this.log.info("HomeKit Secure Video event recording configured to use only: %s.",
|
|
505
|
+
this.log.info("HomeKit Secure Video event recording configured to use only: %s.", toCamelCase(this.rtspQuality.RecordingDefault.toLowerCase()));
|
|
547
506
|
}
|
|
548
507
|
// Configure the video stream with our resolutions.
|
|
549
508
|
this.stream = new ProtectStreamingDelegate(this, this.rtspEntries.map(x => x.resolution));
|
|
550
509
|
// Fire up the controller and inform HomeKit about it.
|
|
551
510
|
this.accessory.configureController(this.stream.controller);
|
|
552
|
-
this.isVideoConfigured = true;
|
|
553
511
|
return true;
|
|
554
512
|
}
|
|
555
513
|
// Configure HomeKit Secure Video support.
|
|
@@ -565,75 +523,66 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
565
523
|
}
|
|
566
524
|
// Configure a switch to manually enable or disable HKSV recording for a camera.
|
|
567
525
|
configureHksvRecordingSwitch() {
|
|
568
|
-
//
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
this.accessory.removeService(switchService);
|
|
526
|
+
// Validate whether we should have this service enabled.
|
|
527
|
+
if (!this.validService(this.hap.Service.Switch, () => {
|
|
528
|
+
// If we don't have HKSV or the HKSV recording switch enabled, disable it and we're done.
|
|
529
|
+
if (!this.hasFeature("Video.HKSV.Recording.Switch")) {
|
|
530
|
+
return false;
|
|
574
531
|
}
|
|
532
|
+
return true;
|
|
533
|
+
}, ProtectReservedNames.SWITCH_HKSV_RECORDING)) {
|
|
575
534
|
// We want to default this back to recording whenever we disable the recording switch.
|
|
576
535
|
this.accessory.context.hksvRecording = true;
|
|
577
536
|
return false;
|
|
578
537
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
return false;
|
|
586
|
-
}
|
|
587
|
-
switchService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
|
|
588
|
-
this.accessory.addService(switchService);
|
|
538
|
+
// Acquire the service.
|
|
539
|
+
const service = this.acquireService(this.hap.Service.Switch, this.accessoryName + " HKSV Recording", ProtectReservedNames.SWITCH_HKSV_RECORDING);
|
|
540
|
+
// Fail gracefully.
|
|
541
|
+
if (!service) {
|
|
542
|
+
this.log.error("Unable to add HKSV recording switch.");
|
|
543
|
+
return false;
|
|
589
544
|
}
|
|
590
545
|
// Activate or deactivate HKSV recording.
|
|
591
|
-
|
|
592
|
-
return this.accessory.context.hksvRecording;
|
|
546
|
+
service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => {
|
|
547
|
+
return this.accessory.context.hksvRecording ?? true;
|
|
593
548
|
});
|
|
594
|
-
|
|
549
|
+
service.getCharacteristic(this.hap.Characteristic.On)?.onSet((value) => {
|
|
595
550
|
if (this.accessory.context.hksvRecording !== value) {
|
|
596
|
-
this.log.info("
|
|
551
|
+
this.log.info("HKSV event recording has been %s.", value === true ? "enabled" : "disabled");
|
|
597
552
|
}
|
|
598
553
|
this.accessory.context.hksvRecording = value === true;
|
|
599
554
|
});
|
|
600
555
|
// Initialize the switch.
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
this.log.info("Enabling HomeKit Secure Video recording switch.");
|
|
556
|
+
service.updateCharacteristic(this.hap.Characteristic.On, this.accessory.context.hksvRecording);
|
|
557
|
+
this.log.info("Enabling HKSV recording switch.");
|
|
604
558
|
return true;
|
|
605
559
|
}
|
|
606
560
|
// Configure a switch to manually enable or disable dynamic bitrate capabilities for a camera.
|
|
607
561
|
configureDynamicBitrateSwitch() {
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
this.accessory.removeService(switchService);
|
|
562
|
+
// Validate whether we should have this service enabled.
|
|
563
|
+
if (!this.validService(this.hap.Service.Switch, () => {
|
|
564
|
+
// If we don't want a dynamic bitrate switch, disable it and we're done.
|
|
565
|
+
if (!this.hasFeature("Video.DynamicBitrate.Switch")) {
|
|
566
|
+
return false;
|
|
614
567
|
}
|
|
568
|
+
return true;
|
|
569
|
+
}, ProtectReservedNames.SWITCH_DYNAMIC_BITRATE)) {
|
|
615
570
|
// We want to default this back to off by default whenever we disable the dynamic bitrate switch.
|
|
616
571
|
this.accessory.context.dynamicBitrate = false;
|
|
617
572
|
return false;
|
|
618
573
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
return false;
|
|
626
|
-
}
|
|
627
|
-
switchService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
|
|
628
|
-
this.accessory.addService(switchService);
|
|
574
|
+
// Acquire the service.
|
|
575
|
+
const service = this.acquireService(this.hap.Service.Switch, this.accessoryName + " Dynamic Bitrate", ProtectReservedNames.SWITCH_DYNAMIC_BITRATE);
|
|
576
|
+
// Fail gracefully.
|
|
577
|
+
if (!service) {
|
|
578
|
+
this.log.error("Unable to add dynamic bitrate switch.");
|
|
579
|
+
return false;
|
|
629
580
|
}
|
|
630
581
|
// Activate or deactivate dynamic bitrate for this device.
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
})
|
|
636
|
-
.onSet(async (value) => {
|
|
582
|
+
service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => {
|
|
583
|
+
return this.accessory.context.dynamicBitrate ?? false;
|
|
584
|
+
});
|
|
585
|
+
service.getCharacteristic(this.hap.Characteristic.On)?.onSet(async (value) => {
|
|
637
586
|
if (this.accessory.context.dynamicBitrate === value) {
|
|
638
587
|
return;
|
|
639
588
|
}
|
|
@@ -643,12 +592,8 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
643
592
|
this.log.info("Dynamic streaming bitrate adjustment on the UniFi Protect controller enabled.");
|
|
644
593
|
return;
|
|
645
594
|
}
|
|
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
|
-
}
|
|
595
|
+
// We're disabling dynamic bitrate for this device. Update the channels JSON to revert to the maximum bitrate we can.
|
|
596
|
+
const updatedChannels = this.ufp.channels.map(channel => ({ ...channel, bitrate: channel.maxBitrate }));
|
|
652
597
|
// Send the channels JSON to Protect.
|
|
653
598
|
const newDevice = await this.nvr.ufpApi.updateDevice(this.ufp, { channels: updatedChannels });
|
|
654
599
|
// We failed.
|
|
@@ -662,8 +607,7 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
662
607
|
this.log.info("Dynamic streaming bitrate adjustment on the UniFi Protect controller disabled.");
|
|
663
608
|
});
|
|
664
609
|
// Initialize the switch.
|
|
665
|
-
|
|
666
|
-
switchService.updateCharacteristic(this.hap.Characteristic.On, this.accessory.context.dynamicBitrate);
|
|
610
|
+
service.updateCharacteristic(this.hap.Characteristic.On, this.accessory.context.dynamicBitrate);
|
|
667
611
|
this.log.info("Enabling the dynamic streaming bitrate adjustment switch.");
|
|
668
612
|
return true;
|
|
669
613
|
}
|
|
@@ -673,31 +617,29 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
673
617
|
// The Protect controller supports three modes for recording on a camera: always, detections, and never. We create switches for each of the modes.
|
|
674
618
|
for (const ufpRecordingSwitchType of [ProtectReservedNames.SWITCH_UFP_RECORDING_ALWAYS, ProtectReservedNames.SWITCH_UFP_RECORDING_DETECTIONS, ProtectReservedNames.SWITCH_UFP_RECORDING_NEVER]) {
|
|
675
619
|
const ufpRecordingSetting = ufpRecordingSwitchType.slice(ufpRecordingSwitchType.lastIndexOf(".") + 1);
|
|
676
|
-
//
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
this.accessory.removeService(switchService);
|
|
620
|
+
// Validate whether we should have this service enabled.
|
|
621
|
+
if (!this.validService(this.hap.Service.Switch, () => {
|
|
622
|
+
// If we don't have the feature option enabled, disable the switch and we're done.
|
|
623
|
+
if (!this.hasFeature("Nvr.Recording.Switch")) {
|
|
624
|
+
return false;
|
|
682
625
|
}
|
|
626
|
+
return true;
|
|
627
|
+
}, ufpRecordingSwitchType)) {
|
|
683
628
|
continue;
|
|
684
629
|
}
|
|
685
|
-
const switchName = this.accessoryName + " UFP Recording " +
|
|
686
|
-
//
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
switchService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
|
|
694
|
-
this.accessory.addService(switchService);
|
|
630
|
+
const switchName = this.accessoryName + " UFP Recording " + toCamelCase(ufpRecordingSetting);
|
|
631
|
+
// Acquire the service.
|
|
632
|
+
const service = this.acquireService(this.hap.Service.Switch, switchName, ufpRecordingSwitchType);
|
|
633
|
+
// Fail gracefully.
|
|
634
|
+
if (!service) {
|
|
635
|
+
this.log.error("Unable to add UniFi Protect recording switches.");
|
|
636
|
+
continue;
|
|
695
637
|
}
|
|
696
638
|
// Activate or deactivate the appropriate recording mode on the Protect controller.
|
|
697
|
-
|
|
639
|
+
service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => {
|
|
698
640
|
return this.ufp.recordingSettings.mode === ufpRecordingSetting;
|
|
699
641
|
});
|
|
700
|
-
|
|
642
|
+
service.getCharacteristic(this.hap.Characteristic.On)?.onSet(async (value) => {
|
|
701
643
|
// 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
644
|
// 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
645
|
if (!value) {
|
|
@@ -729,8 +671,7 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
729
671
|
this.log.info("UniFi Protect recording mode set to %s.", ufpRecordingSetting);
|
|
730
672
|
});
|
|
731
673
|
// Initialize the recording switch state.
|
|
732
|
-
|
|
733
|
-
switchService.updateCharacteristic(this.hap.Characteristic.On, this.ufp.recordingSettings.mode === ufpRecordingSetting);
|
|
674
|
+
service.updateCharacteristic(this.hap.Characteristic.On, this.ufp.recordingSettings.mode === ufpRecordingSetting);
|
|
734
675
|
switchesEnabled.push(ufpRecordingSetting);
|
|
735
676
|
}
|
|
736
677
|
if (switchesEnabled.length) {
|
|
@@ -741,44 +682,28 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
741
682
|
// Configure MQTT capabilities of this camera.
|
|
742
683
|
configureMqtt() {
|
|
743
684
|
// Return the RTSP URLs when requested.
|
|
744
|
-
this.
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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.");
|
|
685
|
+
this.subscribeGet("rtsp", "RTSP information", () => {
|
|
686
|
+
// Grab all the available RTSP channels and return them as a JSON.
|
|
687
|
+
return JSON.stringify(Object.assign({}, ...this.ufp.channels.filter(channel => channel.isRtspEnabled)
|
|
688
|
+
.map(channel => ({ [channel.name]: "rtsps://" + this.nvr.ufp.host + ":" + this.nvr.ufp.ports.rtsp + "/" + channel.rtspAlias + "?enableSrtp" }))));
|
|
760
689
|
});
|
|
761
690
|
// Trigger snapshots when requested.
|
|
762
|
-
this.
|
|
763
|
-
const value = message.toString();
|
|
691
|
+
this.subscribeSet("snapshot", "snapshot trigger", (value) => {
|
|
764
692
|
// When we get the right message, we trigger the snapshot request.
|
|
765
|
-
if (value
|
|
693
|
+
if (value !== "true") {
|
|
766
694
|
return;
|
|
767
695
|
}
|
|
768
696
|
void this.stream?.handleSnapshotRequest();
|
|
769
|
-
this.log.info("Snapshot triggered via MQTT.");
|
|
770
697
|
});
|
|
771
698
|
// Enable doorbell-specific MQTT capabilities only when we have a Protect doorbell or a doorbell trigger enabled.
|
|
772
699
|
if (this.ufp.featureFlags.isDoorbell || this.hasFeature("Doorbell.Trigger")) {
|
|
773
700
|
// Trigger doorbell when requested.
|
|
774
|
-
this.
|
|
775
|
-
const value = message.toString();
|
|
701
|
+
this.subscribeSet("doorbell", "doorbell ring trigger", (value) => {
|
|
776
702
|
// When we get the right message, we trigger the doorbell request.
|
|
777
|
-
if (value
|
|
703
|
+
if (value !== "true") {
|
|
778
704
|
return;
|
|
779
705
|
}
|
|
780
706
|
this.nvr.events.doorbellEventHandler(this, Date.now());
|
|
781
|
-
this.log.info("Doorbell ring event triggered via MQTT.");
|
|
782
707
|
});
|
|
783
708
|
}
|
|
784
709
|
return true;
|
|
@@ -812,9 +737,8 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
812
737
|
}
|
|
813
738
|
// Set the bitrate for a specific camera channel.
|
|
814
739
|
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
|
|
817
|
-
// user has both the global setting and the switch enabled, the switch setting will take precedence.
|
|
740
|
+
// 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,
|
|
741
|
+
// 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
742
|
if ((!this.accessory.context.dynamicBitrate && !this.hasFeature("Video.DynamicBitrate")) ||
|
|
819
743
|
(!this.accessory.context.dynamicBitrate && this.hasFeature("Video.DynamicBitrate") && this.hasFeature("Video.DynamicBitrate.Switch"))) {
|
|
820
744
|
return true;
|
|
@@ -842,44 +766,46 @@ export class ProtectCamera extends ProtectDevice {
|
|
|
842
766
|
return true;
|
|
843
767
|
}
|
|
844
768
|
// Find an RTSP configuration for a given target resolution.
|
|
845
|
-
findRtspEntry(width, height,
|
|
769
|
+
findRtspEntry(width, height, options) {
|
|
770
|
+
const rtspEntries = options?.rtspEntries ?? this.rtspEntries;
|
|
846
771
|
// No RTSP entries to choose from, we're done.
|
|
847
772
|
if (!rtspEntries || !rtspEntries.length) {
|
|
848
773
|
return null;
|
|
849
774
|
}
|
|
850
775
|
// Second, we check to see if we've set an explicit preference for stream quality.
|
|
851
|
-
if (
|
|
852
|
-
|
|
853
|
-
return rtspEntries.find(x => x.channel.name.toUpperCase() ===
|
|
776
|
+
if (options?.default) {
|
|
777
|
+
options.default = options.default.toUpperCase();
|
|
778
|
+
return rtspEntries.find(x => x.channel.name.toUpperCase() === options.default) ?? null;
|
|
854
779
|
}
|
|
855
780
|
// See if we have a match for our desired resolution on the camera. We ignore FPS - HomeKit clients seem to be able to handle it just fine.
|
|
856
|
-
const exactRtsp = rtspEntries.find(x => (x.
|
|
781
|
+
const exactRtsp = rtspEntries.find(x => (x.channel.width === width) && (x.channel.height === height));
|
|
857
782
|
if (exactRtsp) {
|
|
858
783
|
return exactRtsp;
|
|
859
784
|
}
|
|
860
|
-
//
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
const entry = rtspEntries.find(x => x.resolution[0] >= 1280);
|
|
864
|
-
if (entry) {
|
|
865
|
-
return entry;
|
|
866
|
-
}
|
|
785
|
+
// If we haven't found an exact match, by default, we bias ourselves to the next lower resolution we find or the lowest resolution we have available as a backstop.
|
|
786
|
+
if (!options?.biasHigher) {
|
|
787
|
+
return rtspEntries.find(x => x.channel.width < width) ?? rtspEntries[rtspEntries.length - 1];
|
|
867
788
|
}
|
|
868
|
-
// If we
|
|
869
|
-
//
|
|
870
|
-
return rtspEntries.filter(x => x.
|
|
789
|
+
// If we're biasing ourselves toward higher resolutions (primarily used when transcoding so we start with a higher quality input), we look for the first entry that's
|
|
790
|
+
// larger than our requested width and if not found, we return the highest resolution we have available.
|
|
791
|
+
return rtspEntries.filter(x => x.channel.width > width).pop() ?? rtspEntries[0];
|
|
871
792
|
}
|
|
872
793
|
// Find a streaming RTSP configuration for a given target resolution.
|
|
873
|
-
findRtsp(width, height,
|
|
794
|
+
findRtsp(width, height, options) {
|
|
795
|
+
// Create our options JSON if needed.
|
|
796
|
+
options = options ?? {};
|
|
797
|
+
// See if we've been given RTSP entries or whether we should default to our own.
|
|
798
|
+
options.rtspEntries = options.rtspEntries ?? this.rtspEntries;
|
|
874
799
|
// If we've imposed a constraint on the maximum dimensions of what we want due to a hardware limitation, filter out those entries.
|
|
875
|
-
if (
|
|
876
|
-
rtspEntries = rtspEntries.filter(x => (x.channel.width * x.channel.height) <=
|
|
800
|
+
if (options.maxPixels !== undefined) {
|
|
801
|
+
options.rtspEntries = options.rtspEntries.filter(x => (x.channel.width * x.channel.height) <= (options.maxPixels ?? Infinity));
|
|
877
802
|
}
|
|
878
|
-
|
|
803
|
+
// Return the entry.
|
|
804
|
+
return this.findRtspEntry(width, height, options);
|
|
879
805
|
}
|
|
880
806
|
// Find a recording RTSP configuration for a given target resolution.
|
|
881
|
-
findRecordingRtsp(width, height
|
|
882
|
-
return this.findRtspEntry(width, height,
|
|
807
|
+
findRecordingRtsp(width, height) {
|
|
808
|
+
return this.findRtspEntry(width, height, { biasHigher: true, default: this.rtspQuality.RecordingDefault ?? this.stream.ffmpegOptions.recordingDefaultChannel });
|
|
883
809
|
}
|
|
884
810
|
// Utility function for sorting by resolution.
|
|
885
811
|
sortByResolutions(a, b) {
|