homebridge-ring-secure-video 1.0.0-beta.4 → 1.0.0-beta.6
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 +80 -0
- package/README.md +133 -25
- package/config.schema.json +23 -2
- package/lib/camera.js +60 -7
- package/lib/push-health.js +136 -0
- package/lib/ring-platform.js +128 -10
- package/lib/session-refresh-sensor.js +106 -0
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
# homebridge-ring-secure-video
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.6 (2026-11-13)
|
|
4
|
+
|
|
5
|
+
**Changes in behaviour to check after updating:**
|
|
6
|
+
|
|
7
|
+
- Requires Node.js 22 or 24 (Node 20 reached end of life in April 2026) and Homebridge 1.8 or
|
|
8
|
+
later (2.x recommended). Homebridge warns about a plugin whose requirements don't match.
|
|
9
|
+
|
|
10
|
+
**New:**
|
|
11
|
+
|
|
12
|
+
- **A doorbell press can start an HKSV recording** (`doorbellPressMotion`, "Doorbell Press Starts
|
|
13
|
+
a Recording", off by default). HomeKit only records when the camera's motion sensor turns on,
|
|
14
|
+
and Ring reports no motion while a live view is open, or before someone who presses and leaves
|
|
15
|
+
at once is noticed. With the setting on, on a Ring doorbell with HKSV on in the plugin
|
|
16
|
+
(`enableHksv`; by default not for doorbells on battery, see `disableHksvOnBattery`), a press
|
|
17
|
+
also turns on the camera's motion for 30 seconds (a press during a live view is not tested
|
|
18
|
+
yet). A press while motion is already on keeps it on, so it stays one recording per visit.
|
|
19
|
+
Notifications and automations on the camera's motion sensor also see it; the separate
|
|
20
|
+
notification sensor doesn't, as a press already gives a doorbell notification. No effect with
|
|
21
|
+
`hideCameraMotionSensor` on.
|
|
22
|
+
|
|
23
|
+
**Docs:**
|
|
24
|
+
|
|
25
|
+
- Recording codecs: what each one does and costs, which settings apply to which, and what `auto`
|
|
26
|
+
picks per profile and machine. `copy`, `h264_v4l2m2m` and `libx264` all gave complete,
|
|
27
|
+
fully playing clips on a Raspberry Pi 4 with a Ring Video Doorbell 3 Plus; the exact settings
|
|
28
|
+
are listed, and the Pi recommendation is now the `lowPower` profile with its default, `copy`.
|
|
29
|
+
On a Pi 5 (no hardware encoder), set `copy` or `libx264` rather than relying on `auto` in the
|
|
30
|
+
`adaptive` and `quality` profiles.
|
|
31
|
+
- While a live view is open, Ring reports no motion, so no motion recording or notification
|
|
32
|
+
starts then (a running recording carries on until its motion ends).
|
|
33
|
+
- "What it adds to homebridge-ring", supported Node.js and Homebridge versions, and a correction
|
|
34
|
+
to the automatic session refresh description: events without a person are left out only when
|
|
35
|
+
the camera alerts for people only.
|
|
36
|
+
|
|
37
|
+
## 1.0.0-beta.5 (2026-11-13)
|
|
38
|
+
|
|
39
|
+
**Changes in behaviour to check after updating:**
|
|
40
|
+
|
|
41
|
+
- Automatic Ring session refresh is on by default. When it runs, a child bridge restarts itself,
|
|
42
|
+
a new `homebridge-ring-secure-video` entry appears in Authorized Client Devices in the Ring app
|
|
43
|
+
(remove the older one), and the identity in `.ring.json` changes, which other Ring plugins in
|
|
44
|
+
the same Homebridge (such as homebridge-ring) also use the next time they sign in from scratch.
|
|
45
|
+
If Ring is set to send no motion alerts in some Modes or on a schedule, set
|
|
46
|
+
`"automaticSessionRefresh": false`.
|
|
47
|
+
|
|
48
|
+
**New:**
|
|
49
|
+
|
|
50
|
+
- **Automatic Ring session refresh** (`automaticSessionRefresh`, on by default). Ring can stop
|
|
51
|
+
sending push notifications to the plugin's device identity while the connection to Google's
|
|
52
|
+
push service stays healthy: motion then only arrives late from the event history, and doorbell
|
|
53
|
+
presses not at all. When Ring's event history shows 3 motion events over at least 30 minutes
|
|
54
|
+
that Ring sent no push notification for, and no push notification of any kind arrived in
|
|
55
|
+
between, the plugin does what "Force Ring API Session Refresh" does: a new device identity and
|
|
56
|
+
push registration. A child bridge restarts itself for it; on the main bridge it happens at the
|
|
57
|
+
next Homebridge restart (dropped if a push notification arrives first). At most once a day,
|
|
58
|
+
and no more after 2 refreshes that brought no motion push. Not counted:
|
|
59
|
+
motion while snoozed, and motion without a person when the camera alerts for people only.
|
|
60
|
+
Needs the motion history fallback (on by default). After a refresh the log says "Automatic Ring
|
|
61
|
+
session refresh done", naming the Authorized Client Devices entry to remove.
|
|
62
|
+
- **"Ring Session Refreshed" sensor** (`sessionRefreshSensor`, off by default): a contact sensor
|
|
63
|
+
that opens after an automatic session refresh and closes a day later, so the Home app can
|
|
64
|
+
notify you (turn on its notifications; needs a home hub). Needs Automatic Ring Session Refresh.
|
|
65
|
+
|
|
66
|
+
**Fixes:**
|
|
67
|
+
|
|
68
|
+
- A child bridge that Homebridge restarted on its own (after a crash, or for the automatic
|
|
69
|
+
refresh) signed in with the Ring token from when Homebridge started, and couldn't save the
|
|
70
|
+
next one; a "Force Ring ..." setting cleared since was on again. It now takes the token and
|
|
71
|
+
the force settings from its block in config.json.
|
|
72
|
+
- New Ring tokens are also saved when the platform block in config.json is named
|
|
73
|
+
`"homebridge-ring-secure-video.Ring Secure Video"`; before, they weren't, and the next start
|
|
74
|
+
signed in with an old token.
|
|
75
|
+
- README: what to do when Ring's push notifications stop arriving (Troubleshooting).
|
|
76
|
+
|
|
77
|
+
**Maintenance:** dependency updates (`@homebridge/plugin-ui-utils` 2.3, ajv, globals,
|
|
78
|
+
typescript-eslint, concurrently 10), Homebridge 2.5 for development, and newer GitHub Actions for
|
|
79
|
+
CI and publishing. `@eneris/push-receiver` stays at 4.3.1 and ring-client-api at 14.3.0;
|
|
80
|
+
Dependabot no longer bundles them with other updates. werift stays at 0.22.4 (the version
|
|
81
|
+
ring-client-api pins), and Dependabot now ignores it.
|
|
82
|
+
|
|
3
83
|
## 1.0.0-beta.4 (2026-11-13)
|
|
4
84
|
|
|
5
85
|
More fixes from the full independent review of 1.0.0-beta.2.
|
package/README.md
CHANGED
|
@@ -11,6 +11,30 @@
|
|
|
11
11
|
locks, lights, cameras and doorbells), with HomeKit Secure Video (HKSV) recording for Ring cameras.
|
|
12
12
|
It is meant to replace `homebridge-ring` on its own: one plugin, one Ring sign-in.
|
|
13
13
|
|
|
14
|
+
## What it adds to homebridge-ring
|
|
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:
|
|
18
|
+
|
|
19
|
+
- **HomeKit Secure Video recording** for Ring cameras and doorbells, with media profiles for small
|
|
20
|
+
machines such as a Raspberry Pi (`media`, `hksvPerformanceMode`).
|
|
21
|
+
- **Instant motion notifications** with HKSV on: an optional separate motion sensor per camera
|
|
22
|
+
(`cameraMotionNotificationSensor`), so the Home app notifies without waiting for clip analysis.
|
|
23
|
+
- **Motion when Ring's push notifications fail**: motion from Ring's event history
|
|
24
|
+
(`enableCameraMotionHistory`), checked often only until push is seen to work.
|
|
25
|
+
- **Automatic recovery when Ring stops sending push notifications** (`automaticSessionRefresh`),
|
|
26
|
+
with an optional "Ring Session Refreshed" sensor (`sessionRefreshSensor`) to tell you.
|
|
27
|
+
- **A fix for dropped push notifications**: the push library ring-client-api ships (4.3.0) can drop
|
|
28
|
+
them; 4.3.1 is used instead.
|
|
29
|
+
- **A second visit is not missed**: a new Ring motion alert while the last one is still active
|
|
30
|
+
(ring-client-api keeps each active for 65 s) is reported to HomeKit as new motion, so it is
|
|
31
|
+
recorded and notified.
|
|
32
|
+
- **Ring Intercom Video** as a video doorbell camera, and `homeKitAccessoryTag` to expose the same
|
|
33
|
+
Ring device as a separate HomeKit accessory for testing.
|
|
34
|
+
- **Robustness**: one device or location that fails to set up doesn't stop the others; starting
|
|
35
|
+
while Ring can't be reached is retried; a restarted child bridge uses the newest Ring token;
|
|
36
|
+
config.json and the Ring identity are written so a power cut can't cut them short.
|
|
37
|
+
|
|
14
38
|
## Origin and Attribution
|
|
15
39
|
|
|
16
40
|
This project is based on Dusty Greif's original Ring Homebridge ecosystem:
|
|
@@ -28,6 +52,8 @@ Big thanks to Dusty, trinityhades and all upstream contributors. This plugin reu
|
|
|
28
52
|
| --- | --- |
|
|
29
53
|
| `enableHksv` | Enables experimental HKSV support for eligible cameras; on by default, set `false` to turn it off |
|
|
30
54
|
| `enableCameraMotionHistory` | Allows camera motion to fall back to recent Ring event history if push notifications don't arrive. The history is checked every `cameraStatusPollingSeconds` until motion events turn out to come by push too, then every 5 minutes as a safety net (and frequently again once a push is missed, so motion can go unreported for up to 5 minutes after push stops working); disable to only trust push-triggered motion |
|
|
55
|
+
| `automaticSessionRefresh` | When Ring's event history shows motion that Ring sent no push notification for (3 events over at least 30 minutes, none pushed), refreshes the Ring API session by itself, as **Force Ring API Session Refresh** does (see Troubleshooting): a child bridge restarts for it, the main bridge does it at the next Homebridge restart. Events while motion is snoozed are not counted, nor, when the camera alerts for people only, events without a person. At most once a day, and no more after 2 refreshes that brought no motion push notification. Needs `enableCameraMotionHistory`. On by default; set `false` to turn it off, for example if Ring is set to send no motion alerts in some Modes or on a schedule |
|
|
56
|
+
| `sessionRefreshSensor` | Adds a "Ring Session Refreshed" contact sensor that opens after an automatic session refresh (10 s after the Home app is connected to it, within 3 minutes) and stays open for a day, also across restarts (which don't notify again). Turn on its notifications in the Home app (needs a home hub) to be told when to remove the older `homebridge-ring-secure-video` entry from Authorized Client Devices. Needs `automaticSessionRefresh`. Off by default |
|
|
31
57
|
| `disableHksvOnBattery` | Disables HKSV on cameras running on battery to reduce battery/network usage; wired battery models keep it. On by default |
|
|
32
58
|
| `hksvPrebufferLengthMs` | HKSV prebuffer duration (minimum 4000ms) |
|
|
33
59
|
| `hksvFragmentLengthMs` | HKSV fragment duration target |
|
|
@@ -41,6 +67,7 @@ Big thanks to Dusty, trinityhades and all upstream contributors. This plugin reu
|
|
|
41
67
|
| `cameraVideoCodec` | Preferred H.264 handling (`auto`, `copy`, `h264_v4l2m2m`, `h264_videotoolbox`, or `libx264`) |
|
|
42
68
|
| `hideDoorbellSwitch` / `hideCameraMotionSensor` / `hideCameraSirenSwitch` | Hides specific HomeKit-exposed services |
|
|
43
69
|
| `cameraMotionNotificationSensor` | Adds a separate "<camera> Motion" sensor per camera for instant motion notifications (see below) |
|
|
70
|
+
| `doorbellPressMotion` | For Ring doorbells with HKSV recording on: a press also turns on the camera's motion for 30 seconds, so HKSV records the visitor even when Ring reported no motion (during a live view, or a quick press-and-leave). One recording per visit, not two. Notifications and automations on the camera's motion sensor also see it; the separate notification sensor does not. No effect with `hideCameraMotionSensor`. Off by default |
|
|
44
71
|
| `showPanicButtons` | Adds panic switches (use with caution) |
|
|
45
72
|
| `ffmpegPath` | Override FFmpeg binary path |
|
|
46
73
|
| `debug` | Enables additional logging |
|
|
@@ -58,7 +85,8 @@ normally ends a recording itself when motion stops), or to `0` to disable it.
|
|
|
58
85
|
|
|
59
86
|
## Installation
|
|
60
87
|
|
|
61
|
-
If Homebridge is installed
|
|
88
|
+
Requires Node.js 22 or 24 and Homebridge 1.8 or later (2.x recommended). If Homebridge is installed
|
|
89
|
+
globally:
|
|
62
90
|
|
|
63
91
|
```bash
|
|
64
92
|
npm i -g homebridge-ring-secure-video
|
|
@@ -116,12 +144,21 @@ Changing `homeKitAccessoryTag` updates both the exposed accessory name and the g
|
|
|
116
144
|
HKSV support is experimental and actively evolving. Behavior may vary by camera model, wired vs battery power, Ring API changes, and FFmpeg environment.
|
|
117
145
|
|
|
118
146
|
Verified so far: complete HomeKit clips from a Ring Video Doorbell 3 Plus on a Raspberry Pi 4
|
|
119
|
-
(Node 22, Homebridge 2) with
|
|
147
|
+
(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.
|
|
120
148
|
Please report your experience and setup details to help improve support.
|
|
121
149
|
|
|
122
150
|
Recordings follow the frame rate and audio format HomeKit asks for, and include audio only when
|
|
123
151
|
**Record Audio** is on in the camera's settings in the Home app.
|
|
124
152
|
|
|
153
|
+
While a live view of the camera is open in the Home app, Ring reports no motion for it: its Event
|
|
154
|
+
History shows only the live view (probably also with a live view in the Ring app; not tested). So
|
|
155
|
+
no motion event, motion notification or HKSV recording starts during a live view (with Ring
|
|
156
|
+
Protect, Ring may still save the live view itself). Once the live view is closed, motion is
|
|
157
|
+
reported again. Opening a live view doesn't cut off an HKSV recording that is already running
|
|
158
|
+
(both share one Ring connection), but that recording still ends as usual once its motion ends.
|
|
159
|
+
With `doorbellPressMotion` on, a doorbell press during a live view still starts a recording, as
|
|
160
|
+
long as Ring delivers the press during the live view (not tested yet).
|
|
161
|
+
|
|
125
162
|
With recording on, the Home app notifies about a camera's motion only after its home hub has
|
|
126
163
|
analysed the clip, which can be late or never. For a notification as soon as Ring reports
|
|
127
164
|
motion, turn on `cameraMotionNotificationSensor`: each camera gets a separate "<camera> Motion"
|
|
@@ -129,29 +166,69 @@ sensor on this plugin's bridge. If the bridge isn't in the Home app yet, add it
|
|
|
129
166
|
code, then turn on notifications in the sensor's settings (and, if you like, turn off the
|
|
130
167
|
camera's own Activity Notifications to avoid two alerts).
|
|
131
168
|
|
|
132
|
-
For fast-motion pixelation or stuttering in HKSV recordings, try
|
|
133
|
-
`
|
|
134
|
-
|
|
135
|
-
|
|
169
|
+
For fast-motion pixelation or stuttering in HKSV recordings with an encoder (not `copy`), try
|
|
170
|
+
increasing `media.recording.bitrateKbps` first. On Apple Silicon Macs, `"codec":
|
|
171
|
+
"h264_videotoolbox"` should reduce CPU load by using the hardware encoder (not verified in this
|
|
172
|
+
fork).
|
|
173
|
+
|
|
174
|
+
### Recording codecs
|
|
175
|
+
|
|
176
|
+
Only HKSV recordings are encoded; live view passes Ring's video through. `media.recording.codec`
|
|
177
|
+
(or the older `cameraVideoCodec`) chooses how:
|
|
178
|
+
|
|
179
|
+
| Codec | What it does | CPU |
|
|
180
|
+
| --- | --- | --- |
|
|
181
|
+
| `copy` | Records Ring's H.264 as received, without re-encoding (Ring's own picture) | Least |
|
|
182
|
+
| `h264_v4l2m2m` | Raspberry Pi 4 hardware encoder (the Pi 5 has none) | Low to medium: Ring's video is still decoded and scaled in software |
|
|
183
|
+
| `h264_videotoolbox` | Mac hardware encoder (not verified in this fork) | Low |
|
|
184
|
+
| `libx264` | Software encoder, with the most control over quality | Most |
|
|
185
|
+
| `auto` | Picks one of the above, see below | |
|
|
186
|
+
|
|
187
|
+
`copy` works when Ring's stream meets HomeKit's recording requirements, as it does for the
|
|
188
|
+
Doorbell 3 Plus; if clips from another camera don't appear or don't play, choose an encoder.
|
|
189
|
+
`bitrateKbps`, `maxBitrateKbps`, `bufferSizeKbps`, `rateControl` and `keyframeInterval` apply to
|
|
190
|
+
all encoders; `preset` and `crf` (the latter only with `vbr`) apply only to `libx264`. `copy`
|
|
191
|
+
ignores all of them. The Pi's hardware encoder uses `bitrateKbps` and `keyframeInterval`;
|
|
192
|
+
`rateControl` likely makes little difference there.
|
|
193
|
+
|
|
194
|
+
#### What `auto` picks
|
|
195
|
+
|
|
196
|
+
`auto` is the default, except in the `lowPower` profile, whose default is `copy`.
|
|
136
197
|
|
|
137
|
-
|
|
198
|
+
| Profile (`media.profile`) | `auto` picks |
|
|
199
|
+
| --- | --- |
|
|
200
|
+
| `lowPower` (also `"hksvPerformanceMode": "rpi"`) | `copy` |
|
|
201
|
+
| `adaptive` (default) or `quality` | `h264_v4l2m2m` if FFmpeg has it (Raspberry Pi 4), else `h264_videotoolbox` if FFmpeg has it (Mac), else `libx264` |
|
|
202
|
+
|
|
203
|
+
"FFmpeg has it" means the encoder is built into FFmpeg, not that the hardware is there: on a
|
|
204
|
+
Raspberry Pi 5 (no hardware encoder), set `codec` to `copy` or `libx264` rather than relying on
|
|
205
|
+
`auto` in the `adaptive` and `quality` profiles. A valid legacy `cameraVideoCodec` in the config
|
|
206
|
+
counts as the codec when `media.recording.codec` isn't set. The log shows the choice for each recording: `Starting HKSV recording pipeline ...
|
|
207
|
+
video=<codec>`.
|
|
208
|
+
|
|
209
|
+
#### Settings verified on a Raspberry Pi 4
|
|
210
|
+
|
|
211
|
+
Raspberry Pi 4, Ring Video Doorbell 3 Plus (wired), Homebridge 2.5, Node 22, FFmpeg from
|
|
212
|
+
ffmpeg-for-homebridge. HomeKit asked for 1280x720 at 30 fps with 4-second fragments; every
|
|
213
|
+
setting below gave complete clips that play fully in the Home app.
|
|
214
|
+
|
|
215
|
+
| Codec | `media` settings | Notes |
|
|
216
|
+
| --- | --- | --- |
|
|
217
|
+
| `copy` | `{ "profile": "lowPower", "recording": { "codec": "copy" } }` | Least CPU, most natural picture; the recommended start on a Pi |
|
|
218
|
+
| `h264_v4l2m2m` | `{ "profile": "lowPower", "recording": { "codec": "h264_v4l2m2m", "bitrateKbps": 1500, "keyframeInterval": 30 } }` | FFmpeg used about 75% of one of the Pi's four cores while recording |
|
|
219
|
+
| `libx264` | `{ "profile": "lowPower", "recording": { "codec": "libx264", "bitrateKbps": 800, "keyframeInterval": 112 } }` | The `lowPower` preset is `ultrafast` |
|
|
220
|
+
|
|
221
|
+
For a Raspberry Pi, start with:
|
|
138
222
|
|
|
139
223
|
```json
|
|
140
224
|
{
|
|
141
225
|
"enableHksv": true,
|
|
142
226
|
"media": {
|
|
143
|
-
"profile": "lowPower"
|
|
144
|
-
"recording": { "codec": "libx264", "bitrateKbps": 800 }
|
|
227
|
+
"profile": "lowPower"
|
|
145
228
|
}
|
|
146
229
|
}
|
|
147
230
|
```
|
|
148
231
|
|
|
149
|
-
The `lowPower` profile (also chosen by `"hksvPerformanceMode": "rpi"`) records one clip at a
|
|
150
|
-
time with conservative settings. Lighter codecs exist but complete HomeKit clips with them are
|
|
151
|
-
not verified yet: `copy` (the `lowPower` default for `auto`) remuxes Ring's H.264 without
|
|
152
|
-
re-encoding, which only works when Ring's stream meets HomeKit's recording requirements, and
|
|
153
|
-
`h264_v4l2m2m` uses the Pi's hardware encoder when FFmpeg has it.
|
|
154
|
-
|
|
155
232
|
### Minimum specifications for HKSV:
|
|
156
233
|
[TBD - will be added as more users test and report their setups]
|
|
157
234
|
|
|
@@ -163,16 +240,47 @@ For support and debugging:
|
|
|
163
240
|
|
|
164
241
|
- Upstream Ring wiki (token/auth/camera references): <https://github.com/dgreif/ring/wiki>
|
|
165
242
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
243
|
+
### Motion arrives late or doorbell presses are missed
|
|
244
|
+
|
|
245
|
+
If motion arrives 20–30 seconds late with "Detected Motion from Ring event history fallback" in the
|
|
246
|
+
log (or "Ring event history shows motion no push notification reported"), and doorbell presses don't
|
|
247
|
+
reach HomeKit at all, Ring's instant push notifications aren't reaching the plugin. Once they do, the
|
|
248
|
+
"...from Ring event history fallback" lines stop, and (with the motion history fallback on, the
|
|
249
|
+
default) the log says "Ring push notifications are arriving".
|
|
250
|
+
|
|
251
|
+
1. Check that the Homebridge machine can reach Google's push service: `mtalk.google.com` must
|
|
252
|
+
resolve to a real address (not `0.0.0.0`; DNS ad blockers such as AdGuard or Pi-hole can block it)
|
|
253
|
+
and TCP port 5228 must be open. See the upstream
|
|
254
|
+
[notification troubleshooting](https://github.com/dgreif/ring/wiki/Notification-Troubleshooting).
|
|
255
|
+
2. If the connection is fine, Ring may have stopped sending to the plugin's device identity, even
|
|
256
|
+
though the Ring app on your phone still gets alerts. Removing the plugin from Authorized Client
|
|
257
|
+
Devices and signing in again keeps the same identity (it is stored in `.ring.json`), so it may not
|
|
258
|
+
help. What does: turn on **Force Ring API Session Refresh** in the plugin's settings once, and
|
|
259
|
+
restart. The log then shows "Forcing Ring API session refresh" (and usually "Session hardware_id
|
|
260
|
+
not found" as Ring meets the new identity), and the setting turns itself off once the new sign-in
|
|
261
|
+
is saved. Then, in
|
|
262
|
+
the Ring app's Control Center, remove the older `homebridge-ring-secure-video` entry from
|
|
263
|
+
Authorized Client Devices.
|
|
264
|
+
|
|
265
|
+
With **Automatic Ring Session Refresh** on (the default) and the motion history fallback on, the
|
|
266
|
+
plugin does step 2 by itself when Ring's event history shows 3 motion events over at least 30
|
|
267
|
+
minutes that Ring sent no push notification for, and no push arrived in between. Events while
|
|
268
|
+
motion is snoozed, and events Ring saw no person in (which Smart Alerts may keep quiet), are not
|
|
269
|
+
counted. The log then says "Ring event history shows motion that Ring sent no push notification
|
|
270
|
+
for", and as a child bridge it restarts (on the main bridge, the refresh waits for the next
|
|
271
|
+
Homebridge restart, and is dropped if a push arrives first). It does this at most once a day, and
|
|
272
|
+
stops after 2 refreshes that brought no motion push notification, logging "Ring still sends no
|
|
273
|
+
push notifications after 2 automatic Ring session refreshes": then check step 1 and Authorized
|
|
274
|
+
Client Devices. If you have Ring send no motion alerts on purpose (for example in some Modes, or on
|
|
275
|
+
a schedule), turn the setting off, or the plugin will take the missing alerts for a fault.
|
|
276
|
+
|
|
277
|
+
Use the "Force Ring ..." settings only for this, and only once. Each API session refresh, automatic
|
|
278
|
+
or not, adds an entry to Authorized Client Devices, and many entries can themselves make Ring stop
|
|
279
|
+
sending notifications, so after one remove the older `homebridge-ring-secure-video` entry (after
|
|
280
|
+
an automatic refresh the log says "Automatic Ring session refresh done", and the optional
|
|
281
|
+
`sessionRefreshSensor` opens in the Home app). It also
|
|
282
|
+
changes the identity that other Ring plugins in the same Homebridge (such as homebridge-ring) will
|
|
283
|
+
use the next time they sign in from scratch, since they share `.ring.json`.
|
|
176
284
|
|
|
177
285
|
## Disclaimer
|
|
178
286
|
|
package/config.schema.json
CHANGED
|
@@ -152,6 +152,18 @@
|
|
|
152
152
|
"type": "boolean",
|
|
153
153
|
"description": "Advanced recovery option: rotates the Ring API hardware/session identity and forces fresh push credentials on next startup. Try this when refreshing only Ring push credentials still leaves motion/button events delayed or missing. The option clears itself after the updated refresh token is saved. The identity is stored in .ring.json in the Homebridge storage folder, which other Ring plugins in the same Homebridge (such as homebridge-ring) also use, so rotating it can affect their sign-in."
|
|
154
154
|
},
|
|
155
|
+
"automaticSessionRefresh": {
|
|
156
|
+
"title": "Automatic Ring Session Refresh",
|
|
157
|
+
"type": "boolean",
|
|
158
|
+
"description": "When Ring's event history shows motion but Ring has sent the plugin no push notification for it (3 events over at least 30 minutes, none pushed), the plugin does a Ring API session refresh by itself, as Force Ring API Session Refresh does: as a child bridge it restarts, otherwise at the next restart of Homebridge. Events while motion is snoozed are not counted, nor, when the camera alerts for people only, events without a person. At most once a day, and no more after 2 that brought no motion push notification. Needs the camera motion history fallback. Turn this off if Ring is set to send no motion alerts in some Modes or on a schedule. Each refresh adds an entry to Authorized Client Devices in the Ring app (remove the older homebridge-ring-secure-video entries) and changes the identity in .ring.json, which other Ring plugins in the same Homebridge also use.",
|
|
159
|
+
"default": true
|
|
160
|
+
},
|
|
161
|
+
"sessionRefreshSensor": {
|
|
162
|
+
"title": "Session Refresh Sensor",
|
|
163
|
+
"type": "boolean",
|
|
164
|
+
"description": "Adds a \"Ring Session Refreshed\" contact sensor that opens after an automatic Ring session refresh (10 s after the Home app is connected to it, within 3 minutes) and stays open for a day, so the Home app can notify you (turn on its notifications; needs a home hub): then remove the older homebridge-ring-secure-video entry from Authorized Client Devices in the Ring app. Needs Automatic Ring Session Refresh.",
|
|
165
|
+
"default": false
|
|
166
|
+
},
|
|
155
167
|
"enableCameraMotionHistory": {
|
|
156
168
|
"title": "Enable Camera Motion History Fallback",
|
|
157
169
|
"type": "boolean",
|
|
@@ -189,7 +201,7 @@
|
|
|
189
201
|
"hksvPerformanceMode": {
|
|
190
202
|
"title": "HKSV Performance Mode",
|
|
191
203
|
"type": "string",
|
|
192
|
-
"description": "High-level HKSV tuning profile. Use rpi for Raspberry Pi and other small computers: lower advertised recording tiers, one recording at a time, and remuxing (copy) unless a codec other than auto is set. Verified on a Pi 4 with
|
|
204
|
+
"description": "High-level HKSV tuning profile. Use rpi for Raspberry Pi and other small computers: lower advertised recording tiers, one recording at a time, and remuxing (copy) unless a codec other than auto is set. Verified on a Pi 4 with copy, h264_v4l2m2m and libx264.",
|
|
193
205
|
"default": "balanced",
|
|
194
206
|
"oneOf": [
|
|
195
207
|
{
|
|
@@ -307,7 +319,7 @@
|
|
|
307
319
|
"cameraVideoCodec": {
|
|
308
320
|
"title": "Camera Video Codec",
|
|
309
321
|
"type": "string",
|
|
310
|
-
"description": "Preferred FFmpeg H.264 handling for HKSV recordings. auto chooses the lowest-cost option for the configured performance mode; copy remuxes Ring video without re-encoding. Complete clips are verified with libx264; copy
|
|
322
|
+
"description": "Preferred FFmpeg H.264 handling for HKSV recordings. auto chooses the lowest-cost option for the configured performance mode; copy remuxes Ring video without re-encoding. Complete clips are verified on a Raspberry Pi 4 with a Ring Video Doorbell 3 Plus for copy, h264_v4l2m2m and libx264; if clips from another camera don't play with copy, try h264_v4l2m2m (Raspberry Pi 4; the Pi 5 has no hardware H.264 encoder) or libx264.",
|
|
311
323
|
"oneOf": [
|
|
312
324
|
{
|
|
313
325
|
"title": "auto",
|
|
@@ -354,6 +366,12 @@
|
|
|
354
366
|
"type": "boolean",
|
|
355
367
|
"description": "Adds a \"<camera> Motion\" sensor for each camera, driven by the same motion events but not tied to the camera. With HomeKit Secure Video recording on, the Home app only notifies about the camera's own motion after analysing the clip; turn on notifications for this sensor instead to be told at once. It appears on this plugin's bridge."
|
|
356
368
|
},
|
|
369
|
+
"doorbellPressMotion": {
|
|
370
|
+
"title": "Doorbell Press Starts a Recording",
|
|
371
|
+
"type": "boolean",
|
|
372
|
+
"description": "For Ring doorbells with HomeKit Secure Video recording on: a doorbell press also turns on the camera's motion in HomeKit for 30 seconds, so HomeKit records the visitor even when Ring reported no motion (for example while a live view is open, or for someone who presses and leaves at once). A press while motion is already on keeps it on: one recording, not two. Notifications and automations on the camera's own motion sensor also see it; the separate notification sensor does not, as a press already gives a doorbell notification. No effect when the camera's motion sensor is hidden.",
|
|
373
|
+
"default": false
|
|
374
|
+
},
|
|
357
375
|
"hideCameraSirenSwitch": {
|
|
358
376
|
"title": "Hide Camera Siren Switch",
|
|
359
377
|
"type": "boolean"
|
|
@@ -484,6 +502,8 @@
|
|
|
484
502
|
"media",
|
|
485
503
|
"forceRefreshRingPushCredentials",
|
|
486
504
|
"forceRefreshRingApiSession",
|
|
505
|
+
"automaticSessionRefresh",
|
|
506
|
+
"sessionRefreshSensor",
|
|
487
507
|
"enableCameraMotionHistory",
|
|
488
508
|
"disableHksvOnBattery",
|
|
489
509
|
"hksvVideoPreset",
|
|
@@ -512,6 +532,7 @@
|
|
|
512
532
|
"hideCameraLight",
|
|
513
533
|
"hideCameraMotionSensor",
|
|
514
534
|
"cameraMotionNotificationSensor",
|
|
535
|
+
"doorbellPressMotion",
|
|
515
536
|
"hideCameraSirenSwitch",
|
|
516
537
|
"hideInHomeDoorbellSwitch",
|
|
517
538
|
"hideAlarmSirenSwitch",
|
package/lib/camera.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { hap } from "./hap.js";
|
|
2
2
|
import { PushNotificationAction } from 'ring-client-api';
|
|
3
3
|
import { BaseDataAccessory } from "./base-data-accessory.js";
|
|
4
|
-
import { catchError, exhaustMap, filter, finalize, map, mergeMap, share, switchMap, throttleTime, } from 'rxjs/operators';
|
|
5
|
-
import { CameraSource } from "./camera-source.js";
|
|
4
|
+
import { catchError, distinctUntilChanged, exhaustMap, filter, finalize, map, mergeMap, share, startWith, switchMap, throttleTime, } from 'rxjs/operators';
|
|
5
|
+
import { CameraSource, isHksvEnabled } from "./camera-source.js";
|
|
6
6
|
import { TargetValueTimer } from "./target-value-timer.js";
|
|
7
7
|
import { delay, logError, logInfo } from 'ring-client-api/util';
|
|
8
|
-
import { defer, EMPTY, firstValueFrom, merge, of, timer } from 'rxjs';
|
|
8
|
+
import { combineLatest, defer, EMPTY, firstValueFrom, merge, of, timer } from 'rxjs';
|
|
9
9
|
export const MOTION_WATCHDOG_MS = 30_000;
|
|
10
10
|
/** How often Ring's event history is checked once its push notifications are known to arrive. */
|
|
11
11
|
export const HISTORY_CHECK_WHILE_PUSH_WORKS_MS = 5 * 60_000;
|
|
@@ -33,7 +33,7 @@ const ID_MEMORY_MS = 60 * 60_000;
|
|
|
33
33
|
* a gap (5-minute checks, a failure) only events from the last 3 minutes do, as one found later
|
|
34
34
|
* would only record an empty doorway.
|
|
35
35
|
*/
|
|
36
|
-
export function getHistoryMotionEvents(device, pollSeconds) {
|
|
36
|
+
export function getHistoryMotionEvents(device, pollSeconds, pushHealth) {
|
|
37
37
|
return defer(() => {
|
|
38
38
|
const startedAt = Date.now() - 5000, historySeen = new Map(),
|
|
39
39
|
// Events that already gave motion from here (a young one is judged again later).
|
|
@@ -47,6 +47,7 @@ export function getHistoryMotionEvents(device, pollSeconds) {
|
|
|
47
47
|
};
|
|
48
48
|
let pushWorks = false, matchedInARow = 0, lastCheckAt = 0, lastSuccessAt = 0, failureCount = 0, nextCheckAt = 0;
|
|
49
49
|
const notifications = device.onNewNotification.subscribe((notification) => {
|
|
50
|
+
pushHealth?.recordPush(notification.android_config?.category === PushNotificationAction.Motion);
|
|
50
51
|
if (notification.android_config?.category !== PushNotificationAction.Motion)
|
|
51
52
|
return;
|
|
52
53
|
const id = notification.data?.event?.ding?.id, now = Date.now();
|
|
@@ -115,6 +116,14 @@ export function getHistoryMotionEvents(device, pollSeconds) {
|
|
|
115
116
|
lastCheckAt = checkedAt - HISTORY_CHECK_WHILE_PUSH_WORKS_MS + PUSH_GRACE_MS + 10_000;
|
|
116
117
|
}
|
|
117
118
|
if (missed.length) {
|
|
119
|
+
// Ring sends no alert while motion is snoozed, nor, when set to alert for people
|
|
120
|
+
// only, for motion it saw no person in: those say nothing about whether its
|
|
121
|
+
// pushes reach the plugin.
|
|
122
|
+
const peopleOnly = device.data?.settings?.advanced_motion_detection_human_only_mode === true, unexplained = device.data?.motion_snooze
|
|
123
|
+
? []
|
|
124
|
+
: missed.filter((event) => !peopleOnly || event.cv_properties?.person_detected !== false);
|
|
125
|
+
if (unexplained.length)
|
|
126
|
+
pushHealth?.recordMissed(unexplained.length);
|
|
118
127
|
matchedInARow = 0;
|
|
119
128
|
if (pushWorks) {
|
|
120
129
|
pushWorks = false;
|
|
@@ -191,6 +200,32 @@ export function addMotionWatchdog(motionEvents, watchdogMs = MOTION_WATCHDOG_MS)
|
|
|
191
200
|
return merge(of(true), timer(watchdogMs).pipe(map(() => false)));
|
|
192
201
|
}));
|
|
193
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* The camera's motion, also on for a while after each doorbell press (doorbellPressMotion), so
|
|
205
|
+
* HomeKit records a visitor even when Ring reported no motion: while a live view is open Ring
|
|
206
|
+
* reports none, and someone may press and leave before it does. A press while motion is already
|
|
207
|
+
* on keeps it on (no off and on again, which would be a second recording); motion goes off once
|
|
208
|
+
* neither the camera nor a press in the last watchdogMs keeps it on.
|
|
209
|
+
*/
|
|
210
|
+
export function addDoorbellPressMotion(motion, presses, onPressMotion, watchdogMs = MOTION_WATCHDOG_MS) {
|
|
211
|
+
return combineLatest([
|
|
212
|
+
motion.pipe(startWith(false)),
|
|
213
|
+
addMotionWatchdog(presses.pipe(
|
|
214
|
+
// A failing press stream must not stop the camera's own motion.
|
|
215
|
+
catchError((error) => {
|
|
216
|
+
logError(error);
|
|
217
|
+
return EMPTY;
|
|
218
|
+
}), map(() => true)), watchdogMs).pipe(startWith(false)),
|
|
219
|
+
]).pipe(
|
|
220
|
+
// Repeated trues are dropped (HomeKit sees no change from them anyway), so a second motion
|
|
221
|
+
// alert while motion is on no longer reloads the snapshot; a press reloads it itself.
|
|
222
|
+
distinctUntilChanged((a, b) => (a[0] || a[1]) === (b[0] || b[1])), map(([cameraMotion, pressMotion]) => {
|
|
223
|
+
const on = cameraMotion || pressMotion;
|
|
224
|
+
if (on && !cameraMotion)
|
|
225
|
+
onPressMotion?.();
|
|
226
|
+
return on;
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
194
229
|
export class Camera extends BaseDataAccessory {
|
|
195
230
|
inHomeDoorbellStatus;
|
|
196
231
|
cameraSource;
|
|
@@ -198,11 +233,13 @@ export class Camera extends BaseDataAccessory {
|
|
|
198
233
|
accessory;
|
|
199
234
|
config;
|
|
200
235
|
motionDetected;
|
|
201
|
-
|
|
236
|
+
pushHealth;
|
|
237
|
+
constructor(device, accessory, config, pushHealth) {
|
|
202
238
|
super();
|
|
203
239
|
this.device = device;
|
|
204
240
|
this.accessory = accessory;
|
|
205
241
|
this.config = config;
|
|
242
|
+
this.pushHealth = pushHealth;
|
|
206
243
|
this.cameraSource = new CameraSource(this.device, this.config);
|
|
207
244
|
if (!hap.CameraController) {
|
|
208
245
|
const error = 'HAP CameraController not found. Please make sure you are on homebridge version 1.0.0 or newer';
|
|
@@ -226,11 +263,27 @@ export class Camera extends BaseDataAccessory {
|
|
|
226
263
|
if (!config.hideCameraMotionSensor || config.cameraMotionNotificationSensor) {
|
|
227
264
|
this.motionDetected = this.getMotionDetectedObservable().pipe(share());
|
|
228
265
|
}
|
|
266
|
+
if (config.doorbellPressMotion && device.isDoorbot) {
|
|
267
|
+
if (config.hideCameraMotionSensor) {
|
|
268
|
+
logInfo(`${device.name}: doorbellPressMotion has no effect while hideCameraMotionSensor is on (HomeKit records on the camera's motion sensor)`);
|
|
269
|
+
}
|
|
270
|
+
else if (!isHksvEnabled(config, device.operatingOnBattery)) {
|
|
271
|
+
logInfo(`${device.name}: doorbellPressMotion has no effect without HKSV recording`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
229
274
|
if (!config.hideCameraMotionSensor && this.motionDetected) {
|
|
275
|
+
// Only the camera's own motion sensor (which starts HKSV recordings): a press already
|
|
276
|
+
// gives a doorbell notification, so the separate notification sensor doesn't get it too.
|
|
277
|
+
// Only with HKSV recording: without it a press would just add a motion notification.
|
|
278
|
+
const cameraMotion = config.doorbellPressMotion &&
|
|
279
|
+
device.isDoorbot &&
|
|
280
|
+
isHksvEnabled(config, device.operatingOnBattery)
|
|
281
|
+
? addDoorbellPressMotion(this.motionDetected, device.onDoorbellPressed, () => logInfo(`${device.name}: doorbell press sent to HomeKit as motion (doorbellPressMotion)`))
|
|
282
|
+
: this.motionDetected;
|
|
230
283
|
this.registerObservableCharacteristic({
|
|
231
284
|
characteristicType: Characteristic.MotionDetected,
|
|
232
285
|
serviceType: Service.MotionSensor,
|
|
233
|
-
onValue:
|
|
286
|
+
onValue: cameraMotion.pipe(switchMap((motion) => {
|
|
234
287
|
if (!motion) {
|
|
235
288
|
return Promise.resolve(false);
|
|
236
289
|
}
|
|
@@ -365,7 +418,7 @@ export class Camera extends BaseDataAccessory {
|
|
|
365
418
|
return addMotionWatchdog(pushMotionEvents);
|
|
366
419
|
}
|
|
367
420
|
const pollSeconds = Math.max(this.config.cameraStatusPollingSeconds ?? 20, 10);
|
|
368
|
-
return addMotionWatchdog(merge(pushMotionEvents, getHistoryMotionEvents(this.device, pollSeconds)));
|
|
421
|
+
return addMotionWatchdog(merge(pushMotionEvents, getHistoryMotionEvents(this.device, pollSeconds, this.pushHealth)));
|
|
369
422
|
}
|
|
370
423
|
async loadSnapshotForEvent(eventDescription, characteristicValue) {
|
|
371
424
|
let imageUuid = this.device.latestNotificationSnapshotUuid;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { performance } from 'perf_hooks';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { logError, logInfo } from 'ring-client-api/util';
|
|
5
|
+
import { writeFileAtomic } from "./config.js";
|
|
6
|
+
const stateFileName = '.ring-secure-video-push.json';
|
|
7
|
+
/** Motion events in Ring's history with no push notification, since the last push... */
|
|
8
|
+
export const MISSES_BEFORE_REFRESH = 3;
|
|
9
|
+
/** ...spread over at least this long (one visit can make several events)... */
|
|
10
|
+
export const MISS_SPAN_BEFORE_REFRESH_MS = 30 * 60_000;
|
|
11
|
+
/** ...give an automatic session refresh, at most once in this long... */
|
|
12
|
+
export const REFRESH_INTERVAL_MS = 24 * 60 * 60_000;
|
|
13
|
+
/** ...and no more after this many in a row that brought no push. */
|
|
14
|
+
export const MAX_REFRESHES_WITHOUT_PUSH = 2;
|
|
15
|
+
export function readPushRecoveryState(storagePath) {
|
|
16
|
+
try {
|
|
17
|
+
const state = JSON.parse(readFileSync(join(storagePath, stateFileName)).toString());
|
|
18
|
+
return state && typeof state === 'object' ? state : {};
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function writePushRecoveryState(storagePath, state) {
|
|
25
|
+
try {
|
|
26
|
+
writeFileAtomic(join(storagePath, stateFileName), JSON.stringify(state));
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
logError(`Failed to save ${stateFileName}`);
|
|
31
|
+
logError(error);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Notices when Ring has stopped sending push notifications to the plugin while its event history
|
|
37
|
+
* still shows motion, and then asks for a Ring API session refresh (a new device identity and push
|
|
38
|
+
* registration, as "Force Ring API Session Refresh" does), which is what brings them back.
|
|
39
|
+
*
|
|
40
|
+
* Only when no push at all arrived while the misses piled up: Ring sometimes drops a single
|
|
41
|
+
* notification, and then a refresh would only add an entry to Authorized Client Devices.
|
|
42
|
+
*/
|
|
43
|
+
export class PushHealthMonitor {
|
|
44
|
+
missTimes = [];
|
|
45
|
+
pushSeen = false;
|
|
46
|
+
motionPushSeen = false;
|
|
47
|
+
/** A refresh was asked for in this run, and no push has arrived since. */
|
|
48
|
+
requested = false;
|
|
49
|
+
/** This run's refresh has not saved its new token yet: it must stay pending. */
|
|
50
|
+
holdPending = false;
|
|
51
|
+
done = false;
|
|
52
|
+
loggedWaitUntil = 0;
|
|
53
|
+
storagePath;
|
|
54
|
+
refresh;
|
|
55
|
+
now;
|
|
56
|
+
elapsed;
|
|
57
|
+
/**
|
|
58
|
+
* now: the clock saved with a refresh (it must survive restarts); elapsed: the clock the
|
|
59
|
+
* misses are timed with, which a clock set by NTP after a boot (a Pi has no clock of its own)
|
|
60
|
+
* can't make jump.
|
|
61
|
+
*/
|
|
62
|
+
constructor(storagePath, refresh, now = Date.now, elapsed = () => performance.now()) {
|
|
63
|
+
this.storagePath = storagePath;
|
|
64
|
+
this.refresh = refresh;
|
|
65
|
+
this.now = now;
|
|
66
|
+
this.elapsed = elapsed;
|
|
67
|
+
}
|
|
68
|
+
/** Set while this run's own refresh has not saved its new token (see connectToApi). */
|
|
69
|
+
setHoldPending(hold) {
|
|
70
|
+
this.holdPending = hold;
|
|
71
|
+
}
|
|
72
|
+
/** Any push shows they reach the plugin; only a motion push ends the refresh count. */
|
|
73
|
+
recordPush(isMotion) {
|
|
74
|
+
this.missTimes = [];
|
|
75
|
+
const checkPending = !this.pushSeen || this.requested, firstMotionPush = isMotion && !this.motionPushSeen;
|
|
76
|
+
this.pushSeen = true;
|
|
77
|
+
this.requested = false;
|
|
78
|
+
this.motionPushSeen ||= isMotion;
|
|
79
|
+
if (!checkPending && !firstMotionPush)
|
|
80
|
+
return;
|
|
81
|
+
const state = readPushRecoveryState(this.storagePath), update = {};
|
|
82
|
+
// A refresh still waiting for a restart (main bridge) is no longer needed.
|
|
83
|
+
if (checkPending && state.refreshPending && !this.holdPending) {
|
|
84
|
+
update.refreshPending = false;
|
|
85
|
+
}
|
|
86
|
+
if (firstMotionPush && state.refreshesWithoutPush)
|
|
87
|
+
update.refreshesWithoutPush = 0;
|
|
88
|
+
if (!Object.keys(update).length)
|
|
89
|
+
return;
|
|
90
|
+
writePushRecoveryState(this.storagePath, { ...state, ...update });
|
|
91
|
+
if (update.refreshesWithoutPush === 0) {
|
|
92
|
+
logInfo('Ring push notifications arrive again after the automatic Ring session refresh');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
recordMissed(count) {
|
|
96
|
+
if (this.done || count < 1)
|
|
97
|
+
return;
|
|
98
|
+
const at = this.elapsed();
|
|
99
|
+
for (let i = 0; i < count; i++)
|
|
100
|
+
this.missTimes.push(at);
|
|
101
|
+
if (this.missTimes.length < MISSES_BEFORE_REFRESH ||
|
|
102
|
+
at - this.missTimes[0] < MISS_SPAN_BEFORE_REFRESH_MS) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const now = this.now();
|
|
106
|
+
const state = readPushRecoveryState(this.storagePath), refreshes = state.refreshesWithoutPush ?? 0;
|
|
107
|
+
if (refreshes >= MAX_REFRESHES_WITHOUT_PUSH) {
|
|
108
|
+
this.done = true;
|
|
109
|
+
logError(`Ring still sends no push notifications after ${refreshes} automatic Ring session refreshes; not trying again until one arrives. Check that mtalk.google.com resolves and port 5228 is open, and remove old homebridge-ring-secure-video entries from Authorized Client Devices in the Ring app (see the README, Troubleshooting).`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// A refresh saved later than now (one of the clocks was wrong) counts as done now, so the
|
|
113
|
+
// next one waits at most a day.
|
|
114
|
+
if ((state.lastRefreshAt ?? 0) > now) {
|
|
115
|
+
state.lastRefreshAt = now;
|
|
116
|
+
writePushRecoveryState(this.storagePath, state);
|
|
117
|
+
}
|
|
118
|
+
const nextAllowedAt = (state.lastRefreshAt ?? 0) + REFRESH_INTERVAL_MS;
|
|
119
|
+
if (now < nextAllowedAt) {
|
|
120
|
+
if (this.loggedWaitUntil !== nextAllowedAt) {
|
|
121
|
+
this.loggedWaitUntil = nextAllowedAt;
|
|
122
|
+
logInfo(`Ring sends no push notifications for motion; the last automatic Ring session refresh was less than a day ago, so the next one is due after ${new Date(nextAllowedAt).toLocaleString()}`);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.done = true;
|
|
127
|
+
if (writePushRecoveryState(this.storagePath, {
|
|
128
|
+
refreshPending: true,
|
|
129
|
+
lastRefreshAt: now,
|
|
130
|
+
refreshesWithoutPush: refreshes + 1,
|
|
131
|
+
})) {
|
|
132
|
+
this.requested = true;
|
|
133
|
+
this.refresh();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
package/lib/ring-platform.js
CHANGED
|
@@ -19,6 +19,9 @@ 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 { PushHealthMonitor, readPushRecoveryState, writePushRecoveryState, } from "./push-health.js";
|
|
23
|
+
import { readFileSync } from 'fs';
|
|
24
|
+
import { getSessionRefreshSensorUuid, SessionRefreshSensor, } from "./session-refresh-sensor.js";
|
|
22
25
|
import { PanicButtons } from "./panic-buttons.js";
|
|
23
26
|
import { logInfo, useLogger } from 'ring-client-api/util';
|
|
24
27
|
import { FloodFreezeSensor } from "./flood-freeze-sensor.js";
|
|
@@ -135,7 +138,8 @@ function removePushCredentialsFromRefreshToken(refreshToken, removeHardwareId =
|
|
|
135
138
|
}
|
|
136
139
|
function updateRefreshTokenInConfig(configContents, oldRefreshToken, newRefreshToken, clearRefreshFlags) {
|
|
137
140
|
try {
|
|
138
|
-
const homebridgeConfig = JSON.parse(configContents), platformConfig = homebridgeConfig.platforms?.find((platform) => platform.platform === platformName
|
|
141
|
+
const homebridgeConfig = JSON.parse(configContents), platformConfig = homebridgeConfig.platforms?.find((platform) => (platform.platform === platformName ||
|
|
142
|
+
platform.platform === `${pluginName}.${platformName}`) &&
|
|
139
143
|
platform.refreshToken === oldRefreshToken);
|
|
140
144
|
if (!platformConfig) {
|
|
141
145
|
throw new Error('Ring platform config not found');
|
|
@@ -151,15 +155,46 @@ function updateRefreshTokenInConfig(configContents, oldRefreshToken, newRefreshT
|
|
|
151
155
|
return configContents.replace(oldRefreshToken, newRefreshToken);
|
|
152
156
|
}
|
|
153
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* This platform's block as saved in config.json now, if exactly one block matches and it has a
|
|
160
|
+
* Ring token.
|
|
161
|
+
*
|
|
162
|
+
* Homebridge restarts a child bridge with the config it read when it started, not the one in
|
|
163
|
+
* config.json now: after a renewal the token it hands over is out of date (and replacing it in
|
|
164
|
+
* config.json would then fail), and a force setting cleared since would be on again. The block is
|
|
165
|
+
* this child bridge's, or the one on the main bridge.
|
|
166
|
+
*/
|
|
167
|
+
export function getSavedPlatformConfig(api, config) {
|
|
168
|
+
try {
|
|
169
|
+
// Homebridge upper-cases the username it hands over; config.json may have it either way.
|
|
170
|
+
const username = config._bridge?.username?.toUpperCase(), blocks = (JSON.parse(readFileSync(api.user.configPath()).toString()).platforms ?? []).filter((block) => (block.platform === platformName ||
|
|
171
|
+
block.platform === `${pluginName}.${platformName}`) &&
|
|
172
|
+
(username
|
|
173
|
+
? String(block._bridge?.username ?? '').toUpperCase() === username
|
|
174
|
+
: !block._bridge));
|
|
175
|
+
const block = blocks.length === 1 ? blocks[0] : undefined;
|
|
176
|
+
return typeof block?.refreshToken === 'string' && block.refreshToken ? block : undefined;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function clearRefreshPending(storagePath, update = {}) {
|
|
183
|
+
const state = readPushRecoveryState(storagePath);
|
|
184
|
+
if (state.refreshPending) {
|
|
185
|
+
writePushRecoveryState(storagePath, { ...state, ...update, refreshPending: false });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
154
188
|
/**
|
|
155
189
|
* Saves each new Ring refresh token into config.json, replacing the token that is there now.
|
|
156
190
|
*
|
|
157
191
|
* The token in config.json is tracked here rather than taken from ring-client-api's
|
|
158
192
|
* "oldRefreshToken": after a forced refresh, ring-client-api starts from a modified token that
|
|
159
193
|
* was never written to config.json, and following its old token would save only the first new
|
|
160
|
-
* token and none after it (so the next start would register with Ring again).
|
|
194
|
+
* token and none after it (so the next start would register with Ring again). onSaved is called
|
|
195
|
+
* after the first save.
|
|
161
196
|
*/
|
|
162
|
-
export function createRefreshTokenSaver(api, tokenInConfig, clearRefreshFlags) {
|
|
197
|
+
export function createRefreshTokenSaver(api, tokenInConfig, clearRefreshFlags, onSaved) {
|
|
163
198
|
let current = tokenInConfig,
|
|
164
199
|
// Only with the first save: turning a force setting back on later must survive renewals.
|
|
165
200
|
clearFlags = clearRefreshFlags, warned = false;
|
|
@@ -174,6 +209,9 @@ export function createRefreshTokenSaver(api, tokenInConfig, clearRefreshFlags) {
|
|
|
174
209
|
if (saved) {
|
|
175
210
|
current = newRefreshToken;
|
|
176
211
|
clearFlags = false;
|
|
212
|
+
const firstSave = onSaved;
|
|
213
|
+
onSaved = undefined;
|
|
214
|
+
firstSave?.();
|
|
177
215
|
}
|
|
178
216
|
else if (!warned) {
|
|
179
217
|
// Once per run: after a new sign-in while running, every renewal would end up here.
|
|
@@ -206,7 +244,7 @@ export function isPermanentRingError(error) {
|
|
|
206
244
|
*/
|
|
207
245
|
function markNotResponding(accessory) {
|
|
208
246
|
const failure = () => {
|
|
209
|
-
throw new hap.HapStatusError(
|
|
247
|
+
throw new hap.HapStatusError(hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
|
210
248
|
};
|
|
211
249
|
for (const service of accessory.services) {
|
|
212
250
|
if (service.UUID === hap.Service.AccessoryInformation.UUID)
|
|
@@ -268,9 +306,22 @@ export class RingPlatform {
|
|
|
268
306
|
this.api.on('shutdown', () => {
|
|
269
307
|
this.shuttingDown = true;
|
|
270
308
|
clearTimeout(this.retryTimer);
|
|
309
|
+
this.sessionRefreshSensor?.stop();
|
|
271
310
|
});
|
|
311
|
+
if (config.automaticSessionRefresh !== false) {
|
|
312
|
+
this.pushHealth = new PushHealthMonitor(api.user.storagePath(), () => this.refreshSessionAutomatically());
|
|
313
|
+
}
|
|
272
314
|
this.api.on('didFinishLaunching', () => {
|
|
273
315
|
this.log.debug('didFinishLaunching');
|
|
316
|
+
const saved = config.refreshToken ? getSavedPlatformConfig(api, config) : undefined;
|
|
317
|
+
if (saved) {
|
|
318
|
+
if (saved.refreshToken !== config.refreshToken) {
|
|
319
|
+
this.log.info('Using the newer Ring token saved in config.json');
|
|
320
|
+
config.refreshToken = saved.refreshToken;
|
|
321
|
+
}
|
|
322
|
+
config.forceRefreshRingPushCredentials = Boolean(saved.forceRefreshRingPushCredentials);
|
|
323
|
+
config.forceRefreshRingApiSession = Boolean(saved.forceRefreshRingApiSession);
|
|
324
|
+
}
|
|
274
325
|
if (config.refreshToken) {
|
|
275
326
|
void this.connectWithRetry();
|
|
276
327
|
}
|
|
@@ -282,6 +333,31 @@ export class RingPlatform {
|
|
|
282
333
|
retryTimer;
|
|
283
334
|
shuttingDown = false;
|
|
284
335
|
devicesSetUp = false;
|
|
336
|
+
pushHealth;
|
|
337
|
+
gotNewToken = false;
|
|
338
|
+
sessionRefreshSensor;
|
|
339
|
+
startedAt = Date.now();
|
|
340
|
+
/**
|
|
341
|
+
* Ring has stopped sending push notifications: the session is refreshed at the next start (see
|
|
342
|
+
* connectToApi). A child bridge restarts itself for that; Homebridge starts it again at once.
|
|
343
|
+
*/
|
|
344
|
+
refreshSessionAutomatically() {
|
|
345
|
+
if (this.shuttingDown)
|
|
346
|
+
return;
|
|
347
|
+
const reason = 'Ring event history shows motion that Ring sent no push notification for (3 events over at least 30 minutes, none pushed)';
|
|
348
|
+
if (this.config._bridge && typeof process.send === 'function') {
|
|
349
|
+
this.log.warn(`${reason}; restarting this child bridge to refresh the Ring API session (turn off "Automatic Ring Session Refresh" to stop this)`);
|
|
350
|
+
this.restartChildBridge();
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
this.log.warn(`${reason}; the Ring API session will be refreshed the next time Homebridge restarts (run the plugin as a child bridge to have this done automatically)`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// A separate method so tests can stand in for it. Homebridge's own shutdown (SIGTERM) stops
|
|
357
|
+
// the accessories cleanly, and a child bridge that ends that way is started again at once.
|
|
358
|
+
restartChildBridge() {
|
|
359
|
+
process.kill(process.pid, 'SIGTERM');
|
|
360
|
+
}
|
|
285
361
|
/**
|
|
286
362
|
* Connects to Ring, retrying later if Ring can't be reached when Homebridge starts (an outage,
|
|
287
363
|
* a server error), with a fresh connection each time: until then the alarm and every other
|
|
@@ -317,9 +393,13 @@ export class RingPlatform {
|
|
|
317
393
|
this.homebridgeAccessories[accessory.UUID] = accessory;
|
|
318
394
|
}
|
|
319
395
|
async connectToApi() {
|
|
320
|
-
const { api, config } = this,
|
|
321
|
-
|
|
322
|
-
|
|
396
|
+
const { api, config } = this, storagePath = api.user.storagePath(),
|
|
397
|
+
// Asked for by the push check (refreshSessionAutomatically) before the last restart.
|
|
398
|
+
refreshPendingInState = Boolean(readPushRecoveryState(storagePath).refreshPending),
|
|
399
|
+
// Like the force settings, not again on a retry once Ring has given a new token.
|
|
400
|
+
automaticRefresh = Boolean(this.pushHealth) && refreshPendingInState && !this.gotNewToken, shouldRotateRingApiSession = (config.forceRefreshRingApiSession || automaticRefresh) && config.refreshToken, systemId = shouldRotateRingApiSession
|
|
401
|
+
? rotateSystemId(storagePath)
|
|
402
|
+
: getSystemId(storagePath), configuredHomeKitAccessoryTag = config.homeKitAccessoryTag?.trim(), accessoryIdSuffix = configuredHomeKitAccessoryTag
|
|
323
403
|
? `-${configuredHomeKitAccessoryTag}`
|
|
324
404
|
: '', accessoryNameSuffix = configuredHomeKitAccessoryTag
|
|
325
405
|
? ` (${configuredHomeKitAccessoryTag})`
|
|
@@ -329,7 +409,17 @@ export class RingPlatform {
|
|
|
329
409
|
? removePushCredentialsFromRefreshToken(config.refreshToken, Boolean(shouldRotateRingApiSession))
|
|
330
410
|
: undefined;
|
|
331
411
|
if (shouldRotateRingApiSession) {
|
|
332
|
-
logInfo(
|
|
412
|
+
logInfo(automaticRefresh && !config.forceRefreshRingApiSession
|
|
413
|
+
? 'Forcing Ring API session refresh because Ring stopped sending push notifications (automatic session refresh)'
|
|
414
|
+
: 'Forcing Ring API session refresh because forceRefreshRingApiSession is enabled');
|
|
415
|
+
}
|
|
416
|
+
// Until its new token is saved, a push must not drop this run's refresh: the next start
|
|
417
|
+
// would sign in with the old token again.
|
|
418
|
+
if (automaticRefresh)
|
|
419
|
+
this.pushHealth?.setHoldPending(true);
|
|
420
|
+
// Asked for while the setting was on: not done later on old evidence once it is back on.
|
|
421
|
+
if (refreshPendingInState && !this.pushHealth) {
|
|
422
|
+
clearRefreshPending(storagePath);
|
|
333
423
|
}
|
|
334
424
|
if (refreshTokenWithoutPushCredentials) {
|
|
335
425
|
logInfo('Forcing Ring push credential refresh because Ring push credentials were removed from the startup token');
|
|
@@ -342,13 +432,27 @@ export class RingPlatform {
|
|
|
342
432
|
: {}),
|
|
343
433
|
systemId,
|
|
344
434
|
});
|
|
345
|
-
ringApi.onRefreshTokenUpdated.subscribe(createRefreshTokenSaver(this.api, config.refreshToken, Boolean(refreshTokenWithoutPushCredentials)
|
|
435
|
+
ringApi.onRefreshTokenUpdated.subscribe(createRefreshTokenSaver(this.api, config.refreshToken, Boolean(refreshTokenWithoutPushCredentials),
|
|
436
|
+
// Cleared once the new identity's token is saved: until then the next start would
|
|
437
|
+
// sign in with the old one.
|
|
438
|
+
automaticRefresh
|
|
439
|
+
? () => {
|
|
440
|
+
const doneAt = Date.now();
|
|
441
|
+
this.pushHealth?.setHoldPending(false);
|
|
442
|
+
clearRefreshPending(storagePath, { refreshDoneAt: doneAt });
|
|
443
|
+
// Usually saved while connecting, before the sensor is set up (which then reads
|
|
444
|
+
// refreshDoneAt); this covers a save after that.
|
|
445
|
+
this.sessionRefreshSensor?.show(doneAt);
|
|
446
|
+
this.log.warn('Automatic Ring session refresh done: in the Ring app, Control Center > Authorized Client Devices now lists homebridge-ring-secure-video twice. Remove the older entry (keep the newest).');
|
|
447
|
+
}
|
|
448
|
+
: undefined));
|
|
346
449
|
// A retry must sign in with the newest token (Ring may have replaced the old one) and must
|
|
347
450
|
// not force a new identity or push registration again.
|
|
348
451
|
ringApi.onRefreshTokenUpdated.subscribe(({ newRefreshToken }) => {
|
|
349
452
|
config.refreshToken = newRefreshToken;
|
|
350
453
|
config.forceRefreshRingPushCredentials = false;
|
|
351
454
|
config.forceRefreshRingApiSession = false;
|
|
455
|
+
this.gotNewToken = true;
|
|
352
456
|
});
|
|
353
457
|
let locations, loadedDevices;
|
|
354
458
|
try {
|
|
@@ -483,7 +587,7 @@ export class RingPlatform {
|
|
|
483
587
|
addTo = platformAccessories;
|
|
484
588
|
}
|
|
485
589
|
return accessory;
|
|
486
|
-
}, homebridgeAccessory = this.homebridgeAccessories[uuid] || createHomebridgeAccessory(), accessory = new AccessoryClass(device, homebridgeAccessory, config);
|
|
590
|
+
}, homebridgeAccessory = this.homebridgeAccessories[uuid] || createHomebridgeAccessory(), accessory = new AccessoryClass(device, homebridgeAccessory, config, this.pushHealth);
|
|
487
591
|
accessory.initBase();
|
|
488
592
|
// Registered only once set up: a half-built accessory isn't added to HomeKit.
|
|
489
593
|
addTo?.push(homebridgeAccessory);
|
|
@@ -516,6 +620,20 @@ export class RingPlatform {
|
|
|
516
620
|
}
|
|
517
621
|
});
|
|
518
622
|
}));
|
|
623
|
+
// Opens after an automatic session refresh, so the Home app can notify.
|
|
624
|
+
if (this.pushHealth && config.sessionRefreshSensor) {
|
|
625
|
+
const sensorUuid = getSessionRefreshSensorUuid(`${debug ? 'TEST ' : ''}ring-secure-video${accessoryIdSuffix}`), sensorName = `Ring Session Refreshed${accessoryNameSuffix}`;
|
|
626
|
+
let sensorAccessory = this.homebridgeAccessories[sensorUuid];
|
|
627
|
+
if (!sensorAccessory) {
|
|
628
|
+
sensorAccessory = new api.platformAccessory(sensorName, sensorUuid, 10 /* hap.Categories.SENSOR */);
|
|
629
|
+
logInfo(`Adding session refresh sensor ${sensorUuid} ${sensorName}`);
|
|
630
|
+
platformAccessories.push(sensorAccessory);
|
|
631
|
+
this.homebridgeAccessories[sensorUuid] = sensorAccessory;
|
|
632
|
+
}
|
|
633
|
+
this.sessionRefreshSensor = new SessionRefreshSensor(sensorAccessory, sensorName, this.startedAt);
|
|
634
|
+
this.sessionRefreshSensor.show(readPushRecoveryState(storagePath).refreshDoneAt);
|
|
635
|
+
activeAccessoryIds.push(sensorUuid);
|
|
636
|
+
}
|
|
519
637
|
const failedLocations = locations.filter((_, i) => locationResults[i].status === 'rejected');
|
|
520
638
|
locationResults.forEach((result, i) => {
|
|
521
639
|
if (result.status === 'rejected') {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { logInfo } from 'ring-client-api/util';
|
|
2
|
+
import { hap } from "./hap.js";
|
|
3
|
+
/** How long the sensor stays open after an automatic Ring session refresh. */
|
|
4
|
+
export const SESSION_REFRESH_SENSOR_OPEN_MS = 24 * 60 * 60_000;
|
|
5
|
+
/** After a home hub subscribes again, the sensor opens this much later... */
|
|
6
|
+
export const SESSION_REFRESH_SENSOR_SUBSCRIBED_DELAY_MS = 10_000;
|
|
7
|
+
/** ...or after this long at most. */
|
|
8
|
+
export const SESSION_REFRESH_SENSOR_MAX_DELAY_MS = 3 * 60_000;
|
|
9
|
+
// Kept stable so the accessory, and its room and notification settings, survive restarts.
|
|
10
|
+
export function getSessionRefreshSensorUuid(id) {
|
|
11
|
+
return hap.uuid.generate(`${id}-zm-session-refresh`);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A contact sensor that opens after an automatic Ring session refresh, so the Home app can
|
|
15
|
+
* notify: the older homebridge-ring-secure-video entry in Authorized Client Devices is then to
|
|
16
|
+
* be removed. It closes again a day later.
|
|
17
|
+
*
|
|
18
|
+
* The refresh happens right after a restart, and the Home app notifies for a change it sees, not
|
|
19
|
+
* for a value it finds: so after a refresh in this run the sensor opens 10 s after a Home app
|
|
20
|
+
* or home hub is subscribed to it (or after 3 minutes). After a restart later that day it is
|
|
21
|
+
* simply open, without a second notification.
|
|
22
|
+
*/
|
|
23
|
+
export class SessionRefreshSensor {
|
|
24
|
+
characteristic;
|
|
25
|
+
runStartedAt;
|
|
26
|
+
openTimer;
|
|
27
|
+
closeTimer;
|
|
28
|
+
onSubscribe;
|
|
29
|
+
constructor(accessory, name, runStartedAt) {
|
|
30
|
+
const { Characteristic, Service } = hap;
|
|
31
|
+
accessory
|
|
32
|
+
.getService(Service.AccessoryInformation)
|
|
33
|
+
.setCharacteristic(Characteristic.Manufacturer, 'Ring')
|
|
34
|
+
.setCharacteristic(Characteristic.Model, 'Session refresh sensor')
|
|
35
|
+
.setCharacteristic(Characteristic.SerialNumber, 'session-refresh');
|
|
36
|
+
const service = accessory.getService(Service.ContactSensor) ??
|
|
37
|
+
accessory.addService(Service.ContactSensor, name);
|
|
38
|
+
// Its value is kept from the last run (Homebridge caches it) until show() decides: every
|
|
39
|
+
// update is sent to the Home app, so closing it here and opening it again in show() would
|
|
40
|
+
// notify a second time.
|
|
41
|
+
this.characteristic = service.getCharacteristic(Characteristic.ContactSensorState);
|
|
42
|
+
this.runStartedAt = runStartedAt;
|
|
43
|
+
}
|
|
44
|
+
set(value) {
|
|
45
|
+
if (this.characteristic.value !== value)
|
|
46
|
+
this.characteristic.updateValue(value);
|
|
47
|
+
}
|
|
48
|
+
/** Shows a refresh done at refreshDoneAt (if it is less than a day ago); closed otherwise. */
|
|
49
|
+
show(refreshDoneAt) {
|
|
50
|
+
const now = Date.now(), { ContactSensorState } = hap.Characteristic, closeAt = (refreshDoneAt ?? 0) + SESSION_REFRESH_SENSOR_OPEN_MS;
|
|
51
|
+
this.stop();
|
|
52
|
+
if (!refreshDoneAt || refreshDoneAt > now || closeAt <= now) {
|
|
53
|
+
this.set(ContactSensorState.CONTACT_DETECTED);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const open = () => {
|
|
57
|
+
this.stopWaiting();
|
|
58
|
+
if (Date.now() < closeAt)
|
|
59
|
+
this.set(ContactSensorState.CONTACT_NOT_DETECTED);
|
|
60
|
+
};
|
|
61
|
+
if (refreshDoneAt < this.runStartedAt) {
|
|
62
|
+
open();
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// A refresh in this run: closed until the Home app can see it open.
|
|
66
|
+
this.set(ContactSensorState.CONTACT_DETECTED);
|
|
67
|
+
this.openTimer = setTimeout(() => {
|
|
68
|
+
open();
|
|
69
|
+
logInfo('Ring Session Refreshed sensor opened (remove the older entry in Authorized Client Devices)');
|
|
70
|
+
}, Math.min(SESSION_REFRESH_SENSOR_MAX_DELAY_MS, closeAt - now));
|
|
71
|
+
this.onSubscribe = () => {
|
|
72
|
+
clearTimeout(this.openTimer);
|
|
73
|
+
this.openTimer = setTimeout(() => {
|
|
74
|
+
open();
|
|
75
|
+
logInfo('Ring Session Refreshed sensor opened (remove the older entry in Authorized Client Devices)');
|
|
76
|
+
}, SESSION_REFRESH_SENSOR_SUBSCRIBED_DELAY_MS);
|
|
77
|
+
this.openTimer.unref?.();
|
|
78
|
+
this.stopWaiting();
|
|
79
|
+
};
|
|
80
|
+
this.openTimer.unref?.();
|
|
81
|
+
// hap-nodejs only reports the first subscriber; a home hub that came back while the
|
|
82
|
+
// plugin was connecting to Ring has subscribed already (the count is private).
|
|
83
|
+
if (this.characteristic.subscriptions) {
|
|
84
|
+
this.onSubscribe();
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
this.characteristic.on("subscribe" /* hap.CharacteristicEventTypes.SUBSCRIBE */, this.onSubscribe);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
this.closeTimer = setTimeout(() => {
|
|
91
|
+
this.set(ContactSensorState.CONTACT_DETECTED);
|
|
92
|
+
}, closeAt - now);
|
|
93
|
+
this.closeTimer.unref?.();
|
|
94
|
+
}
|
|
95
|
+
stopWaiting() {
|
|
96
|
+
if (this.onSubscribe) {
|
|
97
|
+
this.characteristic.removeListener("subscribe" /* hap.CharacteristicEventTypes.SUBSCRIBE */, this.onSubscribe);
|
|
98
|
+
this.onSubscribe = undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
stop() {
|
|
102
|
+
this.stopWaiting();
|
|
103
|
+
clearTimeout(this.openTimer);
|
|
104
|
+
clearTimeout(this.closeTimer);
|
|
105
|
+
}
|
|
106
|
+
}
|
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.
|
|
4
|
+
"version": "1.0.0-beta.6",
|
|
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",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@eneris/push-receiver": "4.3.1",
|
|
20
20
|
"@homebridge/camera-utils": "^3.0.0",
|
|
21
|
-
"@homebridge/plugin-ui-utils": "^2.
|
|
21
|
+
"@homebridge/plugin-ui-utils": "^2.3.0",
|
|
22
22
|
"ring-client-api": "14.3.0",
|
|
23
23
|
"rxjs": "^7.8.2",
|
|
24
24
|
"werift": "0.22.4"
|
|
@@ -26,18 +26,18 @@
|
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@eslint/js": "9.39.2",
|
|
28
28
|
"@typescript-eslint/parser": "^8.54.0",
|
|
29
|
-
"ajv": "8.
|
|
30
|
-
"concurrently": "^
|
|
29
|
+
"ajv": "8.20.0",
|
|
30
|
+
"concurrently": "^10.0.7",
|
|
31
31
|
"eslint": "9.39.2",
|
|
32
|
-
"globals": "17.
|
|
33
|
-
"homebridge": "^2.
|
|
32
|
+
"globals": "17.14.0",
|
|
33
|
+
"homebridge": "^2.5.0",
|
|
34
34
|
"homebridge-config-ui-x": "^5.24.0",
|
|
35
35
|
"typescript": "5.9.3",
|
|
36
|
-
"typescript-eslint": "8.
|
|
36
|
+
"typescript-eslint": "8.72.2"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
|
-
"node": "^
|
|
40
|
-
"homebridge": "
|
|
39
|
+
"node": "^22 || ^24",
|
|
40
|
+
"homebridge": "^1.8.0 || ^2.0.0-beta.0"
|
|
41
41
|
},
|
|
42
42
|
"homebridge": {
|
|
43
43
|
"pluginAlias": "Ring Secure Video",
|