ring-client-api 9.22.0 → 9.25.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,49 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [9.25.0-beta.0](https://github.com/dgreif/ring/compare/v9.24.0...v9.25.0-beta.0) (2022-01-29)
6
+
7
+
8
+ ### Features
9
+
10
+ * **api:** decouple ffmpeg prep from execution ([#859](https://github.com/dgreif/ring/issues/859)) ([e3cc9d1](https://github.com/dgreif/ring/commit/e3cc9d15584377bf2be94a9d457efc3dfcb4bd8b))
11
+ * handle new camera types with no battery ([11ae286](https://github.com/dgreif/ring/commit/11ae28697d8cb0e2da71e880c82e1883f04047b9))
12
+ * slightly faster snapshots via new endpoint ([b840471](https://github.com/dgreif/ring/commit/b840471bc6b7b66e5873370251bd57c1a35b267a))
13
+
14
+ ## [9.24.0](https://github.com/dgreif/ring/compare/v9.23.0...v9.24.0) (2022-01-22)
15
+
16
+
17
+ ### Features
18
+
19
+ * support glassbreak sensor ([98271f9](https://github.com/dgreif/ring/commit/98271f91f13aaae051fccaab7165cd17f2a2377c))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * allow process to exit when RingApi.disconnect() is called ([#851](https://github.com/dgreif/ring/issues/851)) ([8ef25ac](https://github.com/dgreif/ring/commit/8ef25acca66c533873b7fd43c7c899e34b4a7617))
25
+ * avoid error when ring terminates video stream ([7717d5e](https://github.com/dgreif/ring/commit/7717d5eaa40afed3d34f0e8e6eea1402e21afef7)), closes [#853](https://github.com/dgreif/ring/issues/853)
26
+ * update dependencies ([c80d052](https://github.com/dgreif/ring/commit/c80d052f9eaf4c0a50a495166177daa40299ffa7))
27
+
28
+ ## [9.23.0](https://github.com/dgreif/ring/compare/v9.22.1...v9.23.0) (2021-10-17)
29
+
30
+
31
+ ### Features
32
+
33
+ * **homebridge:** config option to hide camera lights ([58be17c](https://github.com/dgreif/ring/commit/58be17cfee524bfb158566e59d60438e9b6f0f5d)), closes [#802](https://github.com/dgreif/ring/issues/802)
34
+ * **homebridge:** option to treat "knock" as a ding ([7ece8b2](https://github.com/dgreif/ring/commit/7ece8b25314ef403a6c2bab6a28476a3b7229507)), closes [#514](https://github.com/dgreif/ring/issues/514)
35
+
36
+
37
+ ### Bug Fixes
38
+
39
+ * update dependencies ([b685e27](https://github.com/dgreif/ring/commit/b685e27e952a94635a277848821ebfa640d50ee2))
40
+
41
+ ### [9.22.1](https://github.com/dgreif/ring/compare/v9.22.0...v9.22.1) (2021-10-13)
42
+
43
+
44
+ ### Bug Fixes
45
+
46
+ * **homebridge:** prevent login attempt for null refresh tokens ([#791](https://github.com/dgreif/ring/issues/791)) ([bbc9db5](https://github.com/dgreif/ring/commit/bbc9db5578f9b3ed2af49256c8581fef5af8e134))
47
+
5
48
  ## [9.22.0](https://github.com/dgreif/ring/compare/v9.21.2...v9.22.0) (2021-10-02)
6
49
 
7
50
 
package/README.md CHANGED
@@ -45,6 +45,7 @@ Option | Default | Explanation
45
45
  `ffmpegPath` | Uses `ffmpeg-for-homebridge` | A custom path to the `ffmpeg` executable. By default, the static binaries built in [ffmpeg-for-homebridge](https://github.com/oznu/ffmpeg-for-homebridg) will be used. If you prefer to use your own version of ffmpeg, you can pass a complete path, or simply `"ffmpeg"` to use ffmpeg from your `PATH`.
46
46
  `controlCenterDisplayName` | 'ring-client-api' | This allows you to change the displayed name for the Authorized Device within Control Center in the Ring app
47
47
  `avoidSnapshotBatteryDrain` | false | Causes snapshots for battery cameras to be fetched at a minimum 10 minute interval to avoid draining the battery.
48
+ `treatKnockAsDing` | false | Will cause "knock" (`door_activity`) dings to be emitted from `onDoorbellPressed`
48
49
 
49
50
  ## Locations
50
51
  ```typescript
@@ -108,7 +109,7 @@ Camera also includes the following observables:
108
109
  * `onDoorbellPressed`: emits a ding every time the doorbell is pressed
109
110
  * `onMotionDetected`: `true` or `false` based on `onActiveDings` containing a motion ding
110
111
 
111
- Some other useful propeties
112
+ Some other useful properties
112
113
  * `id`
113
114
  * `name`: same as `description` from `data`
114
115
  * `hasLight`: does this camera have a light
package/lib/api/api.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface RingApiOptions extends SessionOptions {
9
9
  cameraDingsPollingSeconds?: number;
10
10
  locationModePollingSeconds?: number;
11
11
  avoidSnapshotBatteryDrain?: boolean;
12
+ treatKnockAsDing?: boolean;
12
13
  debug?: boolean;
13
14
  ffmpegPath?: string;
14
15
  externalPorts?: {
package/lib/api/api.js CHANGED
@@ -24,7 +24,8 @@ class RingApi extends subscribed_1.Subscribed {
24
24
  super();
25
25
  this.options = options;
26
26
  this.restClient = new rest_client_1.RingRestClient(this.options);
27
- this.onRefreshTokenUpdated = this.restClient.onRefreshTokenUpdated.asObservable();
27
+ this.onRefreshTokenUpdated =
28
+ this.restClient.onRefreshTokenUpdated.asObservable();
28
29
  if (options.debug) {
29
30
  (0, util_1.enableDebug)();
30
31
  }
@@ -128,7 +129,7 @@ class RingApi extends subscribed_1.Subscribed {
128
129
  return __awaiter(this, void 0, void 0, function* () {
129
130
  const rawLocations = yield this.fetchRawLocations(), { authorizedDoorbots, chimes, doorbots, allCameras, baseStations, beamBridges, } = yield this.fetchRingDevices(), locationIdsWithHubs = [...baseStations, ...beamBridges].map((x) => x.location_id), cameras = allCameras.map((data) => new ring_camera_1.RingCamera(data, doorbots.includes(data) ||
130
131
  authorizedDoorbots.includes(data) ||
131
- data.kind.startsWith('doorbell'), this.restClient, this.options.avoidSnapshotBatteryDrain || false)), ringChimes = chimes.map((data) => new ring_chime_1.RingChime(data, this.restClient)), locations = rawLocations
132
+ data.kind.startsWith('doorbell'), this.restClient, this.options.avoidSnapshotBatteryDrain || false, this.options.treatKnockAsDing || false)), ringChimes = chimes.map((data) => new ring_chime_1.RingChime(data, this.restClient)), locations = rawLocations
132
133
  .filter((location) => {
133
134
  return (!Array.isArray(this.options.locationIds) ||
134
135
  this.options.locationIds.includes(location.location_id));
@@ -169,6 +170,7 @@ class RingApi extends subscribed_1.Subscribed {
169
170
  .catch((e) => {
170
171
  (0, util_1.logError)(e);
171
172
  });
173
+ this.restClient.clearTimeouts();
172
174
  }
173
175
  }
174
176
  exports.RingApi = RingApi;
@@ -6,6 +6,7 @@ export declare function deviceApi(path: string): string;
6
6
  export declare function appApi(path: string): string;
7
7
  export interface ExtendedResponse {
8
8
  responseTimestamp: number;
9
+ timeMillis: number;
9
10
  }
10
11
  export interface EmailAuth {
11
12
  email: string;
@@ -24,6 +25,7 @@ export declare class RingRestClient {
24
25
  refreshToken: string | undefined;
25
26
  private hardwareIdPromise;
26
27
  private _authPromise;
28
+ private timeouts;
27
29
  private clearPreviousAuth;
28
30
  private get authPromise();
29
31
  private sessionPromise?;
@@ -44,4 +46,5 @@ export declare class RingRestClient {
44
46
  url: string;
45
47
  }): Promise<T & ExtendedResponse>;
46
48
  getCurrentAuth(): Promise<AuthTokenResponse>;
49
+ clearTimeouts(): void;
47
50
  }
@@ -44,8 +44,13 @@ function requestWithRetry(requestOptions) {
44
44
  return __awaiter(this, void 0, void 0, function* () {
45
45
  try {
46
46
  const options = Object.assign(Object.assign({}, defaultRequestOptions), requestOptions), { headers, body } = (yield (0, got_1.default)(options)), data = body;
47
- if (data !== null && typeof data === 'object' && headers.date) {
48
- data.responseTimestamp = new Date(headers.date).getTime();
47
+ if (data !== null && typeof data === 'object') {
48
+ if (headers.date) {
49
+ data.responseTimestamp = new Date(headers.date).getTime();
50
+ }
51
+ if (headers['x-time-millis']) {
52
+ data.timeMillis = Number(headers['x-time-millis']);
53
+ }
49
54
  }
50
55
  return data;
51
56
  }
@@ -66,12 +71,15 @@ function requestWithRetry(requestOptions) {
66
71
  class RingRestClient {
67
72
  constructor(authOptions) {
68
73
  this.authOptions = authOptions;
69
- // prettier-ignore
70
- this.refreshToken = ('refreshToken' in this.authOptions ? this.authOptions.refreshToken : undefined);
71
- this.hardwareIdPromise = (0, util_1.getHardwareId)(this.authOptions.systemId);
74
+ this.timeouts = [];
72
75
  this.sessionPromise = undefined;
73
76
  this.using2fa = false;
74
77
  this.onRefreshTokenUpdated = new rxjs_1.ReplaySubject(1);
78
+ this.refreshToken =
79
+ 'refreshToken' in this.authOptions
80
+ ? this.authOptions.refreshToken
81
+ : undefined;
82
+ this.hardwareIdPromise = (0, util_1.getHardwareId)(this.authOptions.systemId);
75
83
  }
76
84
  clearPreviousAuth() {
77
85
  this._authPromise = undefined;
@@ -83,11 +91,12 @@ class RingRestClient {
83
91
  authPromise
84
92
  .then(({ expires_in }) => {
85
93
  // clear the existing auth promise 1 minute before it expires
86
- setTimeout(() => {
94
+ const timeout = setTimeout(() => {
87
95
  if (this._authPromise === authPromise) {
88
96
  this.clearPreviousAuth();
89
97
  }
90
98
  }, ((expires_in || 3600) - 60) * 1000);
99
+ this.timeouts.push(timeout);
91
100
  })
92
101
  .catch(() => {
93
102
  // ignore these errors here, they should be handled by the function making a rest request
@@ -288,5 +297,8 @@ class RingRestClient {
288
297
  getCurrentAuth() {
289
298
  return this.authPromise;
290
299
  }
300
+ clearTimeouts() {
301
+ this.timeouts.forEach(clearTimeout);
302
+ }
291
303
  }
292
304
  exports.RingRestClient = RingRestClient;
@@ -16,13 +16,13 @@ export declare class RingCamera extends Subscribed {
16
16
  isDoorbot: boolean;
17
17
  private restClient;
18
18
  private avoidSnapshotBatteryDrain;
19
+ private treatKnockAsDing;
19
20
  id: number;
20
21
  deviceType: import("./ring-types").RingCameraKind;
21
22
  model: string;
22
23
  onData: BehaviorSubject<CameraData>;
23
24
  hasLight: boolean;
24
25
  hasSiren: boolean;
25
- hasBattery: boolean;
26
26
  onRequestUpdate: Subject<unknown>;
27
27
  onRequestActiveDings: Subject<unknown>;
28
28
  onNewDing: Subject<ActiveDing>;
@@ -32,13 +32,14 @@ export declare class RingCamera extends Subscribed {
32
32
  onMotionStarted: import("rxjs").Observable<null>;
33
33
  onBatteryLevel: import("rxjs").Observable<number | null>;
34
34
  onInHomeDoorbellStatus: import("rxjs").Observable<boolean>;
35
- constructor(initialData: CameraData, isDoorbot: boolean, restClient: RingRestClient, avoidSnapshotBatteryDrain: boolean);
35
+ constructor(initialData: CameraData, isDoorbot: boolean, restClient: RingRestClient, avoidSnapshotBatteryDrain: boolean, treatKnockAsDing: boolean);
36
36
  updateData(update: CameraData): void;
37
37
  requestUpdate(): void;
38
38
  get data(): CameraData;
39
39
  get name(): string;
40
40
  get activeDings(): ActiveDing[];
41
41
  get batteryLevel(): number | null;
42
+ get hasBattery(): boolean;
42
43
  get hasLowBattery(): boolean;
43
44
  get isCharging(): boolean;
44
45
  get operatingOnBattery(): boolean;
@@ -74,18 +75,21 @@ export declare class RingCamera extends Subscribed {
74
75
  }): Promise<string>;
75
76
  private isTimestampInLifeTime;
76
77
  get snapshotsAreBlocked(): boolean;
77
- private getSnapshotTimestamp;
78
- private refreshSnapshotInProgress?;
79
78
  get snapshotLifeTime(): number;
79
+ private lastSnapshotTimestamp;
80
80
  private lastSnapshotTimestampLocal;
81
81
  private lastSnapshotPromise?;
82
82
  get currentTimestampAge(): number;
83
- get currentTimestampExpiresIn(): number;
84
83
  get hasSnapshotWithinLifetime(): boolean;
85
84
  private checkIfSnapshotsAreBlocked;
86
- private requestSnapshotUpdate;
87
- private refreshSnapshot;
88
- getSnapshot(): Promise<Buffer | undefined>;
85
+ private shouldUseExistingSnapshotPromise;
86
+ private fetchingSnapshot;
87
+ getSnapshot(): Promise<Buffer>;
88
+ getNextSnapshot({ afterMs, maxWaitMs, force, }: {
89
+ afterMs?: number;
90
+ maxWaitMs?: number;
91
+ force?: boolean;
92
+ }): Promise<Buffer & import("./rest-client").ExtendedResponse>;
89
93
  getSipOptions(): Promise<SipOptions>;
90
94
  getUpdatedSipOptions(expiredDingId: string): Promise<SipOptions>;
91
95
  createSipSession(options?: {
@@ -18,8 +18,7 @@ const camera_utils_1 = require("@homebridge/camera-utils");
18
18
  const util_1 = require("./util");
19
19
  const sip_session_1 = require("./sip-session");
20
20
  const subscribed_1 = require("./subscribed");
21
- const snapshotRefreshDelay = 500, maxSnapshotRefreshSeconds = 35, // needs to be 30+ because battery cam can't take snapshot while recording
22
- maxSnapshotRefreshAttempts = (maxSnapshotRefreshSeconds * 1000) / snapshotRefreshDelay, fullDayMs = 24 * 60 * 60 * 1000;
21
+ const maxSnapshotRefreshSeconds = 15, fullDayMs = 24 * 60 * 60 * 1000;
23
22
  function parseBatteryLife(batteryLife) {
24
23
  if (batteryLife === null || batteryLife === undefined) {
25
24
  return null;
@@ -66,37 +65,36 @@ function getSearchQueryString(options) {
66
65
  }
67
66
  exports.getSearchQueryString = getSearchQueryString;
68
67
  class RingCamera extends subscribed_1.Subscribed {
69
- constructor(initialData, isDoorbot, restClient, avoidSnapshotBatteryDrain) {
68
+ constructor(initialData, isDoorbot, restClient, avoidSnapshotBatteryDrain, treatKnockAsDing) {
70
69
  super();
71
70
  this.initialData = initialData;
72
71
  this.isDoorbot = isDoorbot;
73
72
  this.restClient = restClient;
74
73
  this.avoidSnapshotBatteryDrain = avoidSnapshotBatteryDrain;
75
- this.id = this.initialData.id;
76
- this.deviceType = this.initialData.kind;
77
- this.model = ring_types_1.RingCameraModel[this.initialData.kind] || 'Unknown Model';
78
- this.onData = new rxjs_1.BehaviorSubject(this.initialData);
79
- this.hasLight = this.initialData.led_status !== undefined;
80
- this.hasSiren = this.initialData.siren_status !== undefined;
81
- this.hasBattery = (0, ring_types_1.isBatteryCameraKind)(this.deviceType) ||
82
- (typeof this.initialData.battery_life === 'string' &&
83
- this.batteryLevel !== null &&
84
- this.batteryLevel < 100 &&
85
- this.batteryLevel >= 0);
74
+ this.treatKnockAsDing = treatKnockAsDing;
86
75
  this.onRequestUpdate = new rxjs_1.Subject();
87
76
  this.onRequestActiveDings = new rxjs_1.Subject();
88
77
  this.onNewDing = new rxjs_1.Subject();
89
78
  this.onActiveDings = new rxjs_1.BehaviorSubject([]);
90
- this.onDoorbellPressed = this.onNewDing.pipe((0, operators_1.filter)((ding) => ding.kind === 'ding'), (0, operators_1.share)());
79
+ this.onDoorbellPressed = this.onNewDing.pipe((0, operators_1.filter)((ding) => ding.kind === 'ding' ||
80
+ (this.treatKnockAsDing && ding.kind === 'door_activity')), (0, operators_1.share)());
91
81
  this.onMotionDetected = this.onActiveDings.pipe((0, operators_1.map)((dings) => dings.some((ding) => ding.motion || ding.kind === 'motion')), (0, operators_1.distinctUntilChanged)(), (0, operators_1.publishReplay)(1), (0, operators_1.refCount)());
92
82
  this.onMotionStarted = this.onMotionDetected.pipe((0, operators_1.filter)((currentlyDetected) => currentlyDetected), (0, operators_1.mapTo)(null), // no value needed, event is what matters
93
83
  (0, operators_1.share)());
84
+ this.expiredDingIds = [];
85
+ this.lastSnapshotTimestamp = 0;
86
+ this.lastSnapshotTimestampLocal = 0;
87
+ this.fetchingSnapshot = false;
88
+ this.id = this.initialData.id;
89
+ this.deviceType = this.initialData.kind;
90
+ this.model = ring_types_1.RingCameraModel[this.initialData.kind] || 'Unknown Model';
91
+ this.onData = new rxjs_1.BehaviorSubject(this.initialData);
92
+ this.hasLight = this.initialData.led_status !== undefined;
93
+ this.hasSiren = this.initialData.siren_status !== undefined;
94
94
  this.onBatteryLevel = this.onData.pipe((0, operators_1.map)(getBatteryLevel), (0, operators_1.distinctUntilChanged)());
95
95
  this.onInHomeDoorbellStatus = this.onData.pipe((0, operators_1.map)(({ settings: { chime_settings } }) => {
96
96
  return Boolean(chime_settings === null || chime_settings === void 0 ? void 0 : chime_settings.enable);
97
97
  }), (0, operators_1.distinctUntilChanged)());
98
- this.expiredDingIds = [];
99
- this.lastSnapshotTimestampLocal = 0;
100
98
  if (!initialData.subscribed) {
101
99
  this.subscribeToDingEvents().catch((e) => {
102
100
  (0, util_1.logError)('Failed to subscribe ' + initialData.description + ' to ding events');
@@ -128,6 +126,15 @@ class RingCamera extends subscribed_1.Subscribed {
128
126
  get batteryLevel() {
129
127
  return getBatteryLevel(this.data);
130
128
  }
129
+ get hasBattery() {
130
+ if (this.batteryLevel === null) {
131
+ return false;
132
+ }
133
+ return ((0, ring_types_1.isBatteryCameraKind)(this.deviceType) ||
134
+ (typeof this.initialData.battery_life === 'string' &&
135
+ this.batteryLevel < 100 &&
136
+ this.batteryLevel >= 0));
137
+ }
131
138
  get hasLowBattery() {
132
139
  return this.data.alerts.battery === 'low';
133
140
  }
@@ -309,34 +316,14 @@ class RingCamera extends subscribed_1.Subscribed {
309
316
  get snapshotsAreBlocked() {
310
317
  return this.data.settings.motion_detection_enabled === false;
311
318
  }
312
- getSnapshotTimestamp() {
313
- return __awaiter(this, void 0, void 0, function* () {
314
- const { timestamps, responseTimestamp } = yield this.restClient.request({
315
- url: (0, rest_client_1.clientApi)('snapshots/timestamps'),
316
- method: 'POST',
317
- json: {
318
- doorbot_ids: [this.id],
319
- },
320
- }), deviceTimestamp = timestamps[0], timestamp = deviceTimestamp ? deviceTimestamp.timestamp : 0, timestampAge = Math.abs(responseTimestamp - timestamp);
321
- this.lastSnapshotTimestampLocal = timestamp ? Date.now() - timestampAge : 0;
322
- return {
323
- timestamp,
324
- inLifeTime: this.isTimestampInLifeTime(timestampAge),
325
- };
326
- });
327
- }
328
319
  get snapshotLifeTime() {
329
320
  return this.avoidSnapshotBatteryDrain && this.operatingOnBattery
330
321
  ? 600 * 1000 // battery cams only refresh timestamp every 10 minutes
331
- : 10 * 1000; // snapshot updates will be forced. Limit to 10 lifetime
322
+ : 10 * 1000; // snapshot updates will be forced. Limit to 10s lifetime
332
323
  }
333
324
  get currentTimestampAge() {
334
325
  return Date.now() - this.lastSnapshotTimestampLocal;
335
326
  }
336
- get currentTimestampExpiresIn() {
337
- // Gets 0 if stale snapshot is used because snapshot timestamp refused to update (recording in progress on battery cam)
338
- return Math.max(this.lastSnapshotTimestampLocal - Date.now() + this.snapshotLifeTime, 0);
339
- }
340
327
  get hasSnapshotWithinLifetime() {
341
328
  return this.isTimestampInLifeTime(this.currentTimestampAge);
342
329
  }
@@ -344,79 +331,73 @@ class RingCamera extends subscribed_1.Subscribed {
344
331
  if (this.snapshotsAreBlocked) {
345
332
  throw new Error(`Motion detection is disabled for ${this.name}, which prevents snapshots from this camera. This can be caused by Modes settings or by turning off the Record Motion setting.`);
346
333
  }
334
+ if (this.isOffline) {
335
+ throw new Error(`Cannot fetch snapshot for ${this.name} because it is offline`);
336
+ }
347
337
  }
348
- requestSnapshotUpdate() {
349
- return this.restClient.request({
350
- method: 'PUT',
351
- url: (0, rest_client_1.clientApi)('snapshots/update_all'),
352
- json: {
353
- doorbot_ids: [this.id],
354
- refresh: true,
355
- },
356
- });
357
- }
358
- refreshSnapshot() {
359
- return __awaiter(this, void 0, void 0, function* () {
360
- if (this.hasSnapshotWithinLifetime) {
361
- (0, util_1.logDebug)(`Snapshot for ${this.name} is still within its life time (${this.currentTimestampAge / 1000}s old)`);
362
- return true;
363
- }
364
- this.checkIfSnapshotsAreBlocked();
365
- if (!this.avoidSnapshotBatteryDrain || !this.operatingOnBattery) {
366
- // tell the camera to update snapshot immediately.
367
- // avoidSnapshotBatteryDrain is best if you have a battery cam that you request snapshots for frequently. This can lead to battery drain if snapshot updates are forced.
368
- yield this.requestSnapshotUpdate();
369
- }
370
- for (let i = 0; i < maxSnapshotRefreshAttempts; i++) {
371
- this.checkIfSnapshotsAreBlocked();
372
- const { timestamp, inLifeTime } = yield this.getSnapshotTimestamp();
373
- if (!timestamp && this.isOffline) {
374
- throw new Error(`No snapshot available and device ${this.name} is offline`);
375
- }
376
- if (inLifeTime) {
377
- return false;
378
- }
379
- yield (0, util_1.delay)(snapshotRefreshDelay);
380
- }
381
- const extraMessageForBatteryCam = this.operatingOnBattery
382
- ? '. This is normal behavior since this camera is unable to capture snapshots while streaming'
383
- : '';
384
- throw new Error(`Snapshot for ${this.name} (${this.deviceType} - ${this.model}) failed to refresh after ${maxSnapshotRefreshAttempts} attempts${extraMessageForBatteryCam}`);
385
- });
338
+ shouldUseExistingSnapshotPromise() {
339
+ if (this.fetchingSnapshot) {
340
+ return true;
341
+ }
342
+ if (this.hasSnapshotWithinLifetime) {
343
+ (0, util_1.logDebug)(`Snapshot for ${this.name} is still within its life time (${this.currentTimestampAge / 1000}s old)`);
344
+ return true;
345
+ }
346
+ if (!this.avoidSnapshotBatteryDrain || !this.operatingOnBattery) {
347
+ // tell the camera to update snapshot immediately.
348
+ // avoidSnapshotBatteryDrain is best if you have a battery cam that you request snapshots for frequently. This can lead to battery drain if snapshot updates are forced.
349
+ return false;
350
+ }
386
351
  }
387
352
  getSnapshot() {
388
353
  return __awaiter(this, void 0, void 0, function* () {
389
- this.refreshSnapshotInProgress =
390
- this.refreshSnapshotInProgress ||
391
- this.refreshSnapshot().catch((e) => {
392
- (0, util_1.logError)(e.message);
393
- throw e;
394
- });
395
- try {
396
- const useLastSnapshot = yield this.refreshSnapshotInProgress;
397
- this.refreshSnapshotInProgress = undefined;
398
- if (useLastSnapshot && this.lastSnapshotPromise) {
399
- return this.lastSnapshotPromise;
400
- }
401
- }
402
- catch (e) {
403
- this.refreshSnapshotInProgress = undefined;
404
- throw e;
354
+ if (this.lastSnapshotPromise && this.shouldUseExistingSnapshotPromise()) {
355
+ return this.lastSnapshotPromise;
405
356
  }
406
- this.lastSnapshotPromise = this.restClient.request({
407
- url: (0, rest_client_1.clientApi)(`snapshots/image/${this.id}`),
408
- responseType: 'buffer',
409
- });
357
+ this.checkIfSnapshotsAreBlocked();
358
+ this.lastSnapshotPromise = Promise.race([
359
+ this.getNextSnapshot({
360
+ afterMs: this.lastSnapshotTimestamp,
361
+ force: true,
362
+ }),
363
+ (0, util_1.delay)(maxSnapshotRefreshSeconds * 1000).then(() => {
364
+ const extraMessageForBatteryCam = this.operatingOnBattery
365
+ ? '. This is normal behavior since this camera is unable to capture snapshots while streaming'
366
+ : '';
367
+ throw new Error(`Snapshot for ${this.name} (${this.deviceType} - ${this.model}) failed to refresh after ${maxSnapshotRefreshSeconds} seconds${extraMessageForBatteryCam}`);
368
+ }),
369
+ ]);
410
370
  try {
411
371
  yield this.lastSnapshotPromise;
412
372
  }
413
- catch (_) {
373
+ catch (e) {
414
374
  // snapshot request failed, don't use it again
415
375
  this.lastSnapshotPromise = undefined;
376
+ throw e;
416
377
  }
378
+ this.fetchingSnapshot = false;
417
379
  return this.lastSnapshotPromise;
418
380
  });
419
381
  }
382
+ getNextSnapshot({ afterMs, maxWaitMs, force, }) {
383
+ return __awaiter(this, void 0, void 0, function* () {
384
+ const response = yield this.restClient.request({
385
+ url: `https://app-snaps.ring.com/snapshots/next/${this.id}?extras=force`,
386
+ responseType: 'buffer',
387
+ searchParams: {
388
+ 'after-ms': afterMs,
389
+ 'max-wait-ms': maxWaitMs,
390
+ extras: force ? 'force' : undefined,
391
+ },
392
+ headers: {
393
+ accept: 'image/jpeg',
394
+ },
395
+ }), { responseTimestamp, timeMillis } = response, timestampAge = Math.abs(responseTimestamp - timeMillis);
396
+ this.lastSnapshotTimestamp = timeMillis;
397
+ this.lastSnapshotTimestampLocal = Date.now() - timestampAge;
398
+ return response;
399
+ });
400
+ }
420
401
  getSipOptions() {
421
402
  return __awaiter(this, void 0, void 0, function* () {
422
403
  const activeDings = this.onActiveDings.getValue(), existingDing = activeDings
@@ -5,7 +5,7 @@ export declare class RingChime {
5
5
  private initialData;
6
6
  private restClient;
7
7
  id: number;
8
- deviceType: "chime" | "chime_pro" | "chime_v2" | "chime_pro_v2";
8
+ deviceType: import("./ring-types").ChimeKind;
9
9
  model: string;
10
10
  onData: BehaviorSubject<ChimeData>;
11
11
  onRequestUpdate: Subject<unknown>;
@@ -23,11 +23,11 @@ class RingChime {
23
23
  constructor(initialData, restClient) {
24
24
  this.initialData = initialData;
25
25
  this.restClient = restClient;
26
+ this.onRequestUpdate = new rxjs_1.Subject();
26
27
  this.id = this.initialData.id;
27
28
  this.deviceType = this.initialData.kind;
28
29
  this.model = ring_types_1.ChimeModel[this.deviceType] || 'Chime';
29
30
  this.onData = new rxjs_1.BehaviorSubject(this.initialData);
30
- this.onRequestUpdate = new rxjs_1.Subject();
31
31
  }
32
32
  updateData(update) {
33
33
  this.onData.next(update);
@@ -10,6 +10,7 @@ export declare enum RingDeviceType {
10
10
  TemperatureSensor = "sensor.temperature",
11
11
  WaterSensor = "sensor.water",
12
12
  TiltSensor = "sensor.tilt",
13
+ GlassbreakSensor = "sensor.glassbreak",
13
14
  RangeExtender = "range-extender.zwave",
14
15
  ZigbeeAdapter = "adapter.zigbee",
15
16
  AccessCodeVault = "access-code.vault",
@@ -67,6 +68,7 @@ export declare enum RingCameraKind {
67
68
  doorbell_portal = "doorbell_portal",
68
69
  doorbell_scallop = "doorbell_scallop",
69
70
  doorbell_scallop_lite = "doorbell_scallop_lite",
71
+ doorbell_graham_cracker = "doorbell_graham_cracker",
70
72
  lpd_v1 = "lpd_v1",
71
73
  lpd_v2 = "lpd_v2",
72
74
  jbox_v1 = "jbox_v1",
@@ -214,7 +216,7 @@ export interface BeamBridge {
214
216
  role: string;
215
217
  updated_at: string;
216
218
  }
217
- declare type ChimeKind = 'chime' | 'chime_pro' | 'chime_v2' | 'chime_pro_v2';
219
+ export declare type ChimeKind = 'chime' | 'chime_pro' | 'chime_v2' | 'chime_pro_v2';
218
220
  export declare const ChimeModel: {
219
221
  readonly [P in ChimeKind]: string;
220
222
  };
@@ -686,10 +688,6 @@ export interface ActiveDing {
686
688
  sip_token: string;
687
689
  sip_ding_id: string;
688
690
  }
689
- export interface SnapshotTimestamp {
690
- timestamp: number;
691
- doorbot_id: number;
692
- }
693
691
  export interface AuthTokenResponse {
694
692
  access_token: string;
695
693
  expires_in: number;
@@ -14,6 +14,7 @@ var RingDeviceType;
14
14
  RingDeviceType["TemperatureSensor"] = "sensor.temperature";
15
15
  RingDeviceType["WaterSensor"] = "sensor.water";
16
16
  RingDeviceType["TiltSensor"] = "sensor.tilt";
17
+ RingDeviceType["GlassbreakSensor"] = "sensor.glassbreak";
17
18
  RingDeviceType["RangeExtender"] = "range-extender.zwave";
18
19
  RingDeviceType["ZigbeeAdapter"] = "adapter.zigbee";
19
20
  RingDeviceType["AccessCodeVault"] = "access-code.vault";
@@ -75,6 +76,7 @@ var RingCameraKind;
75
76
  RingCameraKind["doorbell_portal"] = "doorbell_portal";
76
77
  RingCameraKind["doorbell_scallop"] = "doorbell_scallop";
77
78
  RingCameraKind["doorbell_scallop_lite"] = "doorbell_scallop_lite";
79
+ RingCameraKind["doorbell_graham_cracker"] = "doorbell_graham_cracker";
78
80
  RingCameraKind["lpd_v1"] = "lpd_v1";
79
81
  RingCameraKind["lpd_v2"] = "lpd_v2";
80
82
  RingCameraKind["jbox_v1"] = "jbox_v1";
@@ -93,7 +95,7 @@ var RingCameraKind;
93
95
  RingCameraKind["stickup_cam_mini"] = "stickup_cam_mini";
94
96
  })(RingCameraKind = exports.RingCameraKind || (exports.RingCameraKind = {}));
95
97
  // RegExp taken from ring.com app
96
- const isWiredCameraRegExp = new RegExp(/(^(lpd|jbox|stickup_cam_elite|stickup_cam_mini|hp_cam|spotlightw|floodlight))/);
98
+ const isWiredCameraRegExp = new RegExp(/(^(lpd|jbox|stickup_cam_elite|stickup_cam_mini|hp_cam|spotlightw|floodlight|doorbell_graham_cracker))/);
97
99
  function isBatteryCameraKind(kind) {
98
100
  return !kind.match(isWiredCameraRegExp);
99
101
  }
@@ -107,6 +109,7 @@ exports.RingCameraModel = {
107
109
  doorbell_portal: 'Door View Cam',
108
110
  doorbell_scallop: 'Doorbell 3 Plus',
109
111
  doorbell_scallop_lite: 'Doorbell 3',
112
+ doorbell_graham_cracker: 'Doorbell Wired',
110
113
  lpd_v1: 'Doorbell Pro',
111
114
  lpd_v2: 'Doorbell Pro',
112
115
  jbox_v1: 'Doorbell Elite',
@@ -27,6 +27,10 @@ export declare class SipSession extends Subscribed {
27
27
  constructor(sipOptions: SipOptions, rtpOptions: RtpOptions, audioSplitter: RtpSplitter, audioRtcpSplitter: RtpSplitter, videoSplitter: RtpSplitter, videoRtcpSplitter: RtpSplitter, tlsPort: number, camera: RingCamera);
28
28
  createSipCall(sipOptions: SipOptions): SipCall;
29
29
  start(ffmpegOptions?: FfmpegOptions): Promise<RtpDescription>;
30
+ prepareTranscoder(transcodeVideoStream: boolean, ffmpegInputOptions: SpawnInput[] | undefined, remoteRtpOptions: RtpOptions, audioPort: number, videoPort: number, sdpInput: string): {
31
+ ffmpegInputArguments: SpawnInput[];
32
+ inputSdpLines: string[];
33
+ };
30
34
  private startTranscoder;
31
35
  reservePort(bufferPorts?: number): Promise<number>;
32
36
  requestKeyFrame(): Promise<void>;
@@ -32,8 +32,8 @@ class SipSession extends subscribed_1.Subscribed {
32
32
  this.hasStarted = false;
33
33
  this.hasCallEnded = false;
34
34
  this.onCallEndedSubject = new rxjs_1.ReplaySubject(1);
35
- this.sipCall = this.createSipCall(this.sipOptions);
36
35
  this.onCallEnded = this.onCallEndedSubject.asObservable();
36
+ this.sipCall = this.createSipCall(this.sipOptions);
37
37
  }
38
38
  createSipCall(sipOptions) {
39
39
  if (this.sipCall) {
@@ -114,31 +114,17 @@ class SipSession extends subscribed_1.Subscribed {
114
114
  }
115
115
  });
116
116
  }
117
- startTranscoder(ffmpegOptions, remoteRtpOptions, audioPort, videoPort) {
118
- const transcodeVideoStream = ffmpegOptions.video !== false, ffmpegArgs = [
117
+ prepareTranscoder(transcodeVideoStream, ffmpegInputOptions, remoteRtpOptions, audioPort, videoPort, sdpInput) {
118
+ const ffmpegInputArguments = [
119
119
  '-hide_banner',
120
120
  '-protocol_whitelist',
121
121
  'pipe,udp,rtp,file,crypto',
122
122
  '-f',
123
123
  'sdp',
124
- ...(ffmpegOptions.input || []),
124
+ ...(ffmpegInputOptions || []),
125
125
  '-i',
126
- 'pipe:',
127
- ...(ffmpegOptions.audio || ['-acodec', 'aac']),
128
- ...(transcodeVideoStream
129
- ? ffmpegOptions.video || ['-vcodec', 'copy']
130
- : []),
131
- ...(ffmpegOptions.output || []),
132
- ], ff = new camera_utils_1.FfmpegProcess({
133
- ffmpegArgs,
134
- ffmpegPath: (0, ffmpeg_1.getFfmpegPath)(),
135
- exitCallback: () => this.callEnded(true),
136
- logLabel: `From Ring (${this.camera.name})`,
137
- logger: {
138
- error: util_1.logError,
139
- info: util_1.logDebug,
140
- },
141
- }), inputSdpLines = [
126
+ sdpInput,
127
+ ], inputSdpLines = [
142
128
  'v=0',
143
129
  'o=105202070 3747 461 IN IP4 127.0.0.1',
144
130
  's=Talk',
@@ -167,8 +153,6 @@ class SipSession extends subscribed_1.Subscribed {
167
153
  };
168
154
  });
169
155
  }
170
- this.onCallEnded.subscribe(() => ff.stop());
171
- ff.writeStdin(inputSdpLines.filter((x) => Boolean(x)).join('\n'));
172
156
  this.audioSplitter.addMessageHandler(({ isRtpMessage, message }) => {
173
157
  if ((0, rtp_utils_1.isStunMessage)(message)) {
174
158
  return null;
@@ -177,6 +161,26 @@ class SipSession extends subscribed_1.Subscribed {
177
161
  port: isRtpMessage ? audioPort : audioPort + 1,
178
162
  };
179
163
  });
164
+ return {
165
+ ffmpegInputArguments,
166
+ inputSdpLines,
167
+ };
168
+ }
169
+ startTranscoder(ffmpegOptions, remoteRtpOptions, audioPort, videoPort) {
170
+ const transcodeVideoStream = ffmpegOptions.video !== false, { ffmpegInputArguments, inputSdpLines } = this.prepareTranscoder(transcodeVideoStream, ffmpegOptions.input, remoteRtpOptions, audioPort, videoPort, 'pipe:'), ff = new camera_utils_1.FfmpegProcess({
171
+ ffmpegArgs: ffmpegInputArguments.concat(...(ffmpegOptions.audio || ['-acodec', 'aac']), ...(transcodeVideoStream
172
+ ? ffmpegOptions.video || ['-vcodec', 'copy']
173
+ : []), ...(ffmpegOptions.output || [])),
174
+ ffmpegPath: (0, ffmpeg_1.getFfmpegPath)(),
175
+ exitCallback: () => this.callEnded(true),
176
+ logLabel: `From Ring (${this.camera.name})`,
177
+ logger: {
178
+ error: util_1.logError,
179
+ info: util_1.logDebug,
180
+ },
181
+ });
182
+ this.onCallEnded.subscribe(() => ff.stop());
183
+ ff.writeStdin(inputSdpLines.filter((x) => Boolean(x)).join('\n'));
180
184
  }
181
185
  reservePort(bufferPorts = 0) {
182
186
  return __awaiter(this, void 0, void 0, function* () {
package/lib/api/sip.d.ts CHANGED
@@ -19,6 +19,7 @@ export function create(options: any, callback: any): {
19
19
  isFlowUri: (uri: any) => boolean;
20
20
  hostname: () => any;
21
21
  destroy: () => void;
22
+ makeResponse: typeof makeResponse;
22
23
  };
23
24
  export function start(options: any, callback: any): void;
24
25
  export function parseAOR(data: any): any;
package/lib/api/sip.js CHANGED
@@ -1300,6 +1300,7 @@ exports.create = function (options, callback) {
1300
1300
  transaction.destroy();
1301
1301
  transport.destroy();
1302
1302
  },
1303
+ makeResponse,
1303
1304
  };
1304
1305
  };
1305
1306
  exports.start = function (options, callback) {
package/lib/api/util.js CHANGED
@@ -8,14 +8,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
11
14
  Object.defineProperty(exports, "__esModule", { value: true });
12
15
  exports.randomString = exports.randomInteger = exports.mapAsync = exports.stringify = exports.requestInput = exports.getHardwareId = exports.generateUuid = exports.enableDebug = exports.useLogger = exports.logError = exports.logInfo = exports.logDebug = exports.delay = void 0;
13
- const debug = require("debug");
16
+ const debug_1 = __importDefault(require("debug"));
14
17
  const colors_1 = require("colors");
15
18
  const readline_1 = require("readline");
16
19
  const uuid_1 = require("uuid");
17
20
  const systeminformation_1 = require("systeminformation");
18
- const debugLogger = debug('ring'), uuidNamespace = 'e53ffdc0-e91d-4ce1-bec2-df939d94739c';
21
+ const debugLogger = (0, debug_1.default)('ring'), uuidNamespace = 'e53ffdc0-e91d-4ce1-bec2-df939d94739c';
19
22
  let logger = {
20
23
  logInfo(message) {
21
24
  debugLogger(message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ring-client-api",
3
- "version": "9.22.0",
3
+ "version": "9.25.0-beta.0",
4
4
  "description": "Unofficial API for Ring doorbells, cameras, security alarm system and smart lighting",
5
5
  "main": "lib/api/index.js",
6
6
  "bin": {
@@ -57,38 +57,38 @@
57
57
  "@homebridge/plugin-ui-utils": "0.0.19",
58
58
  "@types/socket.io-client": "1.4.36",
59
59
  "colors": "1.4.0",
60
- "debug": "4.3.2",
61
- "got": "11.8.2",
62
- "rxjs": "7.3.1",
60
+ "debug": "4.3.3",
61
+ "got": "11.8.3",
62
+ "rxjs": "7.5.2",
63
63
  "sdp": "3.0.3",
64
64
  "socket.io-client": "2.4.0",
65
65
  "stun": "2.1.0",
66
- "systeminformation": "5.9.4",
66
+ "systeminformation": "5.10.7",
67
67
  "uuid": "8.3.2"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@types/debug": "4.1.7",
71
- "@types/jest": "27.0.2",
72
- "@types/node": "16.10.2",
73
- "@types/uuid": "8.3.1",
74
- "@typescript-eslint/eslint-plugin": "4.32.0",
75
- "@typescript-eslint/parser": "4.32.0",
76
- "concurrently": "^6.2.2",
71
+ "@types/jest": "27.4.0",
72
+ "@types/node": "17.0.10",
73
+ "@types/uuid": "8.3.4",
74
+ "@typescript-eslint/eslint-plugin": "5.10.0",
75
+ "@typescript-eslint/parser": "5.10.0",
76
+ "concurrently": "^7.0.0",
77
77
  "conventional-github-releaser": "3.1.5",
78
- "dotenv": "10.0.0",
79
- "eslint": "7.32.0",
78
+ "dotenv": "14.2.0",
79
+ "eslint": "8.7.0",
80
80
  "eslint-config-prettier": "8.3.0",
81
- "eslint-plugin-jest": "24.5.0",
81
+ "eslint-plugin-jest": "25.7.0",
82
82
  "eslint-plugin-prettier": "4.0.0",
83
- "express": "4.17.1",
84
- "homebridge": "1.3.4",
85
- "jest": "27.2.4",
86
- "prettier": "2.4.1",
83
+ "express": "4.17.2",
84
+ "homebridge": "1.4.0",
85
+ "jest": "27.4.7",
86
+ "prettier": "2.5.1",
87
87
  "reflect-metadata": "0.1.13",
88
88
  "rimraf": "3.0.2",
89
- "standard-version": "9.3.1",
90
- "ts-jest": "27.0.5",
91
- "typescript": "4.4.3"
89
+ "standard-version": "9.3.2",
90
+ "ts-jest": "27.1.3",
91
+ "typescript": "4.5.5"
92
92
  },
93
93
  "engines": {
94
94
  "node": "^10 || ^12.20 || ^14 || ^16",