homebridge-ring 10.0.0-beta.3 → 10.0.0-beta.4

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,29 @@
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
+ ## [10.0.0-beta.4](https://github.com/dgreif/ring/compare/v10.0.0-beta.3...v10.0.0-beta.4) (2022-04-03)
6
+
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ * **api:** All ding-related functionality will now use the `PushNotification` type, and the `ActiveDing` type has been removed
11
+ * **api:** The `cameraDingsPollingSeconds` option has been removed. Ding events will now be received via push notifications (no setup required)
12
+ * **api:** The `treatKnockAsDing` option has been removed. Knocks should now register as "dings" automatically, though I have not been able to test this functionality
13
+
14
+ ### Features
15
+
16
+ * **api:** convert ding handling to use notifications ([5b62318](https://github.com/dgreif/ring/commit/5b62318b6a71bcc224a2b29cbd6e21162fc9891e))
17
+ * **api:** update notification types ([15d3e7e](https://github.com/dgreif/ring/commit/15d3e7e40744e2c3ed48140fa94fbc0c6f9d4088))
18
+ * **homebridge:** faster snapshots from camera notifications ([7fc821b](https://github.com/dgreif/ring/commit/7fc821b71013c36314d6d428b8152e631ea4b497))
19
+ * receive dings from push notifications ([c9f130f](https://github.com/dgreif/ring/commit/c9f130f0f1f21b6db2454d611641ef7c29c7e620))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * more closely match ring edge live streaming setup flow ([4bb3189](https://github.com/dgreif/ring/commit/4bb3189d6ce6614f7af64d04b410c3922957d555))
25
+ * update uuid namespace to force new client sessions ([bc3e943](https://github.com/dgreif/ring/commit/bc3e9432c41dda085845f72f332dcf480c92475d))
26
+ * use camera health to determine battery and charging status ([fac7033](https://github.com/dgreif/ring/commit/fac70331bdd4dd94bc6caa143eb1bcb9ead06923)), closes [#873](https://github.com/dgreif/ring/issues/873)
27
+
5
28
  ## [10.0.0-beta.3](https://github.com/dgreif/ring/compare/v10.0.0-beta.2...v10.0.0-beta.3) (2022-03-26)
6
29
 
7
30
 
package/README.md CHANGED
@@ -68,7 +68,6 @@ Only include an optional parameter if you actually need it. Default behavior wi
68
68
  "avoidSnapshotBatteryDrain": true,
69
69
  "treatKnockAsDing": true,
70
70
  "cameraStatusPollingSeconds": 20,
71
- "cameraDingsPollingSeconds": 2,
72
71
  "locationModePollingSeconds": 20,
73
72
  "locationIds": ["488e4800-fcde-4493-969b-d1a06f683102", "4bbed7a7-06df-4f18-b3af-291c89854d60"]
74
73
  }
@@ -92,9 +91,7 @@ Option | Default | Explanation
92
91
  `showPanicButtons` | `false` | Creates a new `Panic Buttons` device in HomeKit with `Burglar Alarm` and `Fire Alarm` switches. **Use these at your own risk. I do not guarantee functionality in case of emergency, nor do I take responsibility for any false alarms**. These function just like the SOS sliders in the Ring app.
93
92
  `nightModeBypassFor` | no Night Mode | Allows you to use Night mode to "Bypass and Arm" a Ring Alarm. Can be set to `all` for Away or `some` for Home. If set and Night mode is activated from HomeKit, any open contact sensors will automatically be bypassed.
94
93
  `avoidSnapshotBatteryDrain` | false | Causes snapshots for battery cameras to be fetched at a minimum 10 minute interval to avoid draining the battery.
95
- `treatKnockAsDing` | false | When someone knocks on your door, it will be recognized as a doorbell press in HomeKit. Limited to select camera models (Peephole Cam).
96
94
  `cameraStatusPollingSeconds` | `20` | How frequently to poll for updates to your cameras and chimes (in seconds). Information like light/siren/volume/snooze status do not update in real time and need to be requested periodically.
97
- `cameraDingsPollingSeconds` | `2` | How frequently to poll for new events from your cameras. These include motion and doorbell presses.
98
95
  `locationModePollingSeconds` | `20` | How frequently to poll for location mode updates (in seconds). This is only useful if you are using location modes to control camera settings and want to keep an up-to-date reference of the current mode for each location. Polling is automatically disabled for locations equipped with a Ring Alarm. Will hide Location Mode switch if set to `0`
99
96
  `locationIds` | All Locations | Use this option if you only want a subset of your locations to appear in HomeKit. If this option is not included, all of your locations will be added to HomeKit (which is what most users will want to do).
100
97
  `onlyDeviceTypes` | All Device Types | A subset of device types to be shown. If set, all other device types will be hidden in HomeKit.
@@ -86,11 +86,6 @@
86
86
  "type": "boolean",
87
87
  "description": "Causes snapshots for battery cameras to be fetched at a minimum 10 minute interval to avoid draining the battery. This is useful if have the Home app open constantly, causing frequent snapshot requests."
88
88
  },
89
- "treatKnockAsDing": {
90
- "title": "Treat Knock as Ding",
91
- "type": "boolean",
92
- "description": "When someone knocks on your door, it will be recognized as a doorbell press in HomeKit. Limited to select camera models (Peephole Cam)"
93
- },
94
89
  "sendCameraMotionNotificationsToTv": {
95
90
  "title": "Create a 'Doorbell' accessory triggered by non-doorbell camera motion alerts for tvOS 14 notification support",
96
91
  "type": "boolean"
@@ -118,13 +113,6 @@
118
113
  "placeholder": "Get camera status every __ seconds",
119
114
  "description": "Camera/Chime status (such as light/siren/volume/snooze state) does not update live and must be fetched periodically. By default this is done every 20 seconds"
120
115
  },
121
- "cameraDingsPollingSeconds": {
122
- "title": "Camera Event Polling",
123
- "type": "integer",
124
- "minimum": 1,
125
- "placeholder": "Get camera events every __ seconds",
126
- "description": "Camera motion and ding events does not update live and must be fetched periodically. By default this is done every 2 seconds"
127
- },
128
116
  "locationModePollingSeconds": {
129
117
  "title": "Location Mode Polling",
130
118
  "type": "integer",
@@ -179,13 +167,11 @@
179
167
  "showPanicButtons",
180
168
  "nightModeBypassFor",
181
169
  "avoidSnapshotBatteryDrain",
182
- "treatKnockAsDing",
183
170
  "sendCameraMotionNotificationsToTv",
184
171
  "sendDoorbellMotionNotificationsToTv",
185
172
  "beamDurationSeconds",
186
173
  "debug",
187
174
  "cameraStatusPollingSeconds",
188
- "cameraDingsPollingSeconds",
189
175
  "locationModePollingSeconds",
190
176
  {
191
177
  "key": "hideDeviceIds",
package/lib/api/api.d.ts CHANGED
@@ -1,15 +1,13 @@
1
1
  import { RefreshTokenAuth, RingRestClient, SessionOptions } from './rest-client';
2
2
  import { Location } from './location';
3
- import { ActiveDing, BaseStation, BeamBridge, CameraData, ChimeData, ProfileResponse, UserLocation } from './ring-types';
3
+ import { BaseStation, BeamBridge, CameraData, ChimeData, ProfileResponse, UserLocation } from './ring-types';
4
4
  import { RingCamera } from './ring-camera';
5
5
  import { Subscribed } from './subscribed';
6
6
  export interface RingApiOptions extends SessionOptions {
7
7
  locationIds?: string[];
8
8
  cameraStatusPollingSeconds?: number;
9
- cameraDingsPollingSeconds?: number;
10
9
  locationModePollingSeconds?: number;
11
10
  avoidSnapshotBatteryDrain?: boolean;
12
- treatKnockAsDing?: boolean;
13
11
  debug?: boolean;
14
12
  ffmpegPath?: string;
15
13
  externalPorts?: {
@@ -34,8 +32,8 @@ export declare class RingApi extends Subscribed {
34
32
  baseStations: BaseStation[];
35
33
  beamBridges: BeamBridge[];
36
34
  }>;
37
- fetchActiveDings(): Promise<ActiveDing[] & import("./rest-client").ExtendedResponse>;
38
35
  private listenForDeviceUpdates;
36
+ registerPushReceiver(cameras: RingCamera[]): Promise<void>;
39
37
  fetchRawLocations(): Promise<UserLocation[]>;
40
38
  fetchAmazonKeyLocks(): Promise<any[] & import("./rest-client").ExtendedResponse>;
41
39
  fetchAndBuildLocations(): Promise<Location[]>;
package/lib/api/api.js CHANGED
@@ -8,6 +8,9 @@ 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.RingApi = void 0;
13
16
  const rest_client_1 = require("./rest-client");
@@ -19,6 +22,7 @@ const operators_1 = require("rxjs/operators");
19
22
  const util_1 = require("./util");
20
23
  const ffmpeg_1 = require("./ffmpeg");
21
24
  const subscribed_1 = require("./subscribed");
25
+ const push_receiver_1 = __importDefault(require("@eneris/push-receiver"));
22
26
  class RingApi extends subscribed_1.Subscribed {
23
27
  constructor(options) {
24
28
  super();
@@ -51,16 +55,13 @@ class RingApi extends subscribed_1.Subscribed {
51
55
  };
52
56
  });
53
57
  }
54
- fetchActiveDings() {
55
- return this.restClient.request({
56
- url: (0, rest_client_1.clientApi)('dings/active'),
57
- });
58
- }
59
58
  listenForDeviceUpdates(cameras, chimes) {
60
- const { cameraStatusPollingSeconds, cameraDingsPollingSeconds } = this.options, onCamerasRequestUpdate = (0, rxjs_1.merge)(...cameras.map((camera) => camera.onRequestUpdate)), onChimesRequestUpdate = (0, rxjs_1.merge)(...chimes.map((chime) => chime.onRequestUpdate)), onCamerasRequestActiveDings = (0, rxjs_1.merge)(...cameras.map((camera) => camera.onRequestActiveDings)), onUpdateReceived = new rxjs_1.Subject(), onActiveDingsReceived = new rxjs_1.Subject(), onPollForStatusUpdate = cameraStatusPollingSeconds
59
+ const { cameraStatusPollingSeconds } = this.options;
60
+ if (!cameraStatusPollingSeconds) {
61
+ return;
62
+ }
63
+ const onCamerasRequestUpdate = (0, rxjs_1.merge)(...cameras.map((camera) => camera.onRequestUpdate)), onChimesRequestUpdate = (0, rxjs_1.merge)(...chimes.map((chime) => chime.onRequestUpdate)), onUpdateReceived = new rxjs_1.Subject(), onPollForStatusUpdate = cameraStatusPollingSeconds
61
64
  ? onUpdateReceived.pipe((0, operators_1.debounceTime)(cameraStatusPollingSeconds * 1000))
62
- : rxjs_1.EMPTY, onPollForActiveDings = cameraDingsPollingSeconds
63
- ? onActiveDingsReceived.pipe((0, operators_1.debounceTime)(cameraDingsPollingSeconds * 1000))
64
65
  : rxjs_1.EMPTY, camerasById = cameras.reduce((byId, camera) => {
65
66
  byId[camera.id] = camera;
66
67
  return byId;
@@ -94,22 +95,59 @@ class RingApi extends subscribed_1.Subscribed {
94
95
  if (cameraStatusPollingSeconds) {
95
96
  onUpdateReceived.next(null); // kick off polling
96
97
  }
97
- this.addSubscriptions((0, rxjs_1.merge)(onCamerasRequestActiveDings, onPollForActiveDings).subscribe(() => __awaiter(this, void 0, void 0, function* () {
98
- const activeDings = yield this.fetchActiveDings().catch(() => null);
99
- onActiveDingsReceived.next(null);
100
- if (!activeDings || !activeDings.length) {
101
- return;
102
- }
103
- activeDings.forEach((activeDing) => {
104
- const camera = camerasById[activeDing.doorbot_id];
105
- if (camera) {
106
- camera.processActiveDing(activeDing);
98
+ }
99
+ registerPushReceiver(cameras) {
100
+ return __awaiter(this, void 0, void 0, function* () {
101
+ const pushReceiver = new push_receiver_1.default({
102
+ logLevel: 'NONE',
103
+ senderId: '876313859327', // for Ring android app. 703521446232 for ring-site
104
+ }), camerasById = cameras.reduce((byId, camera) => {
105
+ byId[camera.id] = camera;
106
+ return byId;
107
+ }, {});
108
+ pushReceiver.onCredentialsChanged(({ newCredentials: { fcm: { token }, }, }) => __awaiter(this, void 0, void 0, function* () {
109
+ try {
110
+ yield this.restClient.request({
111
+ url: (0, rest_client_1.clientApi)('device'),
112
+ method: 'PATCH',
113
+ json: {
114
+ device: {
115
+ metadata: {
116
+ pn_service: 'fcm',
117
+ },
118
+ os: 'android',
119
+ push_notification_token: token,
120
+ },
121
+ },
122
+ });
123
+ }
124
+ catch (e) {
125
+ (0, util_1.logError)(e);
126
+ }
127
+ }));
128
+ pushReceiver.onNotification(({ message }) => {
129
+ var _a;
130
+ const dataJson = (_a = message.data) === null || _a === void 0 ? void 0 : _a.gcmData;
131
+ try {
132
+ const notification = JSON.parse(dataJson);
133
+ if (!notification.ding) {
134
+ // We may get notification that are not for a ding/motion, which can be ignored
135
+ return;
136
+ }
137
+ camerasById[notification.ding.doorbot_id].processPushNotification(notification);
138
+ }
139
+ catch (e) {
140
+ (0, util_1.logError)(e);
107
141
  }
108
142
  });
109
- })));
110
- if (cameras.length && cameraDingsPollingSeconds) {
111
- onActiveDingsReceived.next(null); // kick off polling
112
- }
143
+ try {
144
+ yield pushReceiver.connect();
145
+ }
146
+ catch (e) {
147
+ (0, util_1.logError)('Failed to connect push notification receiver');
148
+ (0, util_1.logError)(e);
149
+ }
150
+ });
113
151
  }
114
152
  fetchRawLocations() {
115
153
  return __awaiter(this, void 0, void 0, function* () {
@@ -129,7 +167,7 @@ class RingApi extends subscribed_1.Subscribed {
129
167
  return __awaiter(this, void 0, void 0, function* () {
130
168
  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) ||
131
169
  authorizedDoorbots.includes(data) ||
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
170
+ data.kind.startsWith('doorbell'), this.restClient, this.options.avoidSnapshotBatteryDrain || false)), ringChimes = chimes.map((data) => new ring_chime_1.RingChime(data, this.restClient)), locations = rawLocations
133
171
  .filter((location) => {
134
172
  return (!Array.isArray(this.options.locationIds) ||
135
173
  this.options.locationIds.includes(location.location_id));
@@ -140,6 +178,9 @@ class RingApi extends subscribed_1.Subscribed {
140
178
  locationModePollingSeconds: this.options.locationModePollingSeconds,
141
179
  }, this.restClient));
142
180
  this.listenForDeviceUpdates(cameras, ringChimes);
181
+ this.registerPushReceiver(cameras).catch((e) => {
182
+ (0, util_1.logError)(e);
183
+ });
143
184
  return locations;
144
185
  });
145
186
  }
@@ -236,7 +236,7 @@ class LiveCallRingEdge extends subscribed_1.Subscribed {
236
236
  return;
237
237
  }
238
238
  this.activated = true;
239
- yield (0, rxjs_1.firstValueFrom)(this.pc.onConnectionState.pipe((0, operators_1.filter)((state) => state === 'connected')));
239
+ yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
240
240
  (0, util_1.logInfo)('Activating Session');
241
241
  this.sendSessionMessage('activate_session');
242
242
  });
@@ -13,7 +13,16 @@ exports.PeerConnection = void 0;
13
13
  const werift_1 = require("@koush/werift");
14
14
  const rxjs_1 = require("rxjs");
15
15
  const util_1 = require("./util");
16
- const debug = false;
16
+ const debug = false, ringIceServers = [
17
+ 'stun:stun.kinesisvideo.us-east-1.amazonaws.com:443',
18
+ 'stun:stun.kinesisvideo.us-east-2.amazonaws.com:443',
19
+ 'stun:stun.kinesisvideo.us-west-2.amazonaws.com:443',
20
+ 'stun:stun.l.google.com:19302',
21
+ 'stun:stun1.l.google.com:19302',
22
+ 'stun:stun2.l.google.com:19302',
23
+ 'stun:stun3.l.google.com:19302',
24
+ 'stun:stun4.l.google.com:19302',
25
+ ];
17
26
  class PeerConnection {
18
27
  constructor() {
19
28
  this.onAudioRtp = new rxjs_1.Subject();
@@ -43,10 +52,12 @@ class PeerConnection {
43
52
  { type: 'nack', parameter: 'pli' },
44
53
  { type: 'goog-remb' },
45
54
  ],
46
- parameters: 'packetization-mode=1;profile-level-id=42801F;level-asymmetry-allowed=1',
55
+ parameters: 'packetization-mode=1;profile-level-id=640029;level-asymmetry-allowed=1',
47
56
  }),
48
57
  ],
49
58
  },
59
+ iceServers: ringIceServers.map((server) => ({ urls: server })),
60
+ iceTransportPolicy: 'all',
50
61
  })), audioTransceiver = pc.addTransceiver(this.returnAudioTrack, {
51
62
  direction: 'sendrecv',
52
63
  }), videoTransceiver = pc.addTransceiver('video', {
@@ -1,12 +1,12 @@
1
1
  /// <reference types="node" />
2
- import { ActiveDing, CameraData, CameraDeviceSettingsData, CameraEventOptions, CameraEventResponse, CameraHealth, HistoryOptions, PeriodicFootageResponse, VideoSearchResponse } from './ring-types';
2
+ import { CameraData, CameraDeviceSettingsData, CameraEventOptions, CameraEventResponse, CameraHealth, HistoryOptions, PeriodicFootageResponse, PushNotification, VideoSearchResponse } from './ring-types';
3
3
  import { RingRestClient } from './rest-client';
4
4
  import { BehaviorSubject, Subject } from 'rxjs';
5
5
  import { DeepPartial } from './util';
6
6
  import { Subscribed } from './subscribed';
7
7
  import { FfmpegOptions, LiveCall } from './live-call';
8
8
  import { LiveCallRingEdge } from './live-call-ring-edge';
9
- export declare function getBatteryLevel(data: Pick<CameraData, 'battery_life' | 'battery_life_2'>): number | null;
9
+ export declare function getBatteryLevel({ health }: Pick<CameraData, 'health'>): number | null;
10
10
  export declare function getSearchQueryString(options: CameraEventOptions | (HistoryOptions & {
11
11
  accountId: string;
12
12
  })): string;
@@ -15,7 +15,6 @@ export declare class RingCamera extends Subscribed {
15
15
  isDoorbot: boolean;
16
16
  private restClient;
17
17
  private avoidSnapshotBatteryDrain;
18
- private treatKnockAsDing;
19
18
  id: number;
20
19
  deviceType: import("./ring-types").RingCameraKind;
21
20
  model: string;
@@ -23,20 +22,20 @@ export declare class RingCamera extends Subscribed {
23
22
  hasLight: boolean;
24
23
  hasSiren: boolean;
25
24
  onRequestUpdate: Subject<unknown>;
26
- onRequestActiveDings: Subject<unknown>;
27
- onNewDing: Subject<ActiveDing>;
28
- onActiveDings: BehaviorSubject<ActiveDing[]>;
29
- onDoorbellPressed: import("rxjs").Observable<ActiveDing>;
25
+ onNewNotification: Subject<PushNotification>;
26
+ onActiveNotifications: BehaviorSubject<PushNotification[]>;
27
+ onDoorbellPressed: import("rxjs").Observable<PushNotification>;
30
28
  onMotionDetected: import("rxjs").Observable<boolean>;
31
29
  onMotionStarted: import("rxjs").Observable<null>;
32
30
  onBatteryLevel: import("rxjs").Observable<number | null>;
33
31
  onInHomeDoorbellStatus: import("rxjs").Observable<boolean>;
34
- constructor(initialData: CameraData, isDoorbot: boolean, restClient: RingRestClient, avoidSnapshotBatteryDrain: boolean, treatKnockAsDing: boolean);
32
+ constructor(initialData: CameraData, isDoorbot: boolean, restClient: RingRestClient, avoidSnapshotBatteryDrain: boolean);
35
33
  updateData(update: CameraData): void;
36
34
  requestUpdate(): void;
37
35
  get data(): CameraData;
38
36
  get name(): string;
39
- get activeDings(): ActiveDing[];
37
+ get activeNotifications(): PushNotification[];
38
+ get latestNotification(): PushNotification | undefined;
40
39
  get batteryLevel(): number | null;
41
40
  get hasBattery(): boolean;
42
41
  get hasLowBattery(): boolean;
@@ -56,7 +55,7 @@ export declare class RingCamera extends Subscribed {
56
55
  getHealth(): Promise<CameraHealth>;
57
56
  startLiveCall(): Promise<LiveCall | LiveCallRingEdge>;
58
57
  private removeDingById;
59
- processActiveDing(ding: ActiveDing): void;
58
+ processPushNotification(notification: PushNotification): void;
60
59
  getEvents(options?: CameraEventOptions): Promise<CameraEventResponse & import("./rest-client").ExtendedResponse>;
61
60
  videoSearch({ dateFrom, dateTo, order }?: {
62
61
  dateFrom: number;
@@ -81,12 +80,16 @@ export declare class RingCamera extends Subscribed {
81
80
  private checkIfSnapshotsAreBlocked;
82
81
  private shouldUseExistingSnapshotPromise;
83
82
  private fetchingSnapshot;
84
- getSnapshot(): Promise<Buffer>;
85
- getNextSnapshot({ afterMs, maxWaitMs, force, }: {
83
+ getSnapshot({ uuid }?: {
84
+ uuid?: string;
85
+ }): Promise<Buffer>;
86
+ getNextSnapshot({ afterMs, maxWaitMs, force, uuid, }: {
86
87
  afterMs?: number;
87
88
  maxWaitMs?: number;
88
89
  force?: boolean;
90
+ uuid?: string;
89
91
  }): Promise<Buffer & import("./rest-client").ExtendedResponse>;
92
+ getSnapshotByUuid(uuid: string): Promise<Buffer & import("./rest-client").ExtendedResponse>;
90
93
  recordToFile(outputPath: string, duration?: number): Promise<void>;
91
94
  streamVideo(ffmpegOptions: FfmpegOptions): Promise<LiveCall | LiveCallRingEdge>;
92
95
  subscribeToDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
@@ -19,33 +19,17 @@ const subscribed_1 = require("./subscribed");
19
19
  const live_call_1 = require("./live-call");
20
20
  const live_call_ring_edge_1 = require("./live-call-ring-edge");
21
21
  const maxSnapshotRefreshSeconds = 15, fullDayMs = 24 * 60 * 60 * 1000;
22
- function parseBatteryLife(batteryLife) {
23
- if (batteryLife === null || batteryLife === undefined) {
24
- return null;
25
- }
26
- const batteryLevel = typeof batteryLife === 'number'
27
- ? batteryLife
28
- : Number.parseFloat(batteryLife);
29
- if (isNaN(batteryLevel)) {
30
- return null;
31
- }
32
- return batteryLevel;
33
- }
34
22
  function getStartOfToday() {
35
23
  return new Date(new Date().toLocaleDateString()).getTime();
36
24
  }
37
25
  function getEndOfToday() {
38
26
  return getStartOfToday() + fullDayMs - 1;
39
27
  }
40
- function getBatteryLevel(data) {
41
- const levels = [
42
- parseBatteryLife(data.battery_life),
43
- parseBatteryLife(data.battery_life_2),
44
- ].filter((level) => level !== null);
45
- if (!levels.length) {
28
+ function getBatteryLevel({ health }) {
29
+ if (!health.battery_percentage && !health.battery_present) {
46
30
  return null;
47
31
  }
48
- return Math.min(...levels);
32
+ return health.battery_percentage || 0;
49
33
  }
50
34
  exports.getBatteryLevel = getBatteryLevel;
51
35
  function getSearchQueryString(options) {
@@ -65,20 +49,17 @@ function getSearchQueryString(options) {
65
49
  }
66
50
  exports.getSearchQueryString = getSearchQueryString;
67
51
  class RingCamera extends subscribed_1.Subscribed {
68
- constructor(initialData, isDoorbot, restClient, avoidSnapshotBatteryDrain, treatKnockAsDing) {
52
+ constructor(initialData, isDoorbot, restClient, avoidSnapshotBatteryDrain) {
69
53
  super();
70
54
  this.initialData = initialData;
71
55
  this.isDoorbot = isDoorbot;
72
56
  this.restClient = restClient;
73
57
  this.avoidSnapshotBatteryDrain = avoidSnapshotBatteryDrain;
74
- this.treatKnockAsDing = treatKnockAsDing;
75
58
  this.onRequestUpdate = new rxjs_1.Subject();
76
- this.onRequestActiveDings = new rxjs_1.Subject();
77
- this.onNewDing = new rxjs_1.Subject();
78
- this.onActiveDings = new rxjs_1.BehaviorSubject([]);
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)());
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)());
59
+ this.onNewNotification = new rxjs_1.Subject();
60
+ this.onActiveNotifications = new rxjs_1.BehaviorSubject([]);
61
+ this.onDoorbellPressed = this.onNewNotification.pipe((0, operators_1.filter)((notification) => notification.action === ring_types_1.PushNotificationAction.Ding), (0, operators_1.share)());
62
+ this.onMotionDetected = this.onActiveNotifications.pipe((0, operators_1.map)((notifications) => notifications.some((notification) => notification.action === ring_types_1.PushNotificationAction.Motion)), (0, operators_1.distinctUntilChanged)(), (0, operators_1.publishReplay)(1), (0, operators_1.refCount)());
82
63
  this.onMotionStarted = this.onMotionDetected.pipe((0, operators_1.filter)((currentlyDetected) => currentlyDetected), (0, operators_1.mapTo)(null), // no value needed, event is what matters
83
64
  (0, operators_1.share)());
84
65
  this.lastSnapshotTimestamp = 0;
@@ -119,26 +100,24 @@ class RingCamera extends subscribed_1.Subscribed {
119
100
  get name() {
120
101
  return this.data.description;
121
102
  }
122
- get activeDings() {
123
- return this.onActiveDings.getValue();
103
+ get activeNotifications() {
104
+ return this.onActiveNotifications.getValue();
105
+ }
106
+ get latestNotification() {
107
+ const notifications = this.activeNotifications;
108
+ return notifications[notifications.length - 1];
124
109
  }
125
110
  get batteryLevel() {
126
111
  return getBatteryLevel(this.data);
127
112
  }
128
113
  get hasBattery() {
129
- if (this.batteryLevel === null) {
130
- return false;
131
- }
132
- return ((0, ring_types_1.isBatteryCameraKind)(this.deviceType) ||
133
- (typeof this.initialData.battery_life === 'string' &&
134
- this.batteryLevel < 100 &&
135
- this.batteryLevel >= 0));
114
+ return this.batteryLevel !== null;
136
115
  }
137
116
  get hasLowBattery() {
138
117
  return this.data.alerts.battery === 'low';
139
118
  }
140
119
  get isCharging() {
141
- return this.initialData.external_connection;
120
+ return Boolean(this.data.health.ac_power || this.data.health.ext_power_state);
142
121
  }
143
122
  get operatingOnBattery() {
144
123
  return this.hasBattery && this.data.settings.power_mode !== 'wired';
@@ -257,15 +236,15 @@ class RingCamera extends subscribed_1.Subscribed {
257
236
  });
258
237
  }
259
238
  removeDingById(idToRemove) {
260
- const allActiveDings = this.activeDings, otherDings = allActiveDings.filter((ding) => ding.id_str !== idToRemove);
261
- this.onActiveDings.next(otherDings);
239
+ const allActiveDings = this.activeNotifications, otherDings = allActiveDings.filter(({ ding }) => ding.id !== idToRemove);
240
+ this.onActiveNotifications.next(otherDings);
262
241
  }
263
- processActiveDing(ding) {
264
- const activeDings = this.activeDings, dingId = ding.id_str;
265
- this.onActiveDings.next(activeDings.filter((d) => d.id_str !== dingId).concat([ding]));
266
- this.onNewDing.next(ding);
242
+ processPushNotification(notification) {
243
+ const activeDings = this.activeNotifications, dingId = notification.ding.id;
244
+ this.onActiveNotifications.next(activeDings.filter((d) => d.ding.id !== dingId).concat([notification]));
245
+ this.onNewNotification.next(notification);
267
246
  setTimeout(() => {
268
- this.removeDingById(ding.id_str);
247
+ this.removeDingById(dingId);
269
248
  }, 65 * 1000); // dings last ~1 minute
270
249
  }
271
250
  getEvents(options = {}) {
@@ -337,17 +316,19 @@ class RingCamera extends subscribed_1.Subscribed {
337
316
  return false;
338
317
  }
339
318
  }
340
- getSnapshot() {
319
+ getSnapshot({ uuid } = {}) {
341
320
  return __awaiter(this, void 0, void 0, function* () {
342
321
  if (this.lastSnapshotPromise && this.shouldUseExistingSnapshotPromise()) {
343
322
  return this.lastSnapshotPromise;
344
323
  }
345
324
  this.checkIfSnapshotsAreBlocked();
346
325
  this.lastSnapshotPromise = Promise.race([
347
- this.getNextSnapshot({
348
- afterMs: this.lastSnapshotTimestamp,
349
- force: true,
350
- }),
326
+ this.getNextSnapshot(uuid
327
+ ? { uuid }
328
+ : {
329
+ afterMs: this.lastSnapshotTimestamp,
330
+ force: true,
331
+ }),
351
332
  (0, util_1.delay)(maxSnapshotRefreshSeconds * 1000).then(() => {
352
333
  const extraMessageForBatteryCam = this.operatingOnBattery
353
334
  ? '. This is normal behavior since this camera is unable to capture snapshots while streaming'
@@ -367,7 +348,7 @@ class RingCamera extends subscribed_1.Subscribed {
367
348
  return this.lastSnapshotPromise;
368
349
  });
369
350
  }
370
- getNextSnapshot({ afterMs, maxWaitMs, force, }) {
351
+ getNextSnapshot({ afterMs, maxWaitMs, force, uuid, }) {
371
352
  return __awaiter(this, void 0, void 0, function* () {
372
353
  const response = yield this.restClient.request({
373
354
  url: `https://app-snaps.ring.com/snapshots/next/${this.id}`,
@@ -376,6 +357,7 @@ class RingCamera extends subscribed_1.Subscribed {
376
357
  'after-ms': afterMs,
377
358
  'max-wait-ms': maxWaitMs,
378
359
  extras: force ? 'force' : undefined,
360
+ uuid,
379
361
  },
380
362
  headers: {
381
363
  accept: 'image/jpeg',
@@ -386,6 +368,15 @@ class RingCamera extends subscribed_1.Subscribed {
386
368
  return response;
387
369
  });
388
370
  }
371
+ getSnapshotByUuid(uuid) {
372
+ return this.restClient.request({
373
+ url: (0, rest_client_1.clientApi)('snapshots/uuid?uuid=' + uuid),
374
+ responseType: 'buffer',
375
+ headers: {
376
+ accept: 'image/jpeg',
377
+ },
378
+ });
379
+ }
389
380
  recordToFile(outputPath, duration = 30) {
390
381
  return __awaiter(this, void 0, void 0, function* () {
391
382
  const liveCall = yield this.streamVideo({
@@ -87,7 +87,6 @@ export declare enum RingCameraKind {
87
87
  cocoa_doorbell = "cocoa_doorbell",
88
88
  stickup_cam_mini = "stickup_cam_mini"
89
89
  }
90
- export declare function isBatteryCameraKind(kind: RingCameraKind): boolean;
91
90
  export declare const RingCameraModel: {
92
91
  readonly [P in RingCameraKind]: string;
93
92
  };
@@ -427,6 +426,44 @@ export interface CameraData {
427
426
  ends_at?: string;
428
427
  seconds_remaining: number;
429
428
  };
429
+ health: {
430
+ device_type: RingCameraKind;
431
+ last_update_time: number;
432
+ connected: boolean;
433
+ rss_connected: boolean;
434
+ vod_enabled: boolean;
435
+ sidewalk_connection: boolean;
436
+ floodlight_on: boolean;
437
+ siren_on?: boolean;
438
+ white_led_on: boolean;
439
+ night_mode_on: boolean;
440
+ hatch_open: boolean;
441
+ packet_loss: number;
442
+ packet_loss_category: 'good' | string;
443
+ rssi: number;
444
+ battery_voltage: number;
445
+ wifi_is_ring_network: boolean;
446
+ supported_rpc_commands: string[];
447
+ ota_status: 'successful' | string;
448
+ ext_power_state?: 0 | number;
449
+ pref_run_mode?: 'low_power' | string;
450
+ run_mode?: 'low_power' | 'full_power' | string;
451
+ network_connection_value: 'wifi' | string;
452
+ ac_power: number;
453
+ battery_present?: boolean;
454
+ external_connection: boolean;
455
+ battery_percentage?: number;
456
+ battery_percentage_category: 'very_good' | 'unknown' | string;
457
+ firmware_version: string;
458
+ rssi_category: 'good' | string;
459
+ battery_voltage_category: 'very_good' | string;
460
+ second_battery_voltage_category: 'unknown' | string;
461
+ second_battery_percentage_category: 'unknown' | string;
462
+ battery_save: boolean;
463
+ firmware_version_status: 'Up to Date';
464
+ tx_rate: number;
465
+ ptz_connected?: 'penguin';
466
+ };
430
467
  }
431
468
  export interface CvDetectionType {
432
469
  enabled: boolean;
@@ -670,31 +707,44 @@ export interface RingDeviceHistoryEvent {
670
707
  body: any;
671
708
  }
672
709
  export declare type DingState = 'ringing' | 'connected' | 'timed_out' | 'completed';
673
- export interface ActiveDing {
674
- id: number;
675
- id_str: string;
676
- state: DingState;
677
- protocol: 'sip';
678
- doorbot_id: number;
679
- doorbot_description: string;
680
- device_kind: RingCameraKind;
681
- motion: boolean;
682
- snapshot_url: '';
683
- kind: DingKind;
684
- sip_server_ip: string;
685
- sip_server_port: number;
686
- sip_server_tls: boolean;
687
- sip_session_id: string;
688
- sip_from: string;
689
- sip_to: string;
690
- audio_jitter_buffer_ms: number;
691
- video_jitter_buffer_ms: number;
692
- sip_endpoints: null;
693
- expires_in: number;
694
- now: number;
695
- optimization_level: number;
696
- sip_token: string;
697
- sip_ding_id: string;
710
+ export declare enum NotificationDetectionType {
711
+ Human = "human",
712
+ Loitering = "loitering",
713
+ Motion = "motion",
714
+ OtherMotion = "other_motion",
715
+ NotAvailable = "null",
716
+ StreamBroken = "stream_broken"
717
+ }
718
+ export declare enum PushNotificationAction {
719
+ Ding = "com.ring.push.HANDLE_NEW_DING",
720
+ Motion = "com.ring.push.HANDLE_NEW_motion"
721
+ }
722
+ export interface PushNotification {
723
+ ding: {
724
+ streaming_protocol: 'ring_media_server';
725
+ location_id: string;
726
+ device_name: string;
727
+ doorbot_id: number;
728
+ e2ee_enabled: boolean;
729
+ streaming_data_hash: string;
730
+ device_kind: RingCameraKind;
731
+ detection_type: NotificationDetectionType;
732
+ human_detected?: boolean;
733
+ id: number;
734
+ pod_id: number;
735
+ request_id: string;
736
+ image_uuid: string;
737
+ properties: {
738
+ active_streaming_profile: 'rms';
739
+ is_sidewalk: boolean;
740
+ };
741
+ };
742
+ aps: {
743
+ alert: string;
744
+ sound: string;
745
+ };
746
+ subtype: 'motion' | 'ding' | 'human' | string;
747
+ action: PushNotificationAction | string;
698
748
  }
699
749
  export interface LiveCallResponse {
700
750
  ding_id: string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isWebSocketSupportedAsset = exports.disabledLocationModes = exports.DispatchSignalType = exports.DoorbellType = exports.ChimeModel = exports.deviceTypesWithVolume = exports.allAlarmStates = exports.RingCameraModel = exports.isBatteryCameraKind = exports.RingCameraKind = exports.RingDeviceCategory = exports.RingDeviceType = void 0;
3
+ exports.isWebSocketSupportedAsset = exports.disabledLocationModes = exports.DispatchSignalType = exports.PushNotificationAction = exports.NotificationDetectionType = exports.DoorbellType = exports.ChimeModel = exports.deviceTypesWithVolume = exports.allAlarmStates = exports.RingCameraModel = exports.RingCameraKind = exports.RingDeviceCategory = exports.RingDeviceType = void 0;
4
4
  // eslint-disable-next-line no-shadow
5
5
  var RingDeviceType;
6
6
  (function (RingDeviceType) {
@@ -95,12 +95,6 @@ var RingCameraKind;
95
95
  RingCameraKind["cocoa_doorbell"] = "cocoa_doorbell";
96
96
  RingCameraKind["stickup_cam_mini"] = "stickup_cam_mini";
97
97
  })(RingCameraKind = exports.RingCameraKind || (exports.RingCameraKind = {}));
98
- // RegExp taken from ring.com app
99
- const isWiredCameraRegExp = new RegExp(/(^(lpd|jbox|stickup_cam_elite|stickup_cam_mini|hp_cam|spotlightw|floodlight|doorbell_graham_cracker))/);
100
- function isBatteryCameraKind(kind) {
101
- return !kind.match(isWiredCameraRegExp);
102
- }
103
- exports.isBatteryCameraKind = isBatteryCameraKind;
104
98
  exports.RingCameraModel = {
105
99
  doorbot: 'Doorbell',
106
100
  doorbell: 'Doorbell',
@@ -158,6 +152,23 @@ var DoorbellType;
158
152
  DoorbellType[DoorbellType["None"] = 2] = "None";
159
153
  })(DoorbellType = exports.DoorbellType || (exports.DoorbellType = {}));
160
154
  // eslint-disable-next-line no-shadow
155
+ var NotificationDetectionType;
156
+ (function (NotificationDetectionType) {
157
+ // Note, this list may not be complete
158
+ NotificationDetectionType["Human"] = "human";
159
+ NotificationDetectionType["Loitering"] = "loitering";
160
+ NotificationDetectionType["Motion"] = "motion";
161
+ NotificationDetectionType["OtherMotion"] = "other_motion";
162
+ NotificationDetectionType["NotAvailable"] = "null";
163
+ NotificationDetectionType["StreamBroken"] = "stream_broken";
164
+ })(NotificationDetectionType = exports.NotificationDetectionType || (exports.NotificationDetectionType = {}));
165
+ // eslint-disable-next-line no-shadow
166
+ var PushNotificationAction;
167
+ (function (PushNotificationAction) {
168
+ PushNotificationAction["Ding"] = "com.ring.push.HANDLE_NEW_DING";
169
+ PushNotificationAction["Motion"] = "com.ring.push.HANDLE_NEW_motion";
170
+ })(PushNotificationAction = exports.PushNotificationAction || (exports.PushNotificationAction = {}));
171
+ // eslint-disable-next-line no-shadow
161
172
  var DispatchSignalType;
162
173
  (function (DispatchSignalType) {
163
174
  DispatchSignalType["Burglar"] = "user-verified-burglar-xa";
package/lib/api/util.js CHANGED
@@ -18,7 +18,7 @@ const colors_1 = require("colors");
18
18
  const readline_1 = require("readline");
19
19
  const uuid_1 = require("uuid");
20
20
  const systeminformation_1 = require("systeminformation");
21
- const debugLogger = (0, debug_1.default)('ring'), uuidNamespace = 'e53ffdc0-e91d-4ce1-bec2-df939d94739c';
21
+ const debugLogger = (0, debug_1.default)('ring'), uuidNamespace = 'e53ffdc0-e91d-4ce1-bec2-df939d94739d';
22
22
  let logger = {
23
23
  logInfo(message) {
24
24
  debugLogger(message);
@@ -7,7 +7,8 @@ export declare class CameraSource implements CameraStreamingDelegate {
7
7
  private cachedSnapshot?;
8
8
  constructor(ringCamera: RingCamera);
9
9
  private previousLoadSnapshotPromise?;
10
- loadSnapshot(): Promise<any>;
10
+ loadSnapshot(imageUuid?: string): Promise<any>;
11
+ fn: number;
11
12
  private loadAndCacheSnapshot;
12
13
  private getCurrentSnapshot;
13
14
  handleSnapshotRequest(request: SnapshotRequest, callback: SnapshotRequestCallback): Promise<void>;
@@ -267,15 +267,16 @@ class CameraSource {
267
267
  },
268
268
  });
269
269
  this.sessions = {};
270
+ this.fn = 1;
270
271
  }
271
- loadSnapshot() {
272
+ loadSnapshot(imageUuid) {
272
273
  return __awaiter(this, void 0, void 0, function* () {
273
274
  // cache a promise of the snapshot load
274
275
  // This prevents multiple concurrent requests for snapshot from pilling up and creating lots of logs
275
276
  if (this.previousLoadSnapshotPromise) {
276
277
  return this.previousLoadSnapshotPromise;
277
278
  }
278
- this.previousLoadSnapshotPromise = this.loadAndCacheSnapshot();
279
+ this.previousLoadSnapshotPromise = this.loadAndCacheSnapshot(imageUuid);
279
280
  try {
280
281
  yield this.previousLoadSnapshotPromise;
281
282
  }
@@ -288,12 +289,12 @@ class CameraSource {
288
289
  }
289
290
  });
290
291
  }
291
- loadAndCacheSnapshot() {
292
+ loadAndCacheSnapshot(imageUuid) {
292
293
  return __awaiter(this, void 0, void 0, function* () {
293
294
  const start = Date.now();
294
- (0, util_1.logDebug)(`Loading new snapshot into cache for ${this.ringCamera.name}`);
295
+ (0, util_1.logDebug)(`Loading new snapshot into cache for ${this.ringCamera.name}${imageUuid ? ' by uuid' : ''}`);
295
296
  try {
296
- const previousSnapshot = this.cachedSnapshot, newSnapshot = yield this.ringCamera.getSnapshot();
297
+ const previousSnapshot = this.cachedSnapshot, newSnapshot = yield this.ringCamera.getSnapshot({ uuid: imageUuid });
297
298
  this.cachedSnapshot = newSnapshot;
298
299
  if (previousSnapshot !== newSnapshot) {
299
300
  // Keep the snapshots in cache 2 minutes longer than their lifetime
@@ -304,7 +305,7 @@ class CameraSource {
304
305
  }
305
306
  }, this.ringCamera.snapshotLifeTime + 2 * 60 * 1000);
306
307
  }
307
- (0, util_1.logDebug)(`Snapshot cached for ${this.ringCamera.name} (${getDurationSeconds(start)}s)`);
308
+ (0, util_1.logDebug)(`Snapshot cached for ${this.ringCamera.name}${imageUuid ? ' by uuid' : ''} (${getDurationSeconds(start)}s)`);
308
309
  }
309
310
  catch (e) {
310
311
  this.cachedSnapshot = undefined;
@@ -150,9 +150,7 @@ class Camera extends base_data_accessory_1.BaseDataAccessory {
150
150
  serviceType: Service.AccessoryInformation,
151
151
  getValue: (data) => data.device_id,
152
152
  });
153
- if (device.hasBattery ||
154
- // detected as having battery in the past
155
- accessory.getService(Service.BatteryService)) {
153
+ if (device.hasBattery) {
156
154
  this.registerCharacteristic({
157
155
  characteristicType: Characteristic.StatusLowBattery,
158
156
  serviceType: Service.BatteryService,
@@ -165,8 +163,8 @@ class Camera extends base_data_accessory_1.BaseDataAccessory {
165
163
  this.registerCharacteristic({
166
164
  characteristicType: Characteristic.ChargingState,
167
165
  serviceType: Service.BatteryService,
168
- getValue: (data) => {
169
- return data.external_connection
166
+ getValue: () => {
167
+ return device.isCharging
170
168
  ? ChargingState.CHARGING
171
169
  : ChargingState.NOT_CHARGING;
172
170
  },
@@ -182,7 +180,8 @@ class Camera extends base_data_accessory_1.BaseDataAccessory {
182
180
  }
183
181
  loadSnapshotForEvent(eventDescription, characteristicValue) {
184
182
  return __awaiter(this, void 0, void 0, function* () {
185
- if (this.device.operatingOnBattery) {
183
+ const latestNotification = this.device.latestNotification, imageUuid = latestNotification === null || latestNotification === void 0 ? void 0 : latestNotification.ding.image_uuid;
184
+ if (this.device.operatingOnBattery && !imageUuid) {
186
185
  // battery cameras cannot fetch a new snapshot while recording is in progress
187
186
  this.logger.info(this.device.name + ' ' + eventDescription);
188
187
  return characteristicValue;
@@ -190,7 +189,7 @@ class Camera extends base_data_accessory_1.BaseDataAccessory {
190
189
  this.logger.info(this.device.name +
191
190
  ` ${eventDescription}. Loading snapshot before sending event to HomeKit`);
192
191
  try {
193
- yield this.cameraSource.loadSnapshot();
192
+ yield this.cameraSource.loadSnapshot(imageUuid);
194
193
  }
195
194
  catch (e) {
196
195
  this.logger.info(this.device.name +
@@ -122,7 +122,7 @@ function getAccessoryClass(device) {
122
122
  }
123
123
  class RingPlatform {
124
124
  constructor(log, config, api) {
125
- var _a, _b, _c;
125
+ var _a, _b;
126
126
  this.log = log;
127
127
  this.config = config;
128
128
  this.api = api;
@@ -140,8 +140,7 @@ class RingPlatform {
140
140
  return;
141
141
  }
142
142
  config.cameraStatusPollingSeconds = (_a = config.cameraStatusPollingSeconds) !== null && _a !== void 0 ? _a : 20;
143
- config.cameraDingsPollingSeconds = (_b = config.cameraDingsPollingSeconds) !== null && _b !== void 0 ? _b : 2;
144
- config.locationModePollingSeconds = (_c = config.locationModePollingSeconds) !== null && _c !== void 0 ? _c : 20;
143
+ config.locationModePollingSeconds = (_b = config.locationModePollingSeconds) !== null && _b !== void 0 ? _b : 20;
145
144
  this.api.on('didFinishLaunching', () => {
146
145
  this.log.debug('didFinishLaunching');
147
146
  if (config.refreshToken) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-ring",
3
- "version": "10.0.0-beta.3",
3
+ "version": "10.0.0-beta.4",
4
4
  "description": "Homebridge plugin for Ring doorbells, cameras, security alarm system and smart lighting",
5
5
  "main": "lib/homebridge/index.js",
6
6
  "bin": {
@@ -39,6 +39,7 @@
39
39
  "author": "dgreif",
40
40
  "license": "MIT",
41
41
  "dependencies": {
42
+ "@eneris/push-receiver": "^3.0.1",
42
43
  "@homebridge/camera-utils": "2.1.0",
43
44
  "@homebridge/plugin-ui-utils": "0.0.19",
44
45
  "@koush/werift": "0.14.5-beta7",
@@ -1 +0,0 @@
1
- export {};
@@ -1,23 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const api_1 = require("../../api");
4
- describe('Ring Camera', () => {
5
- describe('battery level', () => {
6
- it('should handle string battery life', () => {
7
- expect((0, api_1.getBatteryLevel)({ battery_life: '49' })).toEqual(49);
8
- });
9
- it('should handle null battery life', () => {
10
- expect((0, api_1.getBatteryLevel)({ battery_life: null })).toEqual(null);
11
- });
12
- it('should handle right battery only', () => {
13
- expect((0, api_1.getBatteryLevel)({ battery_life: null, battery_life_2: 24 })).toEqual(24);
14
- });
15
- it('should handle left battery only', () => {
16
- expect((0, api_1.getBatteryLevel)({ battery_life: 76, battery_life_2: null })).toEqual(76);
17
- });
18
- it('should handle dual batteries', () => {
19
- expect((0, api_1.getBatteryLevel)({ battery_life: '92', battery_life_2: 84 })).toEqual(84);
20
- expect((0, api_1.getBatteryLevel)({ battery_life: '92', battery_life_2: 100 })).toEqual(92);
21
- });
22
- });
23
- });