homebridge-ring-secure-video 1.0.0-beta.7 → 1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,69 @@
1
1
  # homebridge-ring-secure-video
2
2
 
3
+ ## 1.0.1 (2026-11-14)
4
+
5
+ - Node.js 26 is supported (it became an LTS version in October 2026, and Homebridge 2.5 supports
6
+ it); CI tests Node 22, 24 and 26. On Node 26, npm warns that ring-client-api 14.3.0 doesn't list
7
+ Node 26 (`EBADENGINE`); the warning is harmless. Node 22 needs 22.12 or later, as Homebridge
8
+ does.
9
+
10
+ ## 1.0.0 (2026-11-14)
11
+
12
+ First stable release. Compared with homebridge-ring 14.3.0, it adds HomeKit Secure Video
13
+ recording with media profiles (`media`), a separate motion sensor per camera for instant
14
+ notifications, motion from Ring's event history when push notifications fail, automatic Ring
15
+ session refresh with an optional sensor, an optional recording on doorbell press, Ring Intercom
16
+ Video as a camera, and the fixes listed in the betas below. Every homebridge-ring setting is
17
+ available with the same name. Tested on a Raspberry Pi 4 (Node 22, Homebridge 2.5) with a Ring
18
+ Video Doorbell 3 Plus, Ring Alarm and 21 sensors.
19
+
20
+ Changes since 1.0.0-beta.7:
21
+
22
+ **Fixes:**
23
+
24
+ - A packet on a live view's audio port that isn't valid RTP no longer stops Homebridge (any
25
+ device on the network could send one). Any other error that would have reached rxjs's
26
+ unhandled-error handler (which stops Homebridge) is logged instead; if it ended a stream, such
27
+ as a live view's audio, only that stream stops.
28
+ - Ring Intercom Video: a pre-warmed live call that fails no longer stops Homebridge, and the
29
+ recording starts its own call; an unclaimed pre-warmed call is stopped after 20 s without
30
+ stopping a call a recording took meanwhile. Pre-warming happens only with HKSV recording on.
31
+ The check for an active ding runs only for Intercom Video (audio intercoms get dings by push),
32
+ every 5 s instead of 3 s, and a slow check no longer has its answer dropped when the next is
33
+ due.
34
+ - A renewed Ring token is saved only into this plugin's platform block. Before, if no block of
35
+ this plugin had the old token (it was changed in the settings meanwhile, or only another
36
+ plugin's block had it, such as homebridge-ring set up with the same token), it was written by
37
+ text replace, possibly into the other plugin's block; now config.json is left as it is, and
38
+ "Could not save a new Ring token" is logged once.
39
+ - When Homebridge shuts down, live views, talk-back, recordings and Ring calls are ended and the
40
+ plugin disconnects from Ring, instead of leaving them running until the process exits.
41
+ - With `codec` set to `auto` (the default outside `lowPower`), recordings on a PC, Docker, a NAS
42
+ or a Raspberry Pi 5 no longer fail by picking the Raspberry Pi 4 hardware encoder
43
+ (`h264_v4l2m2m`) just because FFmpeg lists it: it is picked only on Linux with a video encoder
44
+ device Homebridge can open, and `h264_videotoolbox` only on a Mac; otherwise `auto` picks
45
+ `libx264`. The check now asks the FFmpeg that records (the one bundled with the plugin, or the
46
+ one set with `ffmpegPath`) instead of a system `ffmpeg`, and gives up after 10 s instead of
47
+ holding recordings if FFmpeg hangs; a check that fails is logged and tried again 5 minutes
48
+ later. FFmpeg is only asked when `auto` has to choose (not with `lowPower` or a set codec).
49
+
50
+ **Settings form and docs:**
51
+
52
+ - "Custom FFmpeg Path" is in the settings form again (it was only in the JSON editor), and CI
53
+ now fails if a setting is left out of the form. Settings are grouped by topic, the hide-camera
54
+ settings have descriptions (hiding the camera motion sensor also stops HKSV recordings), and
55
+ the prebuffer setting no longer suggests it adds video from before the motion.
56
+ - The README shows the plugin's own icon instead of the "Homebridge x Ring" logo; Ring's logo
57
+ and unused upstream images are removed and no longer in the npm package. The README says which
58
+ setups are tested and which aren't.
59
+
60
+ **Other:** unused code removed; the encoder check runs FFmpeg once instead of twice.
61
+
62
+ **Known:** `npm audit --omit=dev` reports one high-severity advisory for `ip`
63
+ (GHSA-2p57-rm9w-gvfp), listed against `ip`, `werift` and ring-client-api 14.3.0. It concerns
64
+ `ip.isPublic`, which werift doesn't call, so the plugin isn't affected; it goes away with
65
+ ring-client-api 15 and the werift 0.26 it uses.
66
+
3
67
  ## 1.0.0-beta.7 (2026-11-14)
4
68
 
5
69
  **Changes in behaviour to check after updating:**
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img alt="Homebridge x Ring" src="branding/Homebridge_x_Ring.svg" width="500px">
2
+ <img alt="Ring Secure Video" src="https://raw.githubusercontent.com/zmanuel1234/homebridge-ring-secure-video/main/branding/homebridge-ring-secure-video.svg" width="150">
3
3
  </p>
4
4
 
5
5
  # homebridge-ring-secure-video
@@ -13,8 +13,7 @@ It is meant to replace `homebridge-ring` on its own: one plugin, one Ring sign-i
13
13
 
14
14
  ## What it adds to homebridge-ring
15
15
 
16
- Everything `homebridge-ring` exposes is here too (it is not a drop-in swap, though: the plugin and
17
- platform names differ, so its accessories are new to HomeKit and are set up again), plus:
16
+ Everything `homebridge-ring` exposes is here too, with the same settings, plus:
18
17
 
19
18
  - **HomeKit Secure Video recording** for Ring cameras and doorbells, with media profiles for small
20
19
  machines such as a Raspberry Pi (`media`).
@@ -105,8 +104,8 @@ Without `media.profile`, the profile is `adaptive`, so a config that only had
105
104
 
106
105
  ## Installation
107
106
 
108
- Requires Node.js 22 or 24 and Homebridge 1.8 or later (2.x recommended). If Homebridge is installed
109
- globally:
107
+ Requires Node.js 22, 24 or 26 and Homebridge 1.8 or later (tested with 2.5; 1.8 is supported but not
108
+ tested). If Homebridge is installed globally:
110
109
 
111
110
  ```bash
112
111
  npm i -g homebridge-ring-secure-video
@@ -161,11 +160,15 @@ Changing `homeKitAccessoryTag` updates both the exposed accessory name and the g
161
160
 
162
161
  ## HKSV Status
163
162
 
164
- HKSV support is experimental and actively evolving. Behavior may vary by camera model, wired vs battery power, Ring API changes, and FFmpeg environment.
163
+ Behavior may vary by camera model, wired vs battery power, Ring API changes, and FFmpeg
164
+ environment.
165
165
 
166
- Verified so far: complete HomeKit clips from a Ring Video Doorbell 3 Plus on a Raspberry Pi 4
167
- (Node 22, Homebridge 2) with each of `copy`, `h264_v4l2m2m` and `libx264` (see below). The HKSV fork this continues reported 3 cameras on an M4 Mac mini.
168
- Please report your experience and setup details to help improve support.
166
+ Tested: complete HomeKit clips from a Ring Video Doorbell 3 Plus on a Raspberry Pi 4 (Node 22,
167
+ Homebridge 2.5) with each of `copy`, `h264_v4l2m2m` and `libx264` (see below). The HKSV fork this
168
+ continues reported 3 cameras on an M4 Mac mini. Not tested here: other Ring cameras, battery
169
+ cameras, Ring Intercom Video, `h264_videotoolbox` and Homebridge 1.8. Please
170
+ [report](https://github.com/zmanuel1234/homebridge-ring-secure-video/issues) how it works with
171
+ your setup.
169
172
 
170
173
  Recordings follow the frame rate and audio format HomeKit asks for, and include audio only when
171
174
  **Record Audio** is on in the camera's settings in the Home app.
@@ -218,12 +221,15 @@ ignores all of them. The Pi's hardware encoder uses `bitrateKbps` and `keyframeI
218
221
  | Profile (`media.profile`) | `auto` picks |
219
222
  | --- | --- |
220
223
  | `lowPower` | `copy` |
221
- | `adaptive` (default) or `quality` | `h264_v4l2m2m` if FFmpeg has it (Raspberry Pi 4), else `h264_videotoolbox` if FFmpeg has it (Mac), else `libx264` |
224
+ | `adaptive` (default) or `quality` | `h264_v4l2m2m` on Linux with a usable video encoder device (Raspberry Pi 4), else `h264_videotoolbox` on a Mac, else `libx264` |
222
225
 
223
- "FFmpeg has it" means the encoder is built into FFmpeg, not that the hardware is there: on a
224
- Raspberry Pi 5 (no hardware encoder), set `codec` to `copy` or `libx264` rather than relying on
225
- `auto` in the `adaptive` and `quality` profiles. The log shows the choice for each recording:
226
- `Starting HKSV recording pipeline ... video=<codec>`.
226
+ A hardware encoder is picked only if the FFmpeg that records has it and the computer has the
227
+ hardware: `h264_v4l2m2m` needs a Linux video encoder device that Homebridge can open (the Pi 4's
228
+ `bcm2835-codec-encode`), so a Raspberry Pi 5, a PC or a NAS gets `libx264`, as does Docker unless
229
+ the device is passed through. The log shows the choice for each recording:
230
+ `Starting HKSV recording pipeline ... video=<codec>`. If FFmpeg doesn't answer when asked which
231
+ encoders it has, an error is logged (the first time) and `auto` records with `libx264` until FFmpeg
232
+ is asked again, at the first recording after 5 minutes.
227
233
 
228
234
  #### Settings verified on a Raspberry Pi 4
229
235
 
@@ -248,9 +254,6 @@ For a Raspberry Pi, start with:
248
254
  }
249
255
  ```
250
256
 
251
- ### Minimum specifications for HKSV:
252
- [TBD - will be added as more users test and report their setups]
253
-
254
257
  ## Troubleshooting
255
258
 
256
259
  For support and debugging:
@@ -5,7 +5,7 @@
5
5
  "customUi": true,
6
6
  "customUiPath": "./lib/homebridge-ui",
7
7
  "customUiDevServer": "http://localhost:3000/api/plugins/settings-ui/homebridge-ring-secure-video",
8
- "headerDisplay": "See [the wiki](https://github.com/dgreif/ring/wiki/Refresh-Tokens) for details on generating a refresh token",
8
+ "headerDisplay": "Sign in to Ring here to get a refresh token (\"Generate New Refresh Token\" signs in again or changes accounts). See the [README](https://github.com/zmanuel1234/homebridge-ring-secure-video#readme) for each setting.",
9
9
  "accountLinkingHeader": "Please supply your Ring credentials. These will be used to contact the Ring servers and generate a unique access token for your account. Your email and password will be sent directly to Ring and will not be stored anywhere.",
10
10
  "schema": {
11
11
  "type": "object",
@@ -107,7 +107,7 @@
107
107
  "type": "integer",
108
108
  "minimum": 4000,
109
109
  "maximum": 16000,
110
- "description": "How much pre-event video HomeKit retains. Minimum and default 4000 ms."
110
+ "description": "The pre-event length the plugin tells HomeKit it supports, 4000-16000 ms (default 4000). It doesn't add video from before the motion: the plugin keeps no video buffer and starts Ring's video when a recording begins, so leave this at the default."
111
111
  },
112
112
  "fragmentLengthMs": {
113
113
  "title": "Fragment length (ms)",
@@ -193,11 +193,13 @@
193
193
  },
194
194
  "hideCameraLight": {
195
195
  "title": "Hide Camera Light",
196
- "type": "boolean"
196
+ "type": "boolean",
197
+ "description": "Hides the light switch of cameras with a light (Floodlight Cam, Spotlight Cam)."
197
198
  },
198
199
  "hideCameraMotionSensor": {
199
200
  "title": "Hide Camera Motion Sensors",
200
- "type": "boolean"
201
+ "type": "boolean",
202
+ "description": "Hides each camera's motion sensor. HomeKit Secure Video records when that sensor reports motion, so hiding it also stops recordings (and makes Doorbell Press Starts a Recording do nothing)."
201
203
  },
202
204
  "cameraMotionNotificationSensor": {
203
205
  "title": "Separate Motion Sensor for Notifications",
@@ -212,16 +214,18 @@
212
214
  },
213
215
  "hideCameraSirenSwitch": {
214
216
  "title": "Hide Camera Siren Switch",
215
- "type": "boolean"
217
+ "type": "boolean",
218
+ "description": "Hides the siren switch of cameras with a siren."
216
219
  },
217
220
  "hideInHomeDoorbellSwitch": {
218
221
  "title": "Hide In-Home Doorbell Switch",
219
- "type": "boolean"
222
+ "type": "boolean",
223
+ "description": "Hides the switch that turns the in-home (mechanical or digital) chime of a wired Ring doorbell on or off."
220
224
  },
221
225
  "hideAlarmSirenSwitch": {
222
226
  "title": "Hide Alarm Siren Switch",
223
227
  "type": "boolean",
224
- "description": "Hides switch that allows you to turn on the siren of the Ring Alarm. Enable this is your Siri commands keep setting off the siren and you don't care about having the switch"
228
+ "description": "Hides switch that allows you to turn on the siren of the Ring Alarm. Enable this if your Siri commands keep setting off the siren and you don't care about having the switch"
225
229
  },
226
230
  "externalCameraIdSalt": {
227
231
  "title": "External Camera ID Salt",
@@ -231,7 +235,7 @@
231
235
  "showPanicButtons": {
232
236
  "title": "Show Panic Buttons",
233
237
  "type": "boolean",
234
- "description": "Creates a new `Panic Buttons` device in HomeKit with `Burglar Alarm` and `Fire Alarm` switches. **Use these at your own risk. I do not guarantee functionality in case of emergency, nor do I take responsibility for any false alarms**. These function just like the SOS sliders in the Ring app."
238
+ "description": "Creates a new `Panic Buttons` device in HomeKit with `Burglar Alarm` and `Fire Alarm` switches. **Use these at your own risk: they are not guaranteed to work in an emergency, and no responsibility is taken for false alarms.** These function just like the SOS sliders in the Ring app."
235
239
  },
236
240
  "ffmpegPath": {
237
241
  "title": "Custom FFmpeg Path",
@@ -256,7 +260,7 @@
256
260
  "avoidSnapshotBatteryDrain": {
257
261
  "title": "Avoid Snapshot Battery Drain",
258
262
  "type": "boolean",
259
- "description": "Causes snapshots for battery cameras to be fetched at a minimum 10 minute interval to avoid draining the battery. This is useful if have the Home app open constantly, causing frequent snapshot requests."
263
+ "description": "Causes snapshots for battery cameras to be fetched at a minimum 10 minute interval to avoid draining the battery. This is useful if you have the Home app open constantly, causing frequent snapshot requests."
260
264
  },
261
265
  "beamDurationSeconds": {
262
266
  "title": "Ring Smart Lighting Timer",
@@ -273,7 +277,7 @@
273
277
  "disableLogs": {
274
278
  "title": "Disable All Logging",
275
279
  "type": "boolean",
276
- "description": "Disables all logs generated by the Ring plugin"
280
+ "description": "Disables all logs generated by this plugin"
277
281
  },
278
282
  "cameraStatusPollingSeconds": {
279
283
  "title": "Camera/Chime Status Polling",
@@ -327,7 +331,7 @@
327
331
  },
328
332
  {
329
333
  "type": "fieldset",
330
- "title": "Optional Configurations:",
334
+ "title": "Optional Settings",
331
335
  "expandable": false,
332
336
  "items": [
333
337
  {
@@ -335,15 +339,42 @@
335
339
  "title": "HomeKit Secure Video",
336
340
  "expandable": true,
337
341
  "items": [
338
- "alarmOnEntryDelay",
339
342
  "enableHksv",
340
- "media",
341
- "forceRefreshRingPushCredentials",
342
- "forceRefreshRingApiSession",
343
+ "disableHksvOnBattery",
344
+ "media"
345
+ ]
346
+ },
347
+ {
348
+ "type": "fieldset",
349
+ "title": "Motion and Doorbell",
350
+ "expandable": true,
351
+ "items": [
352
+ "cameraMotionNotificationSensor",
353
+ "doorbellPressMotion",
354
+ "enableCameraMotionHistory"
355
+ ]
356
+ },
357
+ {
358
+ "type": "fieldset",
359
+ "title": "Ring Session",
360
+ "expandable": true,
361
+ "items": [
343
362
  "automaticSessionRefresh",
344
363
  "sessionRefreshSensor",
345
- "enableCameraMotionHistory",
346
- "disableHksvOnBattery"
364
+ "forceRefreshRingPushCredentials",
365
+ "forceRefreshRingApiSession"
366
+ ]
367
+ },
368
+ {
369
+ "type": "fieldset",
370
+ "title": "Alarm and Location Mode",
371
+ "expandable": true,
372
+ "items": [
373
+ "alarmOnEntryDelay",
374
+ "nightModeBypassFor",
375
+ "hideAlarmSirenSwitch",
376
+ "showPanicButtons",
377
+ "locationModePollingSeconds"
347
378
  ]
348
379
  },
349
380
  {
@@ -351,17 +382,20 @@
351
382
  "title": "Accessory Visibility",
352
383
  "expandable": true,
353
384
  "items": [
354
- "homeKitAccessoryTag",
355
385
  "hideLightGroups",
356
386
  "hideDoorbellSwitch",
357
387
  "hideCameraLight",
358
388
  "hideCameraMotionSensor",
359
- "cameraMotionNotificationSensor",
360
- "doorbellPressMotion",
361
389
  "hideCameraSirenSwitch",
362
- "hideInHomeDoorbellSwitch",
363
- "hideAlarmSirenSwitch",
364
- "showPanicButtons",
390
+ "hideInHomeDoorbellSwitch"
391
+ ]
392
+ },
393
+ {
394
+ "type": "fieldset",
395
+ "title": "HomeKit Identity (advanced)",
396
+ "expandable": true,
397
+ "items": [
398
+ "homeKitAccessoryTag",
365
399
  "externalCameraIdSalt"
366
400
  ]
367
401
  },
@@ -432,22 +466,21 @@
432
466
  },
433
467
  {
434
468
  "type": "help",
435
- "helpvalue": "<p>If you only want to display certain device types, you can specify a those device types here. All other device types will be removed from HomeKit. Example: security-panel to show your alarm, hp_cam_v1 to show floodlight cams.</p>"
469
+ "helpvalue": "<p>If you only want to display certain device types, you can specify those device types here. All other device types will be removed from HomeKit. Example: security-panel to show your alarm, hp_cam_v1 to show floodlight cams.</p>"
436
470
  }
437
471
  ]
438
472
  },
439
473
  {
440
474
  "type": "fieldset",
441
- "title": "Polling & Diagnostics",
475
+ "title": "Cameras, Lights and Diagnostics",
442
476
  "expandable": true,
443
477
  "items": [
444
478
  "avoidSnapshotBatteryDrain",
445
479
  "beamDurationSeconds",
446
- "debug",
447
- "disableLogs",
448
480
  "cameraStatusPollingSeconds",
449
- "locationModePollingSeconds",
450
- "nightModeBypassFor"
481
+ "ffmpegPath",
482
+ "debug",
483
+ "disableLogs"
451
484
  ]
452
485
  }
453
486
  ]
@@ -10,7 +10,7 @@ import { RtcpSenderInfo, RtcpSrPacket, RtpPacket, SrtpSession, SrtcpSession, } f
10
10
  import path from 'node:path';
11
11
  import { FragmentedMp4Parser } from "./fragmented-mp4-parser.js";
12
12
  import { getFfmpegCapabilities, } from "./ffmpeg-capabilities.js";
13
- import { getHksvRecordingResolutions, selectHksvVideoCodec } from "./hksv-options.js";
13
+ import { getHksvRecordingResolutions, hksvCodecNeedsCapabilities, selectHksvVideoCodec, } from "./hksv-options.js";
14
14
  import { hksvRecordingQueue } from "./hksv-work-queue.js";
15
15
  import { RingMediaIngress, } from "./ring-media-ingress.js";
16
16
  import { normalizeMediaConfig } from "./media-config.js";
@@ -128,7 +128,15 @@ class StreamingSessionWrapper {
128
128
  startStreamRequest.audio.packet_time, audioSrtpSession = new SrtpSession(getSessionConfig(this.audioSrtp));
129
129
  let runningTimestamp;
130
130
  this.transcodedAudioSplitter.addMessageHandler(({ message }) => {
131
- const rtp = RtpPacket.deSerialize(message);
131
+ let rtp;
132
+ try {
133
+ rtp = RtpPacket.deSerialize(message);
134
+ }
135
+ catch {
136
+ // Not a valid RTP packet (too short, or a bad header length): dropped, so it doesn't reach
137
+ // the rxjs unhandled-error path (logged per packet, or fatal if no handler is set).
138
+ return null;
139
+ }
132
140
  // For some reason HAP uses RFC 3550 timestamps instead of following RTP Paylod
133
141
  // Format for Opus Speech and Audio Codec from RFC 7587 like everyone else.
134
142
  // This calculates and replaces the timestamps before forwarding to Homekit.
@@ -374,6 +382,23 @@ export function isHksvEnabled(config, operatingOnBattery) {
374
382
  return (config.enableHksv !== false &&
375
383
  !(config.disableHksvOnBattery !== false && operatingOnBattery));
376
384
  }
385
+ /** Every camera's source, so they can all be stopped when Homebridge shuts down. */
386
+ const cameraSources = new Set();
387
+ /**
388
+ * Homebridge is shutting down: end every live view, recording and Ring call, so no FFmpeg (live
389
+ * view, talk-back, recording) is left running after the bridge stops.
390
+ */
391
+ export function shutDownCameraSources() {
392
+ for (const source of cameraSources) {
393
+ try {
394
+ source.shutdown();
395
+ }
396
+ catch (e) {
397
+ // One camera failing must not keep the others (or Ring's connection) open.
398
+ logError(e);
399
+ }
400
+ }
401
+ }
377
402
  export class CameraSource {
378
403
  controller;
379
404
  sessions = {};
@@ -391,6 +416,7 @@ export class CameraSource {
391
416
  this.ringCamera = ringCamera;
392
417
  this.config = config;
393
418
  this.mediaIngress = new RingMediaIngress(ringCamera);
419
+ cameraSources.add(this);
394
420
  const enableHksv = isHksvEnabled(config, ringCamera.operatingOnBattery);
395
421
  const controllerOptions = {
396
422
  cameraStreamCount: enableHksv ? 1 : 10,
@@ -551,12 +577,11 @@ export class CameraSource {
551
577
  this.previousLoadSnapshotPromise = undefined;
552
578
  }
553
579
  }
554
- fn = 1;
555
580
  async loadAndCacheSnapshot(imageUuid) {
556
581
  const start = Date.now();
557
582
  logDebug(`Loading new snapshot into cache for ${this.ringCamera.name}${imageUuid ? ' by uuid' : ''}`);
558
583
  try {
559
- const previousSnapshot = this.cachedSnapshot, newSnapshot = await this.ringCamera.getSnapshot({ uuid: imageUuid });
584
+ const newSnapshot = await this.ringCamera.getSnapshot({ uuid: imageUuid });
560
585
  this.cachedSnapshot = newSnapshot;
561
586
  this.cachedSnapshotAt = Date.now();
562
587
  logDebug(`Snapshot cached for ${this.ringCamera.name}${imageUuid ? ' by uuid' : ''} (${getDurationSeconds(start)}s)`);
@@ -591,7 +616,7 @@ export class CameraSource {
591
616
  }
592
617
  return this.cachedSnapshot;
593
618
  }
594
- async handleSnapshotRequest(request, callback) {
619
+ async handleSnapshotRequest(_request, callback) {
595
620
  try {
596
621
  const snapshot = await this.getCurrentSnapshot();
597
622
  if (!snapshot) {
@@ -771,7 +796,9 @@ export class CameraSource {
771
796
  releaseQueueSlot = () => queueLease.release();
772
797
  updateQueuedBytes = (bytes) => queueLease.setQueuedBytes(bytes);
773
798
  throwIfClosed();
774
- const capabilities = await getFfmpegCapabilities(), videoArguments = this.getHksvVideoArguments(capabilities), videoCodecIndex = videoArguments.indexOf('-vcodec'), selectedVideoCodec = videoCodecIndex >= 0
799
+ const capabilities = hksvCodecNeedsCapabilities(this.config)
800
+ ? await getFfmpegCapabilities()
801
+ : undefined, videoArguments = this.getHksvVideoArguments(capabilities), videoCodecIndex = videoArguments.indexOf('-vcodec'), selectedVideoCodec = videoCodecIndex >= 0
775
802
  ? String(videoArguments[videoCodecIndex + 1])
776
803
  : 'unknown';
777
804
  throwIfClosed();
@@ -907,6 +934,16 @@ export class CameraSource {
907
934
  releaseQueueSlot?.();
908
935
  }
909
936
  }
937
+ shutdown() {
938
+ for (const [sessionId, session] of Object.entries(this.sessions)) {
939
+ delete this.sessions[sessionId];
940
+ session.stop();
941
+ }
942
+ for (const streamId of [...this.activeRecordingSessions.keys()]) {
943
+ this.closeRecordingStream(streamId, 'Homebridge shutting down');
944
+ }
945
+ this.mediaIngress.shutdown();
946
+ }
910
947
  closeRecordingStream(streamId, reason) {
911
948
  logInfo(`HKSV recording stream closed for ${this.ringCamera.name} (streamId=${streamId}, reason=${String(reason)})`);
912
949
  this.closedRecordingStreams.add(streamId);
package/lib/camera.js CHANGED
@@ -227,7 +227,6 @@ export function addDoorbellPressMotion(motion, presses, onPressMotion, watchdogM
227
227
  }));
228
228
  }
229
229
  export class Camera extends BaseDataAccessory {
230
- inHomeDoorbellStatus;
231
230
  cameraSource;
232
231
  device;
233
232
  accessory;
@@ -346,9 +345,6 @@ export class Camera extends BaseDataAccessory {
346
345
  });
347
346
  }
348
347
  if (device.hasInHomeDoorbell && !config.hideInHomeDoorbellSwitch) {
349
- this.device.onInHomeDoorbellStatus.subscribe((data) => {
350
- this.inHomeDoorbellStatus = data;
351
- });
352
348
  this.registerObservableCharacteristic({
353
349
  characteristicType: Characteristic.On,
354
350
  serviceType: Service.Switch,
@@ -1,20 +1,33 @@
1
1
  import { execFile } from 'node:child_process';
2
+ import { accessSync, constants, readdirSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
2
4
  import { promisify } from 'node:util';
5
+ import { defaultFfmpegPath } from '@homebridge/camera-utils';
3
6
  import { getFfmpegPath } from 'ring-client-api/ffmpeg';
4
- import { logDebug } from 'ring-client-api/util';
7
+ import { logDebug, logError } from 'ring-client-api/util';
5
8
  const execFileAsync = promisify(execFile);
6
- let cachedCapabilities;
9
+ /** A probe that hangs must not hold every recording (they wait for it) forever. */
10
+ export const FFMPEG_PROBE_TIMEOUT_MS = 10_000;
11
+ let cachedCapabilities,
12
+ /** Set when the last check failed: when to ask FFmpeg again. */
13
+ retryAt, reportedFailure = false;
14
+ /** How long a failed check is kept, so a hanging FFmpeg doesn't hold every recording. */
15
+ export const FFMPEG_PROBE_RETRY_MS = 5 * 60_000;
16
+ /** The FFmpeg HKSV recordings run with: the plugin's ffmpegPath, or the bundled one. */
17
+ export function getRecordingFfmpegPath() {
18
+ return getFfmpegPath() || defaultFfmpegPath;
19
+ }
7
20
  function parseNames(output) {
8
21
  return new Set(output
9
22
  .split(/\s+/)
10
23
  .map((entry) => entry.trim())
11
24
  .filter(Boolean));
12
25
  }
13
- async function getFfmpegOutput(args) {
14
- const ffmpegPath = getFfmpegPath() || 'ffmpeg';
26
+ export async function getFfmpegOutput(args, ffmpegPath = getRecordingFfmpegPath(), timeout = FFMPEG_PROBE_TIMEOUT_MS) {
15
27
  try {
16
28
  const { stdout, stderr } = await execFileAsync(ffmpegPath, args, {
17
29
  maxBuffer: 1024 * 1024,
30
+ timeout,
18
31
  });
19
32
  return `${stdout}\n${stderr}`;
20
33
  }
@@ -22,14 +35,70 @@ async function getFfmpegOutput(args) {
22
35
  return `${e.stdout ?? ''}\n${e.stderr ?? ''}`;
23
36
  }
24
37
  }
38
+ /**
39
+ * The names of the Linux video devices Homebridge can open (a Raspberry Pi 4's encoder is
40
+ * "bcm2835-codec-encode"). A device listed in sysfs but not usable, as in Docker without the
41
+ * device passed through or without access to the video group, is left out.
42
+ */
43
+ export function getV4l2DeviceNames(sysfs = '/sys/class/video4linux', dev = '/dev') {
44
+ try {
45
+ return readdirSync(sysfs).flatMap((device) => {
46
+ try {
47
+ accessSync(join(dev, device), constants.R_OK | constants.W_OK);
48
+ return [readFileSync(join(sysfs, device, 'name'), 'utf8').trim()];
49
+ }
50
+ catch {
51
+ return [];
52
+ }
53
+ });
54
+ }
55
+ catch {
56
+ return [];
57
+ }
58
+ }
59
+ /**
60
+ * A video encoder device: not a JPEG/image encoder (the Pi 4 has both) or an MPEG capture card's
61
+ * ("ivtv0 encoder MPG").
62
+ */
63
+ export function isV4l2VideoEncoder(name) {
64
+ return /encode|-enc$/i.test(name) && !/image|jpeg|mpe?g/i.test(name);
65
+ }
66
+ /**
67
+ * FFmpeg builds list hardware encoders whether or not the hardware is there: the bundled FFmpeg
68
+ * and Ubuntu's list h264_v4l2m2m on any x86 machine, where encoding with it fails. So
69
+ * h264_v4l2m2m only counts on Linux with a usable V4L2 video encoder device (a Raspberry Pi 4 has
70
+ * one, a Pi 5 none), and h264_videotoolbox only on macOS.
71
+ */
72
+ export function getUsableEncoders(listed, platform = process.platform, v4l2DeviceNames = platform === 'linux' ? getV4l2DeviceNames() : []) {
73
+ const usable = new Set(listed);
74
+ if (platform !== 'linux' || !v4l2DeviceNames.some(isV4l2VideoEncoder)) {
75
+ usable.delete('h264_v4l2m2m');
76
+ }
77
+ if (platform !== 'darwin') {
78
+ usable.delete('h264_videotoolbox');
79
+ }
80
+ return usable;
81
+ }
25
82
  async function loadCapabilities() {
26
- const [encodersOutput, hwaccelsOutput] = await Promise.all([
27
- getFfmpegOutput(['-hide_banner', '-encoders']),
28
- getFfmpegOutput(['-hide_banner', '-hwaccels']),
29
- ]);
83
+ const encodersOutput = await getFfmpegOutput(['-hide_banner', '-encoders']);
84
+ if (/Encoders:/.test(encodersOutput)) {
85
+ retryAt = undefined;
86
+ }
87
+ else {
88
+ // FFmpeg didn't start or answer in time (a slow first start on a busy Pi): ask again a little
89
+ // later rather than record in software until a restart.
90
+ retryAt = Date.now() + FFMPEG_PROBE_RETRY_MS;
91
+ const message = `Could not list the encoders of FFmpeg (${getRecordingFfmpegPath()}); codec auto records with libx264 until it answers (asked again after ${FFMPEG_PROBE_RETRY_MS / 60_000} minutes)`;
92
+ if (reportedFailure) {
93
+ logDebug(message);
94
+ }
95
+ else {
96
+ reportedFailure = true;
97
+ logError(message);
98
+ }
99
+ }
30
100
  const capabilities = {
31
- encoders: parseNames(encodersOutput),
32
- hwaccels: parseNames(hwaccelsOutput),
101
+ encoders: getUsableEncoders(parseNames(encodersOutput)),
33
102
  };
34
103
  logDebug(`FFmpeg HKSV capabilities: encoders=${[
35
104
  'h264_v4l2m2m',
@@ -41,6 +110,10 @@ async function loadCapabilities() {
41
110
  return capabilities;
42
111
  }
43
112
  export function getFfmpegCapabilities() {
113
+ if (retryAt !== undefined && Date.now() >= retryAt) {
114
+ retryAt = undefined;
115
+ cachedCapabilities = undefined;
116
+ }
44
117
  cachedCapabilities ??= loadCapabilities();
45
118
  return cachedCapabilities;
46
119
  }
@@ -15,6 +15,11 @@ export function getHksvRecordingResolutions(config) {
15
15
  [320, 240, 15],
16
16
  ];
17
17
  }
18
+ /** Only auto outside lowPower asks FFmpeg which encoders it has. */
19
+ export function hksvCodecNeedsCapabilities(config) {
20
+ const media = normalizeMediaConfig(config);
21
+ return media.recording.codec === 'auto' && media.profile !== 'lowPower';
22
+ }
18
23
  export function selectHksvVideoCodec(configuredCodec, capabilities, config) {
19
24
  const media = normalizeMediaConfig(config);
20
25
  const codec = configuredCodec ?? media.recording.codec;
@@ -20,18 +20,6 @@ export class ResourceGovernor {
20
20
  this.queueTimeoutMs = normalizeTimeout(options.queueTimeoutMs);
21
21
  this.drain();
22
22
  }
23
- /**
24
- * Compatibility entry point for v14 callers. New platform code should call
25
- * configure() once at startup instead.
26
- */
27
- setConcurrency(concurrency) {
28
- this.configure({ recordingConcurrency: concurrency });
29
- }
30
- /** Compatibility API returning only an idempotent release function. */
31
- async acquire(signal) {
32
- const lease = await this.acquireLease({ signal });
33
- return () => lease.release();
34
- }
35
23
  acquireLease(request = {}) {
36
24
  const signal = request.signal;
37
25
  if (signal?.aborted) {
package/lib/intercom.js CHANGED
@@ -2,9 +2,9 @@ import { RingCamera } from 'ring-client-api';
2
2
  import { hap } from "./hap.js";
3
3
  import { BaseDataAccessory } from "./base-data-accessory.js";
4
4
  import { logError, logInfo } from 'ring-client-api/util';
5
- import { filter, map, share, switchMap, throttleTime } from 'rxjs/operators';
6
- import { interval, merge, Subject } from 'rxjs';
7
- import { CameraSource } from "./camera-source.js";
5
+ import { exhaustMap, filter, map, share, throttleTime } from 'rxjs/operators';
6
+ import { EMPTY, interval, merge, Subject } from 'rxjs';
7
+ import { CameraSource, isHksvEnabled } from "./camera-source.js";
8
8
  /**
9
9
  * Creates a RingCamera-compatible proxy for a Ring Intercom Video device.
10
10
  * Ring Intercom Video is classified as RingIntercom by ring-client-api but has a
@@ -86,20 +86,32 @@ function createIntercomVideoProxy(device) {
86
86
  if (preWarmedCall)
87
87
  return;
88
88
  logInfo(`${device.name}: pre-warming camera connection`);
89
- preWarmedCall = realStartLiveCall();
90
- // Clean up if HKSV doesn't claim it within 20 seconds
91
- preWarmCleanup = setTimeout(async () => {
92
- if (preWarmedCall) {
93
- try {
94
- const session = await preWarmedCall;
95
- session.stop();
96
- }
97
- catch {
98
- // ignore: the pre-warmed call may have already failed or ended
99
- }
100
- preWarmedCall = null;
89
+ const call = realStartLiveCall();
90
+ preWarmedCall = call;
91
+ // Handled here, so a call that fails before anything awaits it isn't an unhandled rejection
92
+ // (which stops Homebridge); a recording then starts its own call instead of this failed one.
93
+ call.catch((e) => {
94
+ if (preWarmedCall !== call)
95
+ return;
96
+ logError(`${device.name}: pre-warmed camera connection failed`);
97
+ logError(e);
98
+ preWarmedCall = null;
99
+ if (preWarmCleanup) {
100
+ clearTimeout(preWarmCleanup);
101
101
  preWarmCleanup = null;
102
102
  }
103
+ });
104
+ // Clean up if HKSV doesn't claim it within 20 seconds. Taken out of the slot first, so a
105
+ // recording arriving while it still connects starts its own call rather than get this one
106
+ // stopped under it, and a newer pre-warm isn't cleared by this one's timer.
107
+ preWarmCleanup = setTimeout(() => {
108
+ if (preWarmedCall !== call)
109
+ return;
110
+ preWarmedCall = null;
111
+ preWarmCleanup = null;
112
+ call
113
+ .then((session) => session.stop())
114
+ .catch(() => undefined); // a failed call has nothing to stop
103
115
  }, 20000);
104
116
  });
105
117
  // Override getSnapshot: intercom uses the same doorbots snapshot endpoint as cameras.
@@ -124,6 +136,8 @@ function createIntercomVideoProxy(device) {
124
136
  });
125
137
  return proxy;
126
138
  }
139
+ /** How often Intercom Video asks Ring for an active ding (its ding pushes don't arrive). */
140
+ const DING_POLL_INTERVAL_MS = 5000;
127
141
  export class Intercom extends BaseDataAccessory {
128
142
  unlocking = false;
129
143
  unlockTimeout;
@@ -161,29 +175,32 @@ export class Intercom extends BaseDataAccessory {
161
175
  // notifications (different category than RingIntercom.onDing expects),
162
176
  // so onDing never fires. Poll Ring's active-dings endpoint as a reliable
163
177
  // fallback — it reflects the current ring in real time (unlike the history
164
- // endpoint which can lag by ~60 seconds).
178
+ // endpoint which can lag by ~60 seconds). Only Intercom Video needs it; a request
179
+ // still running when the next one is due skips that one rather than piling up.
165
180
  onDingDetected = (() => {
166
181
  let lastDingId;
167
- const polled = interval(3000).pipe(switchMap(async () => {
168
- try {
169
- const active = await device.restClient.request({
170
- url: 'https://api.ring.com/clients_api/dings/active',
171
- });
172
- return Array.isArray(active) ? active : [];
173
- }
174
- catch {
175
- return [];
176
- }
177
- }), filter((active) => {
178
- const ding = active.find((d) => String(d.doorbot_id) === String(device.id) &&
179
- d.kind === 'ding');
180
- if (!ding)
181
- return false;
182
- if (String(ding.id) === lastDingId)
183
- return false;
184
- lastDingId = String(ding.id);
185
- return true;
186
- }), map(() => undefined));
182
+ const polled = !isIntercomVideo
183
+ ? EMPTY
184
+ : interval(DING_POLL_INTERVAL_MS).pipe(exhaustMap(async () => {
185
+ try {
186
+ const active = await device.restClient.request({
187
+ url: 'https://api.ring.com/clients_api/dings/active',
188
+ });
189
+ return Array.isArray(active) ? active : [];
190
+ }
191
+ catch {
192
+ return [];
193
+ }
194
+ }), filter((active) => {
195
+ const ding = active.find((d) => String(d.doorbot_id) === String(device.id) &&
196
+ d.kind === 'ding');
197
+ if (!ding)
198
+ return false;
199
+ if (String(ding.id) === lastDingId)
200
+ return false;
201
+ lastDingId = String(ding.id);
202
+ return true;
203
+ }), map(() => undefined));
187
204
  // share() makes this hot: one polling interval, one lastDingId,
188
205
  // emission fans out to all subscribers (doorbell service + auto-open)
189
206
  return merge(device.onDing, polled).pipe(share());
@@ -279,7 +296,11 @@ export class Intercom extends BaseDataAccessory {
279
296
  });
280
297
  // Pre-warm the camera WebRTC connection on every ding so HKSV recording
281
298
  // gets live video instead of black frames.
282
- if (intercomVideoProxy) {
299
+ // Only with HKSV: recordings are what it is for (a live view opened within 20 s would also
300
+ // use it), and without HKSV it would open a Ring live call on every ding that usually nothing
301
+ // claims.
302
+ if (intercomVideoProxy &&
303
+ isHksvEnabled(config, intercomVideoProxy.operatingOnBattery)) {
283
304
  const proxy = intercomVideoProxy;
284
305
  onDingDetected.subscribe(() => {
285
306
  ;
@@ -1,5 +1,5 @@
1
- import { defaultFfmpegPath, reservePorts, RtpSplitter, } from '@homebridge/camera-utils';
2
- import { getFfmpegPath } from 'ring-client-api/ffmpeg';
1
+ import { reservePorts, RtpSplitter } from '@homebridge/camera-utils';
2
+ import { getRecordingFfmpegPath } from "./ffmpeg-capabilities.js";
3
3
  import { logDebug, logError, logInfo } from 'ring-client-api/util';
4
4
  import { Subscription } from 'rxjs';
5
5
  import { take } from 'rxjs/operators';
@@ -125,7 +125,6 @@ export class RingMediaIngress {
125
125
  return {
126
126
  session,
127
127
  subscribeVideo: (handler) => session.onVideoRtp.subscribe((packet) => handler(clonePacket(packet))),
128
- subscribeAudio: (handler) => session.onAudioRtp.subscribe((packet) => handler(clonePacket(packet))),
129
128
  createTranscoder: (options) => this.createTranscoder(session, options),
130
129
  release: () => {
131
130
  if (released)
@@ -274,7 +273,7 @@ export class RingMediaIngress {
274
273
  }
275
274
  releaseFfmpeg = hksvRecordingQueue.trackFfmpegProcess();
276
275
  ffmpeg = new ManagedFfmpegProcess({
277
- ffmpegPath: getFfmpegPath() || defaultFfmpegPath,
276
+ ffmpegPath: getRecordingFfmpegPath(),
278
277
  ffmpegArgs: [
279
278
  '-hide_banner', '-protocol_whitelist', 'pipe,udp,rtp,file,crypto',
280
279
  ...(usingOpus ? ['-acodec', 'libopus'] : []),
@@ -19,6 +19,7 @@ import { Fan } from "./fan.js";
19
19
  import { Outlet } from "./outlet.js";
20
20
  import { Switch } from "./switch.js";
21
21
  import { Camera } from "./camera.js";
22
+ import { shutDownCameraSources } from "./camera-source.js";
22
23
  import { PushHealthMonitor, readPushRecoveryState, writePushRecoveryState, } from "./push-health.js";
23
24
  import { readFileSync } from 'fs';
24
25
  import { getSessionRefreshSensorUuid, SessionRefreshSensor, } from "./session-refresh-sensor.js";
@@ -36,6 +37,16 @@ import { Valve } from "./valve.js";
36
37
  import { getRemovedSettings, sanitizeMediaConfig } from "./media-config.js";
37
38
  import { configureHksvResourceGovernor } from "./hksv-work-queue.js";
38
39
  import { getMotionNotificationSensorUuid, setUpMotionNotificationSensor, } from "./motion-notification-sensor.js";
40
+ import { config as rxjsConfig } from 'rxjs';
41
+ // rxjs rethrows an error from a subscriber that has no error handler on a timer, where nothing
42
+ // can catch it, and that stops Homebridge and every accessory on it. Log it instead. If the error
43
+ // ended a stream (an operator such as map threw), that stream stays ended, e.g. a live view's
44
+ // audio; a subscriber that throws keeps receiving values.
45
+ // On the main bridge plugins can share one rxjs, so a handler another plugin set is kept.
46
+ rxjsConfig.onUnhandledError ??= (e) => {
47
+ logError('Unhandled error (logged instead of stopping Homebridge):');
48
+ logError(e);
49
+ };
39
50
  const ignoreHiddenDeviceTypes = [
40
51
  RingDeviceType.RingNetAdapter,
41
52
  RingDeviceType.ZigbeeAdapter,
@@ -137,23 +148,28 @@ function removePushCredentialsFromRefreshToken(refreshToken, removeHardwareId =
137
148
  }
138
149
  }
139
150
  function updateRefreshTokenInConfig(configContents, oldRefreshToken, newRefreshToken, clearRefreshFlags) {
151
+ let homebridgeConfig;
140
152
  try {
141
- const homebridgeConfig = JSON.parse(configContents), platformConfig = homebridgeConfig.platforms?.find((platform) => (platform.platform === platformName ||
142
- platform.platform === `${pluginName}.${platformName}`) &&
143
- platform.refreshToken === oldRefreshToken);
144
- if (!platformConfig) {
145
- throw new Error('Ring platform config not found');
146
- }
147
- platformConfig.refreshToken = newRefreshToken;
148
- if (clearRefreshFlags) {
149
- platformConfig.forceRefreshRingPushCredentials = false;
150
- platformConfig.forceRefreshRingApiSession = false;
151
- }
152
- return `${JSON.stringify(homebridgeConfig, null, 4)}\n`;
153
+ homebridgeConfig = JSON.parse(configContents);
153
154
  }
154
155
  catch {
156
+ // Not JSON Homebridge could read either; replace the token where it is.
155
157
  return configContents.replace(oldRefreshToken, newRefreshToken);
156
158
  }
159
+ const platformConfig = homebridgeConfig.platforms?.find((platform) => (platform.platform === platformName ||
160
+ platform.platform === `${pluginName}.${platformName}`) &&
161
+ platform.refreshToken === oldRefreshToken);
162
+ if (!platformConfig) {
163
+ // The token was changed in the config meanwhile, or is only in another plugin's block (a
164
+ // homebridge-ring set up with the same token): leave the file as it is.
165
+ return configContents;
166
+ }
167
+ platformConfig.refreshToken = newRefreshToken;
168
+ if (clearRefreshFlags) {
169
+ platformConfig.forceRefreshRingPushCredentials = false;
170
+ platformConfig.forceRefreshRingApiSession = false;
171
+ }
172
+ return `${JSON.stringify(homebridgeConfig, null, 4)}\n`;
157
173
  }
158
174
  /**
159
175
  * This platform's block as saved in config.json now, if exactly one block matches and it has a
@@ -338,6 +354,13 @@ export class RingPlatform {
338
354
  this.shuttingDown = true;
339
355
  clearTimeout(this.retryTimer);
340
356
  this.sessionRefreshSensor?.stop();
357
+ shutDownCameraSources();
358
+ try {
359
+ this.ringApi?.disconnect();
360
+ }
361
+ catch (e) {
362
+ logError(e);
363
+ }
341
364
  });
342
365
  if (config.automaticSessionRefresh !== false) {
343
366
  this.pushHealth = new PushHealthMonitor(api.user.storagePath(), () => this.refreshSessionAutomatically());
@@ -364,6 +387,7 @@ export class RingPlatform {
364
387
  retryTimer;
365
388
  shuttingDown = false;
366
389
  devicesSetUp = false;
390
+ ringApi;
367
391
  pushHealth;
368
392
  gotNewToken = false;
369
393
  sessionRefreshSensor;
@@ -511,6 +535,7 @@ export class RingPlatform {
511
535
  return;
512
536
  }
513
537
  this.devicesSetUp = true;
538
+ this.ringApi = ringApi;
514
539
  const cachedAccessoryIds = Object.keys(this.homebridgeAccessories), platformAccessories = [], externalAccessories = [], activeAccessoryIds = [];
515
540
  logInfo('Found the following locations:');
516
541
  locations.forEach((location) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-ring-secure-video",
3
3
  "displayName": "Ring Secure Video",
4
- "version": "1.0.0-beta.7",
4
+ "version": "1.0.1",
5
5
  "description": "Homebridge plugin for Ring devices with HomeKit Secure Video recording, based on homebridge-ring and homebridge-ring-hksv",
6
6
  "type": "module",
7
7
  "main": "./lib/index.js",
@@ -36,7 +36,7 @@
36
36
  "typescript-eslint": "8.72.2"
37
37
  },
38
38
  "engines": {
39
- "node": "^22 || ^24",
39
+ "node": "^22.12 || ^24 || ^26",
40
40
  "homebridge": "^1.8.0 || ^2.0.0-beta.0"
41
41
  },
42
42
  "homebridge": {
@@ -58,7 +58,6 @@
58
58
  },
59
59
  "homepage": "https://github.com/zmanuel1234/homebridge-ring-secure-video#readme",
60
60
  "files": [
61
- "branding",
62
61
  "CHANGELOG.md",
63
62
  "config.schema.json",
64
63
  "lib",
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1800" height="630" viewBox="0 0 1800 630"><defs><style>.cls-1{isolation:isolate;}.cls-2{fill:#535556;}.cls-3{fill:#1c9ad6;}.cls-4{fill:#9596ae;}.cls-5{fill:url(#linear-gradient);}.cls-6{fill:url(#radial-gradient);}.cls-7{opacity:0.25;mix-blend-mode:multiply;}.cls-8{fill:#fff;}.cls-9{fill:none;stroke:#9596ae;stroke-linecap:round;stroke-miterlimit:10;stroke-width:5px;}</style><linearGradient id="linear-gradient" x1="450" y1="86.48" x2="450" y2="386.48" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#57277c"/><stop offset="1" stop-color="#491f59"/></linearGradient><radialGradient id="radial-gradient" cx="450" cy="236.48" r="150" gradientUnits="userSpaceOnUse"><stop offset="0" stop-opacity="0"/><stop offset="1" stop-opacity="0.1"/></radialGradient></defs><g class="cls-1"><g id="Plugin_Logo" data-name="Plugin Logo"><path class="cls-2" d="M1210.33,243.93a9.27,9.27,0,0,1-2.9,6.77,9.61,9.61,0,0,1-7.08,2.82,10,10,0,0,1-6.77-2.9c-7.1-7.1-18.76-9.94-26.38-9.91q-17.39,0-27.2,11.9t-9.83,33.79V391.09a8.7,8.7,0,0,1-3.05,7.25,10.06,10.06,0,0,1-13.18,0,8.62,8.62,0,0,1-3-7.25V286.4a86.7,86.7,0,0,1,3.54-25.25,60.49,60.49,0,0,1,10.62-20.58,50.73,50.73,0,0,1,17.53-14.09,55.09,55.09,0,0,1,24.59-5.14,61.34,61.34,0,0,1,21.25,4.18,51.37,51.37,0,0,1,18.66,11.58,9.58,9.58,0,0,1,3.25,6.84Z"/><path class="cls-2" d="M1258.57,391.08a8.68,8.68,0,0,1-3,7.26,10.22,10.22,0,0,1-13.21,0,8.68,8.68,0,0,1-3-7.26V234.27a8.67,8.67,0,0,1,3-7.25,10.24,10.24,0,0,1,13.21,0,8.69,8.69,0,0,1,3,7.25Z"/><path class="cls-2" d="M1426,391.08a8.72,8.72,0,0,1-3.07,7.26,10.24,10.24,0,0,1-13.19,0,8.64,8.64,0,0,1-3-7.26V284.16a42.28,42.28,0,0,0-3.4-16.89,43.47,43.47,0,0,0-80.11,0,42.25,42.25,0,0,0-3.37,16.89V391.08a8.68,8.68,0,0,1-3,7.26,10.22,10.22,0,0,1-13.21,0,8.68,8.68,0,0,1-3-7.26V284.16a61.22,61.22,0,0,1,4.92-24.47,63.29,63.29,0,0,1,13.35-20,64,64,0,0,1,19.68-13.35,62.85,62.85,0,0,1,49.18,0,61.7,61.7,0,0,1,33.29,33.29,61,61,0,0,1,4.92,24.47Z"/><path class="cls-2" d="M1590,402.34a60.9,60.9,0,0,1-4.91,24.59,62.18,62.18,0,0,1-33.37,33.15,61.22,61.22,0,0,1-24.47,4.92,73.94,73.94,0,0,1-14.16-1.45,81.45,81.45,0,0,1-14.76-4.35,64.35,64.35,0,0,1-13.5-7.38,40.48,40.48,0,0,1-10.13-10.3,9.26,9.26,0,0,1-1.6-5.49,8.83,8.83,0,0,1,3-6.91,10,10,0,0,1,6.91-2.75,11,11,0,0,1,4,.81,7.79,7.79,0,0,1,3.71,3.37c3.83,5.76,12.08,10.32,17.54,12.07a61,61,0,0,0,18.81,3.08,42.45,42.45,0,0,0,16.89-3.4,43.67,43.67,0,0,0,23.16-23.16,42.24,42.24,0,0,0,3.52-16.8V380.21a62.5,62.5,0,0,1-19.67,12.71,61.72,61.72,0,0,1-23.8,4.68,61,61,0,0,1-24.6-4.92,62.63,62.63,0,0,1-33.14-33.15,60.57,60.57,0,0,1-4.92-24.59V284.16a61,61,0,0,1,4.92-24.47,62.9,62.9,0,0,1,13.35-20,63.91,63.91,0,0,1,19.67-13.35,62.85,62.85,0,0,1,49.18,0,62.51,62.51,0,0,1,38.22,57.76v118.2Zm-19.3-118.18a42.27,42.27,0,0,0-3.39-16.89,43.58,43.58,0,0,0-80.22,0,42.68,42.68,0,0,0-3.37,16.9v50.53a42.7,42.7,0,0,0,3.37,17,43.72,43.72,0,0,0,9.35,13.84,43.19,43.19,0,0,0,13.84,9.32,43.47,43.47,0,0,0,57-23.16,42.38,42.38,0,0,0,3.47-17Z"/><circle class="cls-3" cx="1248.94" cy="181.5" r="16.5"/></g><g id="Homebridge"><g id="Text"><path class="cls-4" d="M136.23,471.8h29V440.67H178v75.9H165.27V482.8h-29v33.77H123.47v-75.9h12.76Z"/><path class="cls-4" d="M222.8,517.56c-16.94,0-28.38-10.23-28.38-34.1v-9.68c0-23.87,11.44-34.1,28.38-34.1s28.49,10.23,28.49,34.1v9.68C251.29,507.33,239.85,517.56,222.8,517.56Zm-15.62-34.1c0,16.39,5.5,23,15.62,23s15.73-6.6,15.73-23v-9.68c0-16.39-5.61-23-15.73-23s-15.62,6.6-15.62,23Z"/><path class="cls-4" d="M301.67,484.67l21.89-44h12.21v75.9H323.45V465.75l-17.38,36.19h-9L280,466.08v50.49H267.68v-75.9h12.1Z"/><path class="cls-4" d="M401.77,440.67v11.11H368v20h30.47v11H368v22.77h33.77v11H355.24v-75.9Z"/><path class="cls-4" d="M441,440.67c17.82,0,26.95,6.6,26.95,19.69a17.12,17.12,0,0,1-11.11,16.5c8.58,2.42,13.53,8.36,13.53,17.93,0,11.44-7.26,21.78-27,21.78H416.62v-75.9Zm-11.66,31h12.43c9.46,0,13.42-4.29,13.42-10.56,0-5.94-3-9.46-15.07-9.46H429.38Zm0,33.88h13.31c10.23,0,15-4.18,15-11.44,0-8-5.28-11.44-16.5-11.44H429.38Z"/><path class="cls-4" d="M527.72,516.57l-14.63-30.8H498.35v30.8H485.59v-75.9h25.85c17.49,0,27.94,7.59,27.94,22.66,0,10.45-5,17.16-13.75,20.35l16.17,32.89Zm-17.16-41.8c10.78,0,16.06-2.86,16.06-11.44s-5.28-11.55-16.06-11.55H498.35v23Z"/><path class="cls-4" d="M569.4,516.57H556.64v-75.9H569.4Z"/><path class="cls-4" d="M589.86,516.57v-75.9h25.63c17,0,28.49,9.13,28.49,33.44v9c0,24.31-11.44,33.44-28.49,33.44Zm41.36-33.44v-9c0-15.62-4.84-22.33-15.73-22.33H602.62v53.79h12.87C626.38,505.57,631.22,498.75,631.22,483.13Z"/><path class="cls-4" d="M713.61,477.63v5.83c0,23.87-10.89,34.1-27.94,34.1s-28.38-10.23-28.38-34.1v-9.13c0-24.42,11.44-34.65,28.38-34.65,15.4,0,25.19,8.58,27.61,24.53H701c-1.87-8.8-6.71-13.42-15.29-13.42-10.12,0-15.62,6.6-15.62,23v9.68c0,16.39,5.5,23,15.62,23,9.68,0,14.52-5.72,15.4-17.71H686V477.63Z"/><path class="cls-4" d="M776.53,440.67v11.11H742.76v20h30.47v11H742.76v22.77h33.77v11H730v-75.9Z"/></g><g id="Logo"><circle class="cls-5" cx="450" cy="236.48" r="150"/><circle class="cls-6" cx="450" cy="236.48" r="150"/><image class="cls-7" width="255" height="258" transform="translate(324 110.23)" xlink:href="data:image/png;base64,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"/><path class="cls-8" d="M551.72,221.24h-.16l-18.34-18.33a3.58,3.58,0,0,1-1-2.5v-42.3A16.24,16.24,0,0,0,516,141.89H504.51a16.25,16.25,0,0,0-16.24,16.22v.24l-27.06-26.21a16.32,16.32,0,0,0-22.78.19L346.76,224a16.21,16.21,0,0,0,0,22.93l0,0,8.73,8.68a16.25,16.25,0,0,0,22.86.09l70.46-69.42a3.54,3.54,0,0,1,5,0l69,69a3.56,3.56,0,0,1,0,5l-8.48,8.48a3.56,3.56,0,0,1-5,0l-48.47-47a16.28,16.28,0,0,0-22.82.2l-46.51,46.75a16.26,16.26,0,0,0,0,22.92l8.61,8.61a16.27,16.27,0,0,0,22.89.06l25.07-24.78a3.57,3.57,0,0,1,5,0l24.94,24.51a3.51,3.51,0,0,1,0,5L453.38,329.9c-.11,0-.2-.07-.32-.07a9.26,9.26,0,1,0,9.26,9.26v-.16l24.81-24.8a16.24,16.24,0,0,0,0-23l-.1-.09-24.93-24.5a16.29,16.29,0,0,0-22.79,0l-25.15,24.81a3.54,3.54,0,0,1-5,0l-8.6-8.62a3.56,3.56,0,0,1,0-5L447.11,231a3.55,3.55,0,0,1,5-.05l48.46,47.09a16.3,16.3,0,0,0,22.78-.17l8.49-8.49a16.24,16.24,0,0,0,0-22.95l-69.06-69.06a16.26,16.26,0,0,0-22.87-.1l-70.46,69.4a3.57,3.57,0,0,1-5,0L355.76,238a3.54,3.54,0,0,1,0-5l91.67-91.67a3.56,3.56,0,0,1,5,0l48.59,47V158.11a3.55,3.55,0,0,1,3.54-3.54H516a3.57,3.57,0,0,1,3.55,3.54v42.36A16.37,16.37,0,0,0,524.31,212l18.24,18.24c0,.12-.07.21-.07.32a9.26,9.26,0,1,0,9.25-9.27Z"/></g></g><g id="Divider"><line class="cls-9" x1="998" y1="14.23" x2="902" y2="614.23"/></g></g></svg>
package/branding/icon.png DELETED
Binary file
Binary file
Binary file
Binary file
package/lib/util.js DELETED
@@ -1,8 +0,0 @@
1
- import { createHash } from 'crypto';
2
- export function generateMacAddress(uuid) {
3
- const sha1sum = createHash('sha1');
4
- sha1sum.update(uuid);
5
- const s = sha1sum.digest('hex');
6
- let i = 0;
7
- return 'xx:xx:xx:xx:xx:xx'.replace(/[x]/g, () => s[i++]).toUpperCase();
8
- }