homebridge-unifi-protect 5.5.4 → 6.0.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.
Files changed (79) hide show
  1. package/README.md +3 -3
  2. package/config.schema.json +17 -16
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.js +6 -6
  5. package/dist/index.js.map +1 -1
  6. package/dist/protect-camera.d.ts +58 -0
  7. package/dist/protect-camera.js +367 -246
  8. package/dist/protect-camera.js.map +1 -1
  9. package/dist/protect-device.d.ts +48 -0
  10. package/dist/protect-device.js +189 -0
  11. package/dist/protect-device.js.map +1 -0
  12. package/dist/protect-doorbell.d.ts +22 -0
  13. package/dist/protect-doorbell.js +75 -64
  14. package/dist/protect-doorbell.js.map +1 -1
  15. package/dist/protect-ffmpeg-record.d.ts +15 -0
  16. package/dist/protect-ffmpeg-record.js +48 -34
  17. package/dist/protect-ffmpeg-record.js.map +1 -1
  18. package/dist/protect-ffmpeg-stream.d.ts +15 -0
  19. package/dist/protect-ffmpeg-stream.js +22 -12
  20. package/dist/protect-ffmpeg-stream.js.map +1 -1
  21. package/dist/protect-ffmpeg.d.ts +42 -0
  22. package/dist/protect-ffmpeg.js +49 -58
  23. package/dist/protect-ffmpeg.js.map +1 -1
  24. package/dist/protect-light.d.ts +13 -0
  25. package/dist/protect-light.js +63 -40
  26. package/dist/protect-light.js.map +1 -1
  27. package/dist/protect-liveviews.d.ts +17 -0
  28. package/dist/protect-liveviews.js +117 -101
  29. package/dist/protect-liveviews.js.map +1 -1
  30. package/dist/protect-mqtt.d.ts +19 -0
  31. package/dist/protect-mqtt.js +26 -35
  32. package/dist/protect-mqtt.js.map +1 -1
  33. package/dist/protect-nvr-events.d.ts +30 -0
  34. package/dist/protect-nvr-events.js +168 -431
  35. package/dist/protect-nvr-events.js.map +1 -1
  36. package/dist/protect-nvr-systeminfo.d.ts +15 -0
  37. package/dist/protect-nvr-systeminfo.js +43 -49
  38. package/dist/protect-nvr-systeminfo.js.map +1 -1
  39. package/dist/protect-nvr.d.ts +48 -0
  40. package/dist/protect-nvr.js +327 -359
  41. package/dist/protect-nvr.js.map +1 -1
  42. package/dist/protect-options.d.ts +39 -0
  43. package/dist/protect-options.js +172 -6
  44. package/dist/protect-options.js.map +1 -1
  45. package/dist/protect-platform.d.ts +17 -0
  46. package/dist/protect-platform.js +17 -30
  47. package/dist/protect-platform.js.map +1 -1
  48. package/dist/protect-record.d.ts +33 -0
  49. package/dist/protect-record.js +130 -126
  50. package/dist/protect-record.js.map +1 -1
  51. package/dist/protect-rtp.d.ts +29 -0
  52. package/dist/protect-rtp.js +133 -16
  53. package/dist/protect-rtp.js.map +1 -1
  54. package/dist/protect-securitysystem.d.ts +18 -0
  55. package/dist/protect-securitysystem.js +105 -109
  56. package/dist/protect-securitysystem.js.map +1 -1
  57. package/dist/protect-sensor.d.ts +28 -0
  58. package/dist/protect-sensor.js +79 -97
  59. package/dist/protect-sensor.js.map +1 -1
  60. package/dist/protect-stream.d.ts +41 -0
  61. package/dist/protect-stream.js +298 -156
  62. package/dist/protect-stream.js.map +1 -1
  63. package/dist/protect-timeshift.d.ts +30 -0
  64. package/dist/protect-timeshift.js +65 -48
  65. package/dist/protect-timeshift.js.map +1 -1
  66. package/dist/protect-types.d.ts +50 -0
  67. package/dist/protect-types.js +22 -0
  68. package/dist/protect-types.js.map +1 -0
  69. package/dist/protect-viewer.d.ts +17 -0
  70. package/dist/protect-viewer.js +41 -47
  71. package/dist/protect-viewer.js.map +1 -1
  72. package/dist/settings.d.ts +22 -0
  73. package/dist/settings.js +30 -35
  74. package/dist/settings.js.map +1 -1
  75. package/homebridge-ui/public/index.html +715 -0
  76. package/homebridge-ui/server.js +156 -0
  77. package/package.json +15 -15
  78. package/dist/protect-accessory.js +0 -184
  79. package/dist/protect-accessory.js.map +0 -1
@@ -1,67 +1,158 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ProtectNvr = void 0;
4
- const settings_1 = require("./settings");
5
- const unifi_protect_1 = require("unifi-protect");
6
- const protect_camera_1 = require("./protect-camera");
7
- const protect_doorbell_1 = require("./protect-doorbell");
8
- const protect_light_1 = require("./protect-light");
9
- const protect_liveviews_1 = require("./protect-liveviews");
10
- const protect_mqtt_1 = require("./protect-mqtt");
11
- const protect_nvr_events_1 = require("./protect-nvr-events");
12
- const protect_nvr_systeminfo_1 = require("./protect-nvr-systeminfo");
13
- const protect_sensor_1 = require("./protect-sensor");
14
- const protect_viewer_1 = require("./protect-viewer");
15
- class ProtectNvr {
1
+ import { PLATFORM_NAME, PLUGIN_NAME, PROTECT_CONTROLLER_REFRESH_INTERVAL } from "./settings.js";
2
+ import { ProtectApi } from "unifi-protect";
3
+ import { optionEnabled } from "./protect-options.js";
4
+ import { ProtectCamera } from "./protect-camera.js";
5
+ import { ProtectDoorbell } from "./protect-doorbell.js";
6
+ import { ProtectLight } from "./protect-light.js";
7
+ import { ProtectLiveviews } from "./protect-liveviews.js";
8
+ import { ProtectMqtt } from "./protect-mqtt.js";
9
+ import { ProtectNvrEvents } from "./protect-nvr-events.js";
10
+ import { ProtectNvrSystemInfo } from "./protect-nvr-systeminfo.js";
11
+ import { ProtectSensor } from "./protect-sensor.js";
12
+ import { ProtectViewer } from "./protect-viewer.js";
13
+ import util from "node:util";
14
+ export class ProtectNvr {
16
15
  constructor(platform, nvrOptions) {
17
16
  this.api = platform.api;
18
17
  this.config = nvrOptions;
19
18
  this.configuredDevices = {};
20
- this.debug = platform.debug.bind(platform);
21
- this.doorbellCount = 0;
22
19
  this.isEnabled = false;
23
20
  this.hap = this.api.hap;
24
21
  this.lastMotion = {};
25
22
  this.lastRing = {};
26
23
  this.liveviews = null;
27
- this.log = platform.log;
24
+ this.logApiErrors = true;
28
25
  this.mqtt = null;
29
- this.name = nvrOptions.name;
30
- this.eventTimers = {};
31
- this.nvrAddress = nvrOptions.address;
26
+ this.name = nvrOptions.name ?? nvrOptions.address;
27
+ this.nvrOptions = nvrOptions;
32
28
  this.platform = platform;
33
- this.refreshInterval = nvrOptions.refreshInterval;
34
29
  this.systemInfo = null;
30
+ this.ufp = {};
35
31
  this.unsupportedDevices = {};
36
- // Assign a name, if we don't have one.
37
- if (!this.name) {
38
- this.name = this.nvrAddress;
39
- }
32
+ // Configure our logging.
33
+ this.log = {
34
+ debug: (message, ...parameters) => this.platform.debug(util.format(this.name + ": " + message, ...parameters)),
35
+ error: (message, ...parameters) => this.platform.log.error(util.format(this.name + ": " + message, ...parameters)),
36
+ info: (message, ...parameters) => this.platform.log.info(util.format(this.name + ": " + message, ...parameters)),
37
+ warn: (message, ...parameters) => this.platform.log.warn(util.format(this.name + ": " + message, ...parameters))
38
+ };
40
39
  // Validate our Protect address and login information.
41
40
  if (!nvrOptions.address || !nvrOptions.username || !nvrOptions.password) {
42
41
  return;
43
42
  }
44
43
  // Initialize our connection to the UniFi Protect API.
45
- this.nvrApi = new unifi_protect_1.ProtectApi(nvrOptions.address, nvrOptions.username, nvrOptions.password, this.log);
46
- // Initialize our event handlers.
47
- this.events = new protect_nvr_events_1.ProtectNvrEvents(this);
48
- // Initialize our liveviews.
49
- this.liveviews = new protect_liveviews_1.ProtectLiveviews(this);
50
- // Initialize our NVR system information.
51
- this.systemInfo = new protect_nvr_systeminfo_1.ProtectNvrSystemInfo(this);
52
- // Cleanup any stray ffmpeg sessions on shutdown.
44
+ const ufpLog = {
45
+ debug: (message, ...parameters) => this.platform.debug(util.format(message, ...parameters)),
46
+ error: (message, ...parameters) => {
47
+ if (this.logApiErrors) {
48
+ this.platform.log.error(util.format(message, ...parameters));
49
+ }
50
+ },
51
+ info: (message, ...parameters) => this.platform.log.info(util.format(message, ...parameters)),
52
+ warn: (message, ...parameters) => this.platform.log.warn(util.format(message, ...parameters))
53
+ };
54
+ this.ufpApi = new ProtectApi(ufpLog);
55
+ // Initialize our UniFi Protect realtime event handlers.
56
+ this.events = new ProtectNvrEvents(this);
57
+ // Inform users when we've logged in.
58
+ this.ufpApi.once("login", (success) => {
59
+ // We had login issues.
60
+ if (!success) {
61
+ this.ufpApi.clearLoginCredentials();
62
+ this.log.info("Unable to login to the UniFi Protect API. Ensure your login credentials and Protect controller address are correct.");
63
+ return;
64
+ }
65
+ // We logged in successfully.
66
+ this.platform.log.info("Connected to the UniFi Protect API at %s.", nvrOptions.address);
67
+ void this.ufpUpdate();
68
+ });
69
+ // Once we've connected to the Protect API, we can initialize ourselves.
70
+ this.ufpApi.once("bootstrap", this.ufpStartupHandler.bind(this));
71
+ // Cleanup any remaining streaming sessions on shutdown.
53
72
  this.api.on("shutdown" /* APIEvent.SHUTDOWN */, () => {
54
- var _a;
55
- for (const protectCamera of Object.values(this.configuredDevices)) {
56
- if (protectCamera instanceof protect_camera_1.ProtectCamera) {
57
- this.debug("%s: Shutting down all video stream processes.", protectCamera.name());
58
- void ((_a = protectCamera.stream) === null || _a === void 0 ? void 0 : _a.shutdown());
73
+ for (const protectDevice of Object.values(this.configuredDevices)) {
74
+ if (("ufp" in protectDevice) && (protectDevice.ufp.modelKey === "camera")) {
75
+ protectDevice.log.debug("Shutting down all video stream processes.");
76
+ void protectDevice.stream?.shutdown();
59
77
  }
60
78
  }
61
79
  });
62
80
  }
63
- // Configure a UniFi Protect device in HomeKit.
64
- configureDevice(accessory, device) {
81
+ // Maintain our connection to the UniFi Protect controller. This is only called after we've successfully logged in for the first time at plugin launch.
82
+ async ufpUpdate() {
83
+ // Periodically refresh our bootstrap of the Protect controller to ensure our state always remains in sync in between updates.
84
+ for (;;) {
85
+ // Gently continue to attempt bootstrapping the Protect controller until we're successful.
86
+ // eslint-disable-next-line no-await-in-loop
87
+ if (!(await this.ufpApi.getBootstrap())) {
88
+ // eslint-disable-next-line no-await-in-loop
89
+ await this.sleep(10 * 1000);
90
+ continue;
91
+ }
92
+ // Sync status and check for any new or removed accessories.
93
+ this.discoverAndSyncAccessories();
94
+ // Refresh the accessory cache.
95
+ this.api.updatePlatformAccessories(this.platform.accessories);
96
+ // Sleep and continue looping.
97
+ // eslint-disable-next-line no-await-in-loop
98
+ await this.sleep(PROTECT_CONTROLLER_REFRESH_INTERVAL * 1000);
99
+ }
100
+ }
101
+ // Initialize ourselves once we've bootstrapped the Protect controller for the first time.
102
+ ufpStartupHandler(bootstrap) {
103
+ this.ufp = bootstrap.nvr;
104
+ // Assign our name if the user hasn't explicitly specified a preference.
105
+ this.name = this.nvrOptions.name ?? this.ufp.name;
106
+ // This NVR has been disabled. Let the user know that we're done here.
107
+ if (!this.isEnabled && !this.optionEnabled(this.ufp, "Nvr")) {
108
+ this.log.info("Disabling this UniFi Protect controller.");
109
+ this.ufpApi.clearLoginCredentials();
110
+ return;
111
+ }
112
+ this.isEnabled = true;
113
+ // Configure any NVR-specific settings.
114
+ void this.configureNvr();
115
+ // Initialize our liveviews.
116
+ this.liveviews = new ProtectLiveviews(this);
117
+ // Initialize our NVR system information.
118
+ this.systemInfo = new ProtectNvrSystemInfo(this);
119
+ // Initialize MQTT, if needed.
120
+ if (!this.mqtt && this.config.mqttUrl) {
121
+ this.mqtt = new ProtectMqtt(this);
122
+ }
123
+ // Inform the user about the devices we see.
124
+ this.log.info("Discovered %s: %s.", this.ufp.modelKey, this.ufpApi.getDeviceName(this.ufp, this.ufp.name, true));
125
+ for (const device of [...bootstrap.cameras, ...bootstrap.lights, ...bootstrap.sensors, ...bootstrap.viewers]) {
126
+ // Filter out any devices that aren't adopted by this Protect controller.
127
+ if (device.isAdoptedByOther || device.isAdopting || !device.isAdopted) {
128
+ continue;
129
+ }
130
+ this.log.info("Discovered %s: %s.", device.modelKey, this.ufpApi.getDeviceName(device, device.name, true));
131
+ }
132
+ }
133
+ // Configure NVR-specific settings.
134
+ async configureNvr() {
135
+ // Configure the default doorbell message on the NVR.
136
+ await this.configureDefaultDoorbellMessage();
137
+ return true;
138
+ }
139
+ // Configure a default doorbell message on the Protect doorbell.
140
+ async configureDefaultDoorbellMessage() {
141
+ const defaultMessage = this.nvrOptions.defaultDoorbellMessage ?? "WELCOME";
142
+ // Set the default message.
143
+ const newUfp = await this.ufpApi.updateDevice(this.ufp, { doorbellSettings: { defaultMessageText: defaultMessage } });
144
+ if (!newUfp) {
145
+ this.log.error("Unable to set the default doorbell message. Please ensure this username has the Administrator role in UniFi Protect.");
146
+ return false;
147
+ }
148
+ // Update our internal view of the NVR configuration.
149
+ this.ufp = newUfp;
150
+ // Inform the user.
151
+ this.log.info("Default doorbell message set to: %s.", defaultMessage);
152
+ return true;
153
+ }
154
+ // Create instances of Protect device types in our plugin.
155
+ addProtectDevice(accessory, device) {
65
156
  if (!accessory || !device) {
66
157
  return false;
67
158
  }
@@ -69,380 +160,258 @@ class ProtectNvr {
69
160
  case "camera":
70
161
  // We have a UniFi Protect camera or doorbell.
71
162
  if (device.featureFlags.hasChime) {
72
- this.configuredDevices[accessory.UUID] = new protect_doorbell_1.ProtectDoorbell(this, accessory);
163
+ this.configuredDevices[accessory.UUID] = new ProtectDoorbell(this, device, accessory);
73
164
  }
74
165
  else {
75
- this.configuredDevices[accessory.UUID] = new protect_camera_1.ProtectCamera(this, accessory);
166
+ this.configuredDevices[accessory.UUID] = new ProtectCamera(this, device, accessory);
76
167
  }
77
168
  return true;
78
169
  break;
79
170
  case "light":
80
171
  // We have a UniFi Protect light.
81
- this.configuredDevices[accessory.UUID] = new protect_light_1.ProtectLight(this, accessory);
172
+ this.configuredDevices[accessory.UUID] = new ProtectLight(this, device, accessory);
82
173
  return true;
83
174
  break;
84
175
  case "sensor":
85
176
  // We have a UniFi Protect sensor.
86
- this.configuredDevices[accessory.UUID] = new protect_sensor_1.ProtectSensor(this, accessory);
177
+ this.configuredDevices[accessory.UUID] = new ProtectSensor(this, device, accessory);
87
178
  return true;
88
179
  break;
89
180
  case "viewer":
90
181
  // We have a UniFi Protect viewer.
91
- this.configuredDevices[accessory.UUID] = new protect_viewer_1.ProtectViewer(this, accessory);
182
+ this.configuredDevices[accessory.UUID] = new ProtectViewer(this, device, accessory);
92
183
  return true;
93
184
  break;
94
185
  default:
95
- this.log.error("%s: Unknown device class `%s` detected for ``%s``", this.nvrApi.getNvrName(), device.modelKey, device.name);
186
+ this.log.error("Unknown device class `%s` detected for ``%s``", device.modelKey, device.name);
96
187
  return false;
97
188
  }
98
189
  }
99
190
  // Discover UniFi Protect devices that may have been added to the NVR since we last checked.
100
191
  discoverDevices(devices) {
101
- var _a;
102
192
  // Iterate through the list of cameras that Protect has returned and sync them with what we show HomeKit.
103
- for (const device of devices !== null && devices !== void 0 ? devices : []) {
104
- // If we have no MAC address, name, or this camera isn't being managed by Protect, we skip.
105
- if (!device.mac || !device.name || device.isAdopting || !device.isAdopted) {
106
- continue;
107
- }
108
- // We only support certain devices.
109
- switch (device.modelKey) {
110
- case "camera":
111
- case "light":
112
- case "sensor":
113
- case "viewer":
114
- break;
115
- default:
116
- // If we've already informed the user about this one, we're done.
117
- if (this.unsupportedDevices[device.mac]) {
118
- continue;
119
- }
120
- // Notify the user we see this device, but we aren't adding it to HomeKit.
121
- this.unsupportedDevices[device.mac] = true;
122
- this.log.info("%s: UniFi Protect device type '%s' is not currently supported, ignoring: %s.", this.nvrApi.getNvrName(), device.modelKey, this.nvrApi.getDeviceName(device));
123
- continue;
124
- }
125
- // Exclude or include certain devices based on configuration parameters.
126
- if (!this.optionEnabled(device)) {
127
- continue;
128
- }
129
- // Generate this device's unique identifier.
130
- const uuid = this.hap.uuid.generate(device.mac);
131
- let accessory;
132
- // See if we already know about this accessory or if it's truly new. If it is new, add it to HomeKit.
133
- if ((accessory = this.platform.accessories.find(x => x.UUID === uuid)) === undefined) {
134
- accessory = new this.api.platformAccessory(device.name, uuid);
135
- this.log.info("%s: Adding %s to HomeKit.", this.nvrApi.getFullName(device), device.modelKey);
136
- // Register this accessory with homebridge and add it to the accessory array so we can track it.
137
- this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
138
- this.platform.accessories.push(accessory);
139
- }
140
- // Link the accessory to it's device object and it's hosting NVR.
141
- accessory.context.device = device;
142
- accessory.context.nvr = (_a = this.nvrApi.bootstrap) === null || _a === void 0 ? void 0 : _a.nvr.mac;
143
- // Setup the Protect device if it hasn't been configured yet.
144
- if (!this.configuredDevices[accessory.UUID]) {
145
- this.configureDevice(accessory, device);
146
- }
147
- else {
148
- // Device-specific periodic reconfiguration. We need to do this to reflect state changes in
149
- // the Protect NVR (e.g. device settings changes) that we want to catch. Many of the realtime
150
- // changes are sent through the realtime update API, but a few things aren't, so we deal with that
151
- // here.
152
- switch (device.modelKey) {
153
- case "camera":
154
- // Check if we have changes to the exposed RTSP streams on our cameras.
155
- void this.configuredDevices[accessory.UUID].configureVideoStream();
156
- // Check for changes to the doorbell LCD as well.
157
- if (device.featureFlags.hasLcdScreen) {
158
- void this.configuredDevices[accessory.UUID].configureDoorbellLcdSwitch();
159
- }
160
- break;
161
- case "viewer":
162
- // Sync the viewer state with HomeKit.
163
- void this.configuredDevices[accessory.UUID].updateDevice();
164
- break;
165
- default:
166
- break;
167
- }
168
- }
193
+ for (const device of devices) {
194
+ this.addHomeKitDevice(device);
169
195
  }
170
196
  return true;
171
197
  }
172
- // Discover and sync UniFi Protect devices between HomeKit and the Protect NVR.
198
+ // Add a newly detected Protect device to HomeKit.
199
+ addHomeKitDevice(device) {
200
+ // If we have no MAC address, name, or this camera isn't being managed by this Protect controller, we're done.
201
+ if (!device || !device.mac || !device.name || device.isAdoptedByOther || !device.isAdopted) {
202
+ return null;
203
+ }
204
+ // We only support certain devices.
205
+ switch (device.modelKey) {
206
+ case "camera":
207
+ case "light":
208
+ case "sensor":
209
+ case "viewer":
210
+ break;
211
+ default:
212
+ // If we've already informed the user about this one, we're done.
213
+ if (this.unsupportedDevices[device.mac]) {
214
+ return null;
215
+ }
216
+ // Notify the user we see this device, but we aren't adding it to HomeKit.
217
+ this.unsupportedDevices[device.mac] = true;
218
+ this.log.info("UniFi Protect device type '%s' is not currently supported, ignoring: %s.", device.modelKey, this.ufpApi.getDeviceName(device));
219
+ return null;
220
+ break;
221
+ }
222
+ // Exclude or include certain devices based on configuration parameters.
223
+ if (!this.optionEnabled(device, "Device")) {
224
+ return null;
225
+ }
226
+ // Generate this device's unique identifier.
227
+ const uuid = this.hap.uuid.generate(device.mac);
228
+ let accessory;
229
+ // See if we already know about this accessory or if it's truly new. If it is new, add it to HomeKit.
230
+ if ((accessory = this.platform.accessories.find(x => x.UUID === uuid)) === undefined) {
231
+ accessory = new this.api.platformAccessory(device.name, uuid);
232
+ this.log.info("%s: Adding %s to HomeKit.", this.ufpApi.getFullName(device), device.modelKey);
233
+ // Register this accessory with homebridge and add it to the accessory array so we can track it.
234
+ this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
235
+ this.platform.accessories.push(accessory);
236
+ this.api.updatePlatformAccessories(this.platform.accessories);
237
+ }
238
+ // Link the accessory to it's device object and it's hosting NVR.
239
+ accessory.context.nvr = this.ufp.mac;
240
+ // Locate our existing Protect device instance, if we have one.
241
+ const protectDevice = this.configuredDevices[accessory.UUID];
242
+ // Setup the Protect device if it hasn't been configured yet.
243
+ if (!protectDevice) {
244
+ this.addProtectDevice(accessory, device);
245
+ }
246
+ return protectDevice;
247
+ }
248
+ // Discover and sync UniFi Protect devices between HomeKit and the Protect controller.
173
249
  discoverAndSyncAccessories() {
174
- var _a, _b;
175
- if (this.nvrApi.cameras && !this.discoverDevices(this.nvrApi.cameras)) {
176
- this.log.error("%s: Error discovering camera devices.", this.nvrApi.getNvrName());
250
+ if (!this.ufpApi.bootstrap) {
251
+ return false;
177
252
  }
178
- if (this.nvrApi.lights && !this.discoverDevices(this.nvrApi.lights)) {
179
- this.log.error("%s: Error discovering light devices.", this.nvrApi.getNvrName());
253
+ if (this.ufpApi.bootstrap.cameras && !this.discoverDevices(this.ufpApi.bootstrap?.cameras)) {
254
+ this.log.error("Error discovering camera devices.");
180
255
  }
181
- if (this.nvrApi.sensors && !this.discoverDevices(this.nvrApi.sensors)) {
182
- this.log.error("%s: Error discovering sensor devices.", this.nvrApi.getNvrName());
256
+ if (this.ufpApi.bootstrap.lights && !this.discoverDevices(this.ufpApi.bootstrap?.lights)) {
257
+ this.log.error("Error discovering light devices.");
183
258
  }
184
- if (this.nvrApi.viewers && !this.discoverDevices(this.nvrApi.viewers)) {
185
- this.log.error("%s: Error discovering viewer devices.", this.nvrApi.getNvrName());
259
+ if (this.ufpApi.bootstrap.sensors && !this.discoverDevices(this.ufpApi.bootstrap?.sensors)) {
260
+ this.log.error("Error discovering sensor devices.");
261
+ }
262
+ if (this.ufpApi.bootstrap.viewers && !this.discoverDevices(this.ufpApi.bootstrap?.viewers)) {
263
+ this.log.error("Error discovering viewer devices.");
186
264
  }
187
265
  // Remove Protect devices that are no longer found on this Protect NVR, but we still have in HomeKit.
188
266
  this.cleanupDevices();
189
267
  // Configure our liveview-based accessories.
190
- (_a = this.liveviews) === null || _a === void 0 ? void 0 : _a.configureLiveviews();
191
- // Configure our NVR system information-related accessories.
192
- (_b = this.systemInfo) === null || _b === void 0 ? void 0 : _b.configureAccessory();
268
+ this.liveviews?.configureLiveviews();
269
+ // Update our viewer accessories.
270
+ Object.keys(this.configuredDevices)
271
+ .filter(x => this.configuredDevices[x].ufp.modelKey === "viewer")
272
+ .map(x => this.configuredDevices[x].updateDevice());
193
273
  return true;
194
274
  }
195
- // Update HomeKit with the latest status from Protect.
196
- async updateAccessories() {
197
- var _a;
198
- // Refresh the full device list from the Protect API.
199
- if (!(await this.nvrApi.refreshDevices())) {
200
- return false;
201
- }
202
- // This NVR has been disabled. Stop polling for updates and let the user know that we're done here.
203
- // Only run this check once, since we don't need to repeat it again.
204
- if (!this.isEnabled && !this.optionEnabled(null)) {
205
- this.log.info("%s: Disabling this Protect controller.", this.nvrApi.getNvrName());
206
- this.nvrApi.clearLoginCredentials();
207
- return true;
208
- }
209
- // Set a name for this NVR, if we haven't configured one for ourselves.
210
- if (!this.name && ((_a = this.nvrApi.bootstrap) === null || _a === void 0 ? void 0 : _a.nvr)) {
211
- this.name = this.nvrApi.bootstrap.nvr.name;
212
- }
213
- // If not already configured by the user, set the refresh interval here depending on whether we
214
- // have UniFi OS devices or not, since non-UniFi OS devices don't have a realtime API. We also
215
- // check to see whether doorbell devices have been removed and restore the prior refresh interval, if needed.
216
- let refreshUpdated = false;
217
- if (!this.refreshInterval || (!this.doorbellCount && (this.refreshInterval !== this.config.refreshInterval))) {
218
- if (!this.refreshInterval) {
219
- this.refreshInterval = this.config.refreshInterval = settings_1.PROTECT_NVR_UNIFIOS_REFRESH_INTERVAL;
275
+ // Cleanup removed Protect devices from HomeKit.
276
+ cleanupDevices() {
277
+ for (const accessory of this.platform.accessories) {
278
+ const protectDevice = this.configuredDevices[accessory.UUID];
279
+ // Check to see if we have an orphan - where we haven't configured this in the plugin, but the accessory still exists in HomeKit. One example of
280
+ // when this might happen is when Homebridge might be shutdown and a camera removed. When we start back up, the camera still exists in HomeKit but
281
+ // not in Protect. We catch those orphan devices here.
282
+ if (!protectDevice) {
283
+ // We only remove devices if they're on the Protect controller we're interested in.
284
+ if (("nvr" in accessory.context) && accessory.context.nvr !== this.ufp.mac) {
285
+ continue;
286
+ }
287
+ // We only store MAC addresses on devices that exist on the Protect controller. Any other accessories created are ones we created ourselves
288
+ // and are managed elsewhere.
289
+ if (!("mac" in accessory.context)) {
290
+ continue;
291
+ }
292
+ this.log.info("%s: Removing device from HomeKit.", accessory.displayName);
293
+ // Unregister the accessory and delete it's remnants from HomeKit.
294
+ this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
295
+ this.platform.accessories.splice(this.platform.accessories.indexOf(accessory), 1);
296
+ this.api.updatePlatformAccessories(this.platform.accessories);
297
+ continue;
220
298
  }
221
- else {
222
- this.refreshInterval = this.config.refreshInterval;
299
+ // If we don't have the Protect bootstrap JSON available, we're done. We need to know what's on the Protect controller in order to determine what
300
+ // to do with the accessories we know about.
301
+ if (!this.ufpApi.bootstrap) {
302
+ continue;
223
303
  }
224
- // In case someone puts in an overly aggressive default value.
225
- if (this.refreshInterval < 2) {
226
- this.refreshInterval = this.config.refreshInterval = 2;
304
+ // Check to see if the device still exists on the Protect controller and the user has not chosen to hide it.
305
+ switch (protectDevice.ufp.modelKey) {
306
+ case "camera":
307
+ if (this.ufpApi.bootstrap.cameras.some((x) => x.mac === protectDevice.ufp.mac) && this.optionEnabled(protectDevice.ufp, "Device")) {
308
+ continue;
309
+ }
310
+ break;
311
+ case "light":
312
+ if (this.ufpApi.bootstrap.lights.some((x) => x.mac === protectDevice.ufp.mac) && this.optionEnabled(protectDevice.ufp, "Device")) {
313
+ continue;
314
+ }
315
+ break;
316
+ case "sensor":
317
+ if (this.ufpApi.bootstrap.sensors.some((x) => x.mac === protectDevice.ufp.mac) && this.optionEnabled(protectDevice.ufp, "Device")) {
318
+ continue;
319
+ }
320
+ break;
321
+ case "viewer":
322
+ if (this.ufpApi.bootstrap.viewers.some((x) => x.mac === protectDevice.ufp.mac) && this.optionEnabled(protectDevice.ufp, "Device")) {
323
+ continue;
324
+ }
325
+ break;
326
+ default:
327
+ break;
227
328
  }
228
- refreshUpdated = true;
329
+ // Process the device removal.
330
+ this.removeHomeKitDevice(this.configuredDevices[accessory.UUID]);
229
331
  }
230
- if (refreshUpdated || !this.isEnabled) {
231
- // On startup or refresh interval change, we want to notify the user.
232
- this.log.info("%s: Controller refresh interval set to %s seconds.", this.nvrApi.getNvrName(), this.refreshInterval);
332
+ }
333
+ // Cleanup removed Protect devices from HomeKit.
334
+ removeHomeKitDevice(protectDevice) {
335
+ // Sanity check.
336
+ if (!protectDevice) {
337
+ return;
233
338
  }
234
- this.isEnabled = true;
235
- // Create an MQTT connection, if needed.
236
- if (!this.mqtt && this.config.mqttUrl) {
237
- this.mqtt = new protect_mqtt_1.ProtectMqtt(this);
339
+ // We only remove devices if they're on the Protect controller we're interested in.
340
+ if (protectDevice.accessory.context.nvr !== this.ufp.mac) {
341
+ return;
238
342
  }
239
- // Check for any updates to the events API connection.
240
- this.events.update();
241
- // Sync status and check for any new or removed accessories.
242
- this.discoverAndSyncAccessories();
243
- // Refresh the accessory cache.
244
- this.api.updatePlatformAccessories(this.platform.accessories);
245
- return true;
246
- }
247
- // Periodically poll the Protect API for status.
248
- async poll() {
249
- // Loop forever.
250
- for (;;) {
251
- // Sleep until our next update.
252
- // eslint-disable-next-line no-await-in-loop
253
- await this.sleep(this.refreshInterval * 1000);
254
- // Refresh our Protect device information and gracefully handle Protect errors.
255
- // eslint-disable-next-line no-await-in-loop
256
- if (await this.updateAccessories()) {
257
- // Our Protect NVR is disabled. We're done.
258
- if (!this.isEnabled) {
259
- return;
260
- }
343
+ // Package cameras are handled elsewhere.
344
+ if ("packageCamera" in protectDevice.accessory.context) {
345
+ return;
346
+ }
347
+ // The NVR system information accessory is handled elsewhere.
348
+ if ("systemInfo" in protectDevice.accessory.context) {
349
+ return;
350
+ }
351
+ // Liveview-centric accessories are handled elsewhere.
352
+ if (("liveview" in protectDevice.accessory.context) || protectDevice.accessory.getService(this.hap.Service.SecuritySystem)) {
353
+ return;
354
+ }
355
+ // Remove this device.
356
+ this.log.info("%s: Removing %s from HomeKit.", protectDevice.ufp.name ? this.ufpApi.getDeviceName(protectDevice.ufp) : protectDevice.accessory.displayName, protectDevice.ufp.modelKey ? protectDevice.ufp.modelKey : "device");
357
+ const deletingAccessories = [protectDevice.accessory];
358
+ // Check to see if we're removing a camera device that has a package camera as well.
359
+ if (protectDevice.ufp.modelKey === "camera") {
360
+ const protectCamera = protectDevice;
361
+ if (protectCamera && protectCamera.packageCamera) {
362
+ // Ensure we delete the accessory and cleanup after ourselves.
363
+ deletingAccessories.push(protectCamera.packageCamera.accessory);
364
+ this.platform.accessories.splice(this.platform.accessories.indexOf(protectCamera.packageCamera.accessory), 1);
365
+ protectCamera.packageCamera.cleanup();
366
+ protectCamera.packageCamera = null;
261
367
  }
262
368
  }
369
+ // Cleanup our event handlers.
370
+ protectDevice.cleanup();
371
+ // Unregister the accessory and delete it's remnants from HomeKit and the plugin.
372
+ this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, deletingAccessories);
373
+ delete this.configuredDevices[protectDevice.accessory.UUID];
374
+ this.platform.accessories.splice(this.platform.accessories.indexOf(protectDevice.accessory), 1);
375
+ this.api.updatePlatformAccessories(this.platform.accessories);
263
376
  }
264
- // Cleanup removed Protect devices from HomeKit.
265
- cleanupDevices() {
266
- var _a, _b, _c, _d, _e;
267
- const nvr = (_a = this.nvrApi.bootstrap) === null || _a === void 0 ? void 0 : _a.nvr;
268
- // If we don't have a valid bootstrap configuration, we're done here.
269
- if (!nvr) {
377
+ // Reauthenticate with the NVR and reset any HKSV timeshift buffers, as needed.
378
+ async resetNvrConnection() {
379
+ // Clear our login credentials.
380
+ this.ufpApi.clearLoginCredentials();
381
+ // Bootstrap the Protect NVR.
382
+ if (!(await this.ufpApi.getBootstrap())) {
270
383
  return;
271
384
  }
272
- for (const oldAccessory of this.platform.accessories) {
273
- const oldDevice = oldAccessory.context.device;
274
- const oldNvr = oldAccessory.context.nvr;
275
- // Since we're accessing the shared accessories list for the entire platform, we need to ensure we
276
- // are only touching our cameras and not another NVR's.
277
- if (oldNvr !== nvr.mac) {
278
- continue;
279
- }
280
- // The NVR system information accessory is handled elsewhere.
281
- if (("systemInfo" in oldAccessory.context)) {
282
- continue;
283
- }
284
- // Liveview-centric accessories are handled elsewhere.
285
- if (("liveview" in oldAccessory.context) || oldAccessory.getService(this.hap.Service.SecuritySystem)) {
385
+ // Inform all HKSV-enabled devices that are using a timeshift buffer to reset.
386
+ // protectCamera.hints.timeshift for timeshift buffer enablement.
387
+ //
388
+ for (const accessory of this.platform.accessories) {
389
+ // Retrieve the HBUP object for this device.
390
+ const protectDevice = this.configuredDevices[accessory.UUID];
391
+ // Ensure it's a camera device and that we have HKSV as well as timeshifting enabled.
392
+ if ((protectDevice?.ufp?.modelKey !== "camera") || !protectDevice.hasHksv || !protectDevice.hints.timeshift) {
286
393
  continue;
287
394
  }
288
- // We found this accessory and it's for this NVR. Figure out if we really want to see it in HomeKit.
289
- if (oldDevice) {
290
- // Check to see if the device still exists on the NVR and the user has not chosen to hide it.
291
- switch (oldDevice.modelKey) {
292
- case "camera":
293
- if (((_b = this.nvrApi.cameras) === null || _b === void 0 ? void 0 : _b.some((x) => x.mac === oldDevice.mac)) &&
294
- this.optionEnabled(oldDevice)) {
295
- continue;
296
- }
297
- break;
298
- case "light":
299
- if (((_c = this.nvrApi.lights) === null || _c === void 0 ? void 0 : _c.some((x) => x.mac === oldDevice.mac)) &&
300
- this.optionEnabled(oldDevice)) {
301
- continue;
302
- }
303
- break;
304
- case "sensor":
305
- if (((_d = this.nvrApi.sensors) === null || _d === void 0 ? void 0 : _d.some((x) => x.mac === oldDevice.mac)) &&
306
- this.optionEnabled(oldDevice)) {
307
- continue;
308
- }
309
- break;
310
- case "viewer":
311
- if (((_e = this.nvrApi.viewers) === null || _e === void 0 ? void 0 : _e.some((x) => x.mac === oldDevice.mac)) &&
312
- this.optionEnabled(oldDevice)) {
313
- continue;
314
- }
315
- break;
316
- default:
317
- break;
318
- }
319
- }
320
- // Decrement our doorbell count.
321
- if (oldAccessory.getService(this.hap.Service.Doorbell)) {
322
- this.doorbellCount--;
323
- }
324
- // Remove this device.
325
- this.log.info("%s %s: Removing %s from HomeKit.", this.nvrApi.getNvrName(), oldDevice ? this.nvrApi.getDeviceName(oldDevice) : oldAccessory.displayName, oldDevice ? oldDevice.modelKey : "device");
326
- // Unregister the accessory and delete it's remnants from HomeKit and the plugin.
327
- this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [oldAccessory]);
328
- delete this.configuredDevices[oldAccessory.UUID];
329
- this.platform.accessories.splice(this.platform.accessories.indexOf(oldAccessory), 1);
395
+ // Restart the timeshift buffer.
396
+ // eslint-disable-next-line no-await-in-loop
397
+ await protectDevice.stream.hksv?.restartTimeshifting();
330
398
  }
331
399
  }
332
- // Lookup a device by it's identifier and return the associated accessory, if any.
333
- accessoryLookup(deviceId) {
334
- if (!deviceId) {
335
- return undefined;
336
- }
337
- // Find the device in our list of accessories.
338
- const foundDevice = Object.keys(this.configuredDevices).find(x => this.configuredDevices[x].accessory.context.device.id === deviceId);
339
- return foundDevice ? this.configuredDevices[foundDevice].accessory : undefined;
400
+ // Lookup a device by it's identifier and return it if it exists.
401
+ deviceLookup(deviceId) {
402
+ // Find the device.
403
+ const foundDevice = Object.keys(this.configuredDevices).find(x => (this.configuredDevices[x].ufp).id === deviceId);
404
+ return foundDevice ? this.configuredDevices[foundDevice] : null;
340
405
  }
341
406
  // Utility function to let us know if a device or feature should be enabled or not.
342
407
  optionEnabled(device, option = "", defaultReturnValue = true, address = "", addressOnly = false) {
343
- var _a, _b, _c;
344
- // There are a couple of ways to enable and disable options. The rules of the road are:
345
- //
346
- // 1. Explicitly disabling, or enabling an option on the NVR propogates to all the devices
347
- // that are managed by that NVR. Why might you want to do this? Because...
348
- //
349
- // 2. Explicitly disabling, or enabling an option on a device by its MAC address will always
350
- // override the above. This means that it's possible to disable an option for an NVR,
351
- // and all the cameras that are managed by it, and then override that behavior on a single
352
- // camera that it's managing.
353
- const configOptions = (_a = this.platform) === null || _a === void 0 ? void 0 : _a.configOptions;
354
- // Nothing configured - we assume the default return value.
355
- if (!configOptions) {
356
- return defaultReturnValue;
357
- }
358
- // Upper case parameters for easier checks.
359
- option = option ? option.toUpperCase() : "";
360
- address = address ? address.toUpperCase() : "";
361
- const deviceMac = (device === null || device === void 0 ? void 0 : device.mac) ? device.mac.toUpperCase() : "";
362
- let optionSetting;
363
- // If we've specified an address parameter - we check for device and address-specific options before
364
- // anything else.
365
- if (address && option) {
366
- // Test for device-specific and address-specific option settings, used together.
367
- if (deviceMac) {
368
- optionSetting = option + "." + deviceMac + "." + address;
369
- // We've explicitly enabled this option for this device and address combination.
370
- if (configOptions.indexOf("ENABLE." + optionSetting) !== -1) {
371
- return true;
372
- }
373
- // We've explicitly disabled this option for this device and address combination.
374
- if (configOptions.indexOf("DISABLE." + optionSetting) !== -1) {
375
- return false;
376
- }
377
- }
378
- // Test for address-specific option settings only.
379
- optionSetting = option + "." + address;
380
- // We've explicitly enabled this option for this address.
381
- if (configOptions.indexOf("ENABLE." + optionSetting) !== -1) {
382
- return true;
383
- }
384
- // We've explicitly disabled this option for this address.
385
- if (configOptions.indexOf("DISABLE." + optionSetting) !== -1) {
386
- return false;
387
- }
388
- // We're only interested in address-specific options.
389
- if (addressOnly) {
390
- return false;
391
- }
392
- }
393
- // If we've specified a device, check for device-specific options first. Otherwise, we're dealing
394
- // with an NVR-specific or global option.
395
- if (deviceMac) {
396
- // First we test for camera-level option settings.
397
- // No option specified means we're testing to see if this device should be shown in HomeKit.
398
- optionSetting = option ? option + "." + deviceMac : deviceMac;
399
- // We've explicitly enabled this option for this device.
400
- if (configOptions.indexOf("ENABLE." + optionSetting) !== -1) {
401
- return true;
402
- }
403
- // We've explicitly disabled this option for this device.
404
- if (configOptions.indexOf("DISABLE." + optionSetting) !== -1) {
405
- return false;
406
- }
407
- }
408
- // If we don't have a managing device attached, we're done here.
409
- if (!((_c = (_b = this.nvrApi.bootstrap) === null || _b === void 0 ? void 0 : _b.nvr) === null || _c === void 0 ? void 0 : _c.mac)) {
410
- return defaultReturnValue;
411
- }
412
- // Now we test for NVR-level option settings.
413
- // No option specified means we're testing to see if this NVR (and it's attached devices) should be shown in HomeKit.
414
- const nvrMac = this.nvrApi.bootstrap.nvr.mac.toUpperCase();
415
- optionSetting = option ? option + "." + nvrMac : nvrMac;
416
- // We've explicitly enabled this option for this NVR and all the devices attached to it.
417
- if (configOptions.indexOf("ENABLE." + optionSetting) !== -1) {
418
- return true;
419
- }
420
- // We've explicitly disabled this option for this NVR and all the devices attached to it.
421
- if (configOptions.indexOf("DISABLE." + optionSetting) !== -1) {
422
- return false;
423
- }
424
- // Finally, let's see if we have a global option here.
425
- // No option means we're done - it's a special case for testing if an NVR or camera should be hidden in HomeKit.
426
- if (!option) {
427
- return defaultReturnValue;
428
- }
429
- // We've explicitly enabled this globally for all devices.
430
- if (configOptions.indexOf("ENABLE." + option) !== -1) {
431
- return true;
432
- }
433
- // We've explicitly disabled this globally for all devices.
434
- if (configOptions.indexOf("DISABLE." + option) !== -1) {
435
- return false;
436
- }
437
- // Nothing special to do - assume the option is defaultReturnValue.
438
- return defaultReturnValue;
408
+ return optionEnabled(this.platform.configOptions, this.ufp, device, option, defaultReturnValue, address, addressOnly);
439
409
  }
440
410
  // Utility function to return a configuration parameter for a Protect device.
441
411
  optionGet(device, option, address = "") {
442
- var _a, _b, _c, _d;
443
412
  // Using the same rules as we do to test for whether an option is enabled, retrieve options with parameters and
444
413
  // return them. If we don't find anything, we return undefined.
445
- const configOptions = (_a = this.platform) === null || _a === void 0 ? void 0 : _a.configOptions;
414
+ const configOptions = this.platform?.configOptions;
446
415
  // Nothing configured - we assume there's nothing.
447
416
  if (!configOptions || !option) {
448
417
  return undefined;
@@ -450,7 +419,7 @@ class ProtectNvr {
450
419
  // Upper case parameters for easier checks.
451
420
  address = address ? address.toUpperCase() : "";
452
421
  option = option.toUpperCase();
453
- const deviceMac = (_b = device === null || device === void 0 ? void 0 : device.mac.toUpperCase()) !== null && _b !== void 0 ? _b : null;
422
+ const deviceMac = device?.mac.toUpperCase() ?? null;
454
423
  let foundOption;
455
424
  let optionSetting;
456
425
  // If we've specified an address parameter - we check for device and address-specific options before
@@ -497,12 +466,12 @@ class ProtectNvr {
497
466
  }
498
467
  }
499
468
  // If we don't have a managing device attached, we're done here.
500
- if (!((_d = (_c = this.nvrApi.bootstrap) === null || _c === void 0 ? void 0 : _c.nvr) === null || _d === void 0 ? void 0 : _d.mac)) {
469
+ if (!this.ufp.mac) {
501
470
  return undefined;
502
471
  }
503
472
  // Now we test for NVR-level option settings.
504
473
  // No option specified means we're testing to see if this NVR (and it's attached devices) should be shown in HomeKit.
505
- const nvrMac = this.nvrApi.bootstrap.nvr.mac.toUpperCase();
474
+ const nvrMac = this.ufp.mac.toUpperCase();
506
475
  optionSetting = "ENABLE." + option + "." + nvrMac + ".";
507
476
  // We've explicitly enabled this option for this NVR and all the devices attached to it.
508
477
  if ((foundOption = configOptions.find(x => optionSetting === x.slice(0, optionSetting.length))) !== undefined) {
@@ -527,5 +496,4 @@ class ProtectNvr {
527
496
  return new Promise(resolve => setTimeout(resolve, ms));
528
497
  }
529
498
  }
530
- exports.ProtectNvr = ProtectNvr;
531
499
  //# sourceMappingURL=protect-nvr.js.map