Skip to content

Commit 8fabd14

Browse files
committed
fix: improved snapshot timing for battery cameras
1 parent 705860a commit 8fabd14

File tree

3 files changed

+60
-43
lines changed

3 files changed

+60
-43
lines changed

api/ring-camera.ts

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {
22
ActiveDing,
3+
batteryCameraKinds,
34
CameraData,
45
CameraHealth,
56
HistoricalDingGlobal,
67
RingCameraModel,
7-
batteryCameraKinds,
88
SnapshotTimestamp
99
} from './ring-types'
1010
import { clientApi, RingRestClient } from './rest-client'
@@ -20,7 +20,7 @@ import {
2020
} from 'rxjs/operators'
2121
import { createSocket } from 'dgram'
2222
import { bindToRandomPort, getPublicIp } from './rtp-utils'
23-
import { delay, logError } from './util'
23+
import { delay, logError, logInfo } from './util'
2424
import { SipSession, SrtpOptions } from './sip-session'
2525

2626
const getPort = require('get-port')
@@ -204,7 +204,11 @@ export class RingCamera {
204204
return response.url
205205
}
206206

207-
private async getTimestampAge() {
207+
private isTimestampInLifeTime(timestampAge: number) {
208+
return timestampAge < this.snapshotLifeTime
209+
}
210+
211+
private async getSnapshotTimestamp() {
208212
const { timestamps, responseTimestamp } = await this.restClient.request<{
209213
timestamps: SnapshotTimestamp[]
210214
}>({
@@ -216,34 +220,44 @@ export class RingCamera {
216220
json: true
217221
}),
218222
deviceTimestamp = timestamps[0],
219-
timestamp = deviceTimestamp ? deviceTimestamp.timestamp : 0
220-
221-
return Math.abs(responseTimestamp - timestamp)
222-
}
223-
224-
private refreshSnapshotInProgress?: Promise<void>
225-
private snapshotLifeTime = (this.hasBattery ? 600 : 30) * 1000 // battery cams only refresh timestamp every 10 minutes
223+
timestamp = deviceTimestamp ? deviceTimestamp.timestamp : 0,
224+
timestampAge = Math.abs(responseTimestamp - timestamp)
226225

227-
private async refreshSnapshot(allowStale: boolean) {
228-
const initialTimestampAge = await this.getTimestampAge(),
229-
snapshotInLifeTime = initialTimestampAge < this.snapshotLifeTime
226+
this.lastSnapshotTimestampLocal = timestamp ? Date.now() - timestampAge : 0
230227

231-
if (allowStale && (this.hasBattery || snapshotInLifeTime)) {
232-
// battery cameras take a long time to refresh snapshots. Just return the stale one immediately.
233-
// for non battery, stale snapshots can be used if they are within the last 30 seconds
234-
return
228+
return {
229+
timestamp,
230+
inLifeTime: this.isTimestampInLifeTime(timestampAge)
235231
}
232+
}
236233

237-
if (snapshotInLifeTime) {
238-
// not allowing stale, so wait until a new snapshot should be available
239-
await delay(this.snapshotLifeTime - initialTimestampAge)
234+
private refreshSnapshotInProgress?: Promise<boolean>
235+
private snapshotLifeTime = (this.hasBattery ? 600 : 30) * 1000 // battery cams only refresh timestamp every 10 minutes
236+
private lastSnapshotTimestampLocal = 0
237+
private lastSnapshotPromise?: Promise<Buffer>
238+
239+
private async refreshSnapshot() {
240+
const currentTimestampAge = Date.now() - this.lastSnapshotTimestampLocal
241+
if (this.isTimestampInLifeTime(currentTimestampAge)) {
242+
logInfo(
243+
`Snapshot for ${
244+
this.name
245+
} is still within it's life time (${currentTimestampAge / 1000}s old)`
246+
)
247+
return true
240248
}
241249

242250
for (let i = 0; i < maxSnapshotRefreshAttempts; i++) {
243-
const timestampAge = await this.getTimestampAge()
251+
const { timestamp, inLifeTime } = await this.getSnapshotTimestamp()
244252

245-
if (timestampAge < initialTimestampAge) {
246-
return
253+
if (!timestamp && this.isOffline) {
254+
throw new Error(
255+
`No snapshot available and device ${this.name} is offline`
256+
)
257+
}
258+
259+
if (inLifeTime) {
260+
return false
247261
}
248262

249263
await delay(snapshotRefreshDelay)
@@ -256,23 +270,35 @@ export class RingCamera {
256270

257271
async getSnapshot(allowStale = false) {
258272
this.refreshSnapshotInProgress =
259-
this.refreshSnapshotInProgress || this.refreshSnapshot(allowStale)
273+
this.refreshSnapshotInProgress || this.refreshSnapshot()
260274

261275
try {
262-
await this.refreshSnapshotInProgress
276+
const useLastSnapshot = await this.refreshSnapshotInProgress
277+
278+
if (useLastSnapshot && this.lastSnapshotPromise) {
279+
this.refreshSnapshotInProgress = undefined
280+
return this.lastSnapshotPromise
281+
}
263282
} catch (e) {
283+
logError(e.message)
264284
if (!allowStale) {
265-
logError(e)
266285
throw e
267286
}
268287
}
269288

270289
this.refreshSnapshotInProgress = undefined
271290

272-
return this.restClient.request<Buffer>({
291+
this.lastSnapshotPromise = this.restClient.request<Buffer>({
273292
url: clientApi(`snapshots/image/${this.id}`),
274293
responseType: 'arraybuffer'
275294
})
295+
296+
this.lastSnapshotPromise.catch(() => {
297+
// snapshot request failed, don't use it again
298+
this.lastSnapshotPromise = undefined
299+
})
300+
301+
return this.lastSnapshotPromise
276302
}
277303

278304
sipUsedDingIds: string[] = []

homebridge/README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,19 +98,18 @@ Walk through the setup pages and when you are done, you should see several devic
9898

9999
* Camera Feed
100100
* Shows a snapshot from the camera while viewing the room in Home
101-
* (**NEW**) Shows a live feed from the camera if you click on it. This still has some kinks to work out, like lack of sound and only staying active for about 30 seconds. I will be tweaking live feeds over the coming weeks.
101+
* Shows a live feed from the camera if you click on it. The feed is **video only** and may not work on some networks with strict NAT settings.
102102
* Motion Sensor
103103
* Can be hidden with `hideCameraMotionSensor`
104-
* Light (if camera is equipped)
104+
* Light (if camera is equipped)
105105
* Siren Switch (if camera is equipped)
106106
* Can be hidden with `hideCameraSirenSwitch`
107107
* Programmable switch for doorbells (triggers `Single Press` actions)
108108
* Note: doorbell event notifications should be configured via settings on the camera feed
109109
* Can be hidden with `hideDoorbellSwitch`
110110

111-
**Battery Camera Limitations** - Ring cameras that have batteries (even if they are wired) only refresh their snapshot image every 10 minutes (vs 30 seconds for wired cameras).
112-
Because of this limitation, most snapshots for these cameras will be stale (meaning they could have been taken minutes or hours in the past).
113-
This is limitation of the Ring api does not have a good workaround at this point.
111+
**Battery Camera Limitations** - Ring cameras that have batteries (even if they are plugged in or using solar) only refresh their snapshot image every 10 minutes (vs 30 seconds for wired cameras).
112+
Because of this limitation, snapshots from these cameras may be up to 10 minutes old. This is a limitation of the Ring api and there is no way to work around it.
114113

115114
If you turn on notifications for the motion sensors, or for any doorbell camera, you will get rich notifications from
116115
HomeKit with a snapshot from the camera

homebridge/camera-source.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,20 +75,12 @@ export class CameraSource {
7575
) {
7676
const start = Date.now()
7777
try {
78-
this.logger.info(`Snapshot Requested for ${this.ringCamera.name}`)
79-
80-
if (this.ringCamera.isOffline) {
81-
this.logger.error(
82-
`Cannot retrieve snapshot because ${this.ringCamera.name} is offline. Make sure it has power and a good wifi connection.`
83-
)
84-
callback(new Error('Offline'))
85-
return
86-
}
78+
this.logger.info(`Snapshot for ${this.ringCamera.name} Requested`)
8779

8880
const snapshot = await this.ringCamera.getSnapshot(true),
8981
duration = (Date.now() - start) / 1000
9082
this.logger.info(
91-
`Snapshot Received for ${this.ringCamera.name} (${getDurationSeconds(
83+
`Snapshot for ${this.ringCamera.name} Received (${getDurationSeconds(
9284
start
9385
)}s)`
9486
)
@@ -101,7 +93,7 @@ export class CameraSource {
10193
this.ringCamera.name
10294
} (${getDurationSeconds(
10395
start
104-
)}s). Make sure the camera has power and a good wifi connection. The camera currently reports that is it ${
96+
)}s). The camera currently reports that is it ${
10597
this.ringCamera.isOffline ? 'offline' : 'online'
10698
}`
10799
)

0 commit comments

Comments
 (0)