ring-client-api 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 +23 -0
- package/README.md +0 -3
- package/lib/api/api.d.ts +2 -4
- package/lib/api/api.js +64 -23
- package/lib/api/live-call-ring-edge.js +1 -1
- package/lib/api/peer-connection.js +13 -2
- package/lib/api/ring-camera.d.ts +15 -12
- package/lib/api/ring-camera.js +41 -50
- package/lib/api/ring-types.d.ts +76 -26
- package/lib/api/ring-types.js +18 -7
- package/lib/api/util.js +1 -1
- package/package.json +3 -2
- package/lib/test/api/ring-camera.spec.d.ts +0 -1
- package/lib/test/api/ring-camera.spec.js +0 -23
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
|
@@ -27,7 +27,6 @@ const ringApi = new RingApi({
|
|
|
27
27
|
|
|
28
28
|
// The following are all optional. See below for details
|
|
29
29
|
cameraStatusPollingSeconds: 20,
|
|
30
|
-
cameraDingsPollingSeconds: 2,
|
|
31
30
|
locationIds: ['488e4800-fcde-4493-969b-d1a06f683102', '4bbed7a7-06df-4f18-b3af-291c89854d60']
|
|
32
31
|
});
|
|
33
32
|
```
|
|
@@ -37,7 +36,6 @@ const ringApi = new RingApi({
|
|
|
37
36
|
Option | Default | Explanation
|
|
38
37
|
--- | --- | ---
|
|
39
38
|
`cameraStatusPollingSeconds` | `undefined` (No Polling) | 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.
|
|
40
|
-
`cameraDingsPollingSeconds` | `undefined` (No Polling) | How frequently to poll for new events from your cameras (in seconds). These include motion and doorbell presses. Without this option, cameras will not emit any information about motion and doorbell presses.
|
|
41
39
|
`locationModePollingSeconds` | `undefined` (No Polling) | 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.
|
|
42
40
|
`locationIds` | All Locations | Allows you to limit the results to a specific set of locations. This is mainly useful for the [homebridge-ring Plugin](./homebridge), but can also be used if you only care about listening for events at a subset of your locations and don't want to create websocket connections to _all_ of your locations. This will also limit the results for `ringApi.getCameras()` to the configured locations. If this option is not included, all locations will be returned.
|
|
43
41
|
`debug` | false | Turns on additional logging. In particular, ffmpeg logging.
|
|
@@ -45,7 +43,6 @@ Option | Default | Explanation
|
|
|
45
43
|
`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
44
|
`controlCenterDisplayName` | 'ring-client-api' | This allows you to change the displayed name for the Authorized Device within Control Center in the Ring app
|
|
47
45
|
`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`
|
|
49
46
|
|
|
50
47
|
## Locations
|
|
51
48
|
```typescript
|
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 {
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
|
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.
|
|
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=
|
|
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', {
|
package/lib/api/ring-camera.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
import {
|
|
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(
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
85
|
-
|
|
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>;
|
package/lib/api/ring-camera.js
CHANGED
|
@@ -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(
|
|
41
|
-
|
|
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
|
|
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
|
|
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.
|
|
77
|
-
this.
|
|
78
|
-
this.
|
|
79
|
-
this.
|
|
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
|
|
123
|
-
return this.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
261
|
-
this.
|
|
239
|
+
const allActiveDings = this.activeNotifications, otherDings = allActiveDings.filter(({ ding }) => ding.id !== idToRemove);
|
|
240
|
+
this.onActiveNotifications.next(otherDings);
|
|
262
241
|
}
|
|
263
|
-
|
|
264
|
-
const activeDings = this.
|
|
265
|
-
this.
|
|
266
|
-
this.
|
|
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(
|
|
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
|
-
|
|
349
|
-
|
|
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({
|
package/lib/api/ring-types.d.ts
CHANGED
|
@@ -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
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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;
|
package/lib/api/ring-types.js
CHANGED
|
@@ -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.
|
|
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-
|
|
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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ring-client-api",
|
|
3
|
-
"version": "10.0.0-beta.
|
|
3
|
+
"version": "10.0.0-beta.4",
|
|
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": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "concurrently -c yellow,blue --kill-others \"npm:hb\" \"npm:watch:ui\"",
|
|
12
12
|
"watch:ui": "npm start --prefix homebridge-ui",
|
|
13
|
-
"test": "
|
|
13
|
+
"test": "eslint '**/*.ts'",
|
|
14
14
|
"watch:tests": "jest --watch",
|
|
15
15
|
"lint": "eslint '**/*.ts' --fix",
|
|
16
16
|
"build": "rm -rf lib && tsc --declaration",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"author": "dgreif",
|
|
56
56
|
"license": "MIT",
|
|
57
57
|
"dependencies": {
|
|
58
|
+
"@eneris/push-receiver": "^3.0.1",
|
|
58
59
|
"@homebridge/camera-utils": "2.1.0",
|
|
59
60
|
"@homebridge/plugin-ui-utils": "0.0.19",
|
|
60
61
|
"@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
|
-
});
|