homebridge-ring 10.0.0 → 11.0.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 +50 -0
- package/README.md +0 -1
- package/lib/api/api.js +79 -96
- package/lib/api/device-data.js +20 -31
- package/lib/api/location.js +125 -157
- package/lib/api/refresh-token.js +23 -36
- package/lib/api/rest-client.js +159 -165
- package/lib/api/ring-camera.js +121 -157
- package/lib/api/ring-chime.js +32 -51
- package/lib/api/ring-device.js +4 -1
- package/lib/api/streaming/peer-connection.js +11 -26
- package/lib/api/streaming/ring-edge-connection.js +56 -65
- package/lib/api/streaming/simple-webrtc-session.js +27 -42
- package/lib/api/streaming/streaming-connection-base.js +12 -25
- package/lib/api/streaming/streaming-session.js +56 -73
- package/lib/api/streaming/webrtc-connection.js +22 -30
- package/lib/api/util.js +26 -39
- package/lib/homebridge/base-accessory.js +3 -12
- package/lib/homebridge/camera-source.js +218 -239
- package/lib/homebridge/camera.js +16 -27
- package/lib/homebridge/homebridge-ui/server.js +6 -15
- package/lib/homebridge/location-mode-switch.js +10 -19
- package/lib/homebridge/ring-platform.js +98 -107
- package/lib/homebridge/security-panel.js +40 -51
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,58 @@
|
|
|
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
|
+
## [11.0.0](https://github.com/dgreif/ring/compare/v10.0.0...v11.0.0) (2022-05-08)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* Node 12/14 are no longer supported, please update to node 16. A dependency added in v10 did not support node 12/14, which is why I'm officially dropping support now.
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* officially drop node 12 & 14 support ([3d65c77](https://github.com/dgreif/ring/commit/3d65c77fab9399f29d4fb15d0e87dfe60f3db40f)), closes [#926](https://github.com/dgreif/ring/issues/926)
|
|
15
|
+
|
|
5
16
|
## [10.0.0](https://github.com/dgreif/ring/compare/v10.0.0-beta.8...v10.0.0) (2022-05-08)
|
|
6
17
|
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* support ring edge camera streaming ([b30d99f](https://github.com/dgreif/ring/commit/b30d99f9ffaa1fabc1d09b830cf8ba0e69fe84a0))
|
|
21
|
+
* receive dings from push notifications ([c9f130f](https://github.com/dgreif/ring/commit/c9f130f0f1f21b6db2454d611641ef7c29c7e620))
|
|
22
|
+
* **homebridge:** faster snapshots from camera notifications ([7fc821b](https://github.com/dgreif/ring/commit/7fc821b71013c36314d6d428b8152e631ea4b497))
|
|
23
|
+
* add `cocoa_floodlight` ([c7a7e9c](https://github.com/dgreif/ring/commit/c7a7e9cabf45ec6579a7d9a1033fa01930923243)), closes [#893](https://github.com/dgreif/ring/issues/893)
|
|
24
|
+
* **api:** simple webrtc session ([bac7a49](https://github.com/dgreif/ring/commit/bac7a49178bb5a22914e53fda91c0fdd522b4a74))
|
|
25
|
+
* **api:** allow custom peer connections for streaming ([3e9e784](https://github.com/dgreif/ring/commit/3e9e78458afe2849a8c414a6b50eec8718bf77cc))
|
|
26
|
+
* **api:** convert ding handling to use notifications ([5b62318](https://github.com/dgreif/ring/commit/5b62318b6a71bcc224a2b29cbd6e21162fc9891e))
|
|
27
|
+
* **api:** update notification types ([15d3e7e](https://github.com/dgreif/ring/commit/15d3e7e40744e2c3ed48140fa94fbc0c6f9d4088))
|
|
28
|
+
|
|
29
|
+
### Bug Fixes
|
|
30
|
+
|
|
31
|
+
* **homebridge:** add `name` option to schema so that it won't be removed by config-ui ([af6755a](https://github.com/dgreif/ring/commit/af6755aa09cf4aaad0963499ad0d1afd0beeab96)), closes [#870](https://github.com/dgreif/ring/issues/870)
|
|
32
|
+
* include device name when setting push token ([2466a25](https://github.com/dgreif/ring/commit/2466a2519341771d8c79557b5a61c339f31eddf6)), closes [#920](https://github.com/dgreif/ring/issues/920)
|
|
33
|
+
* npm audit fix ([c1fe605](https://github.com/dgreif/ring/commit/c1fe6051e46dac92b00bb2fd1202746fd576b15a))
|
|
34
|
+
* remove doorbell press simulation options ([e1d1225](https://github.com/dgreif/ring/commit/e1d12257b2acbdafdc6995a53d9052822c8a3460))
|
|
35
|
+
* update dependencies ([4b25595](https://github.com/dgreif/ring/commit/4b255950bcfbf94486192d55cfc90d966cd79d24))
|
|
36
|
+
* **cameras:** wait to activate session until connected ([9a49389](https://github.com/dgreif/ring/commit/9a49389aa434cc5df4cfb4ba039bad0f2a30f756))
|
|
37
|
+
* handle push notification for unknown camera ([cc8fc2a](https://github.com/dgreif/ring/commit/cc8fc2a6377084515d3b169a85644b2a2b13235e))
|
|
38
|
+
* hide ECONNRESET errors from push receiver ([1196d96](https://github.com/dgreif/ring/commit/1196d96e67e26952f18dfbe75103419a7261bd1e))
|
|
39
|
+
* update dependencies ([4b97fb3](https://github.com/dgreif/ring/commit/4b97fb33623248147997944bab3de4ea3c2e5ee2))
|
|
40
|
+
* update uuid namespace to force new client sessions ([bc3e943](https://github.com/dgreif/ring/commit/bc3e9432c41dda085845f72f332dcf480c92475d))
|
|
41
|
+
* separate streaming setup for ring edge vs regular cameras ([6281cf9](https://github.com/dgreif/ring/commit/6281cf966de12dcea8acc6516cd9079634734dec))
|
|
42
|
+
* **api:** allow snapshot to be force-updated based on options ([fa0212a](https://github.com/dgreif/ring/commit/fa0212ab539cc268f8000c5b9ea39a00799cb8d7))
|
|
43
|
+
* drop support for node 10 ([509d556](https://github.com/dgreif/ring/commit/509d55670b3dd4efee1be968618f6bc6b142ae2b))
|
|
44
|
+
* **homebridge:** increase minimum homebridge version to 1.4.0 ([d5be74a](https://github.com/dgreif/ring/commit/d5be74aae45a3e353bcaba4fc42fa25c6b2fa37e))
|
|
45
|
+
* remove sip calling in favor of live calls ([7aeae37](https://github.com/dgreif/ring/commit/7aeae37e6e93b68747e14983cea9c0a9229faec6))
|
|
46
|
+
* use Buffer.from ([2b28d90](https://github.com/dgreif/ring/commit/2b28d9028fc7f2c2e11b11cf3b27cc03f097eab2))
|
|
47
|
+
|
|
48
|
+
### ⚠ BREAKING CHANGES
|
|
49
|
+
|
|
50
|
+
* The `sendDoorbellMotionNotificationsToTv` and `sendCameraMotionNotificationsToTv` options have been removed. These options were originally inteded as a workaround for the fact that tvOS would only automatically start streaming for doorbell presses and not for motion events. tvOS 15 now supports this features as an option in the UI, so we no longer need this option at the plugin level.
|
|
51
|
+
* **api:** All ding-related functionality will now use the `PushNotification` type, and the `ActiveDing` type has been removed
|
|
52
|
+
* **api:** The `cameraDingsPollingSeconds` option has been removed. Ding events will now be received via push notifications (no setup required)
|
|
53
|
+
* **api:** The `treatKnockAsDing` option has been removed. Knocks should now register as "dings" automatically, though I have not been able to test this functionality
|
|
54
|
+
* `ring-client-api` only (homebridge not impacted) - All streaming will now be done using the new Ring api via WebRTC + WebSockets. For those using `camera.streamVideo`, you should not notice any difference. Anyone directly interacting with sip sessions, you will need to migrate to the new `LiveCall` api. It's fairly similar, and should be more reliable long term. A number of other sip-specific methods have been removed from the `RingCamera` api as well.
|
|
55
|
+
* Node 10 is no longer officially supported. It may still work, but could break without warning in the future.
|
|
56
|
+
* **homebridge:** The minimum supported homebridge version for this plugin is now 1.4.0
|
|
7
57
|
|
|
8
58
|
### Bug Fixes
|
|
9
59
|
|
package/README.md
CHANGED
|
@@ -64,7 +64,6 @@ Only include an optional parameter if you actually need it. Default behavior wi
|
|
|
64
64
|
"hideAlarmSirenSwitch": true,
|
|
65
65
|
"hideUnsupportedServices": true,
|
|
66
66
|
"avoidSnapshotBatteryDrain": true,
|
|
67
|
-
"treatKnockAsDing": true,
|
|
68
67
|
"cameraStatusPollingSeconds": 20,
|
|
69
68
|
"locationModePollingSeconds": 20,
|
|
70
69
|
"locationIds": ["488e4800-fcde-4493-969b-d1a06f683102", "4bbed7a7-06df-4f18-b3af-291c89854d60"]
|
package/lib/api/api.js
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
4
|
};
|
|
@@ -41,19 +32,17 @@ class RingApi extends subscribed_1.Subscribed {
|
|
|
41
32
|
(0, ffmpeg_1.setFfmpegPath)(ffmpegPath);
|
|
42
33
|
}
|
|
43
34
|
}
|
|
44
|
-
fetchRingDevices() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
};
|
|
56
|
-
});
|
|
35
|
+
async fetchRingDevices() {
|
|
36
|
+
const { doorbots, chimes, authorized_doorbots: authorizedDoorbots, stickup_cams: stickupCams, base_stations: baseStations, beams_bridges: beamBridges, } = await this.restClient.request({ url: (0, rest_client_1.clientApi)('ring_devices') });
|
|
37
|
+
return {
|
|
38
|
+
doorbots,
|
|
39
|
+
chimes,
|
|
40
|
+
authorizedDoorbots,
|
|
41
|
+
stickupCams,
|
|
42
|
+
allCameras: doorbots.concat(stickupCams, authorizedDoorbots),
|
|
43
|
+
baseStations,
|
|
44
|
+
beamBridges,
|
|
45
|
+
};
|
|
57
46
|
}
|
|
58
47
|
listenForDeviceUpdates(cameras, chimes) {
|
|
59
48
|
const { cameraStatusPollingSeconds } = this.options;
|
|
@@ -96,91 +85,87 @@ class RingApi extends subscribed_1.Subscribed {
|
|
|
96
85
|
onUpdateReceived.next(null); // kick off polling
|
|
97
86
|
}
|
|
98
87
|
}
|
|
99
|
-
registerPushReceiver(cameras) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
push_notification_token: token,
|
|
88
|
+
async registerPushReceiver(cameras) {
|
|
89
|
+
const pushReceiver = new push_receiver_1.default({
|
|
90
|
+
logLevel: 'NONE',
|
|
91
|
+
senderId: '876313859327', // for Ring android app. 703521446232 for ring-site
|
|
92
|
+
}), camerasById = cameras.reduce((byId, camera) => {
|
|
93
|
+
byId[camera.id] = camera;
|
|
94
|
+
return byId;
|
|
95
|
+
}, {});
|
|
96
|
+
pushReceiver.onCredentialsChanged(async ({ newCredentials: { fcm: { token }, }, }) => {
|
|
97
|
+
try {
|
|
98
|
+
await this.restClient.request({
|
|
99
|
+
url: (0, rest_client_1.clientApi)('device'),
|
|
100
|
+
method: 'PATCH',
|
|
101
|
+
json: {
|
|
102
|
+
device: {
|
|
103
|
+
metadata: {
|
|
104
|
+
...this.restClient.baseSessionMetadata,
|
|
105
|
+
pn_service: 'fcm',
|
|
118
106
|
},
|
|
107
|
+
os: 'android',
|
|
108
|
+
push_notification_token: token,
|
|
119
109
|
},
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
catch (e) {
|
|
123
|
-
(0, util_1.logError)(e);
|
|
124
|
-
}
|
|
125
|
-
}));
|
|
126
|
-
pushReceiver.onNotification(({ message }) => {
|
|
127
|
-
var _a, _b;
|
|
128
|
-
const dataJson = (_a = message.data) === null || _a === void 0 ? void 0 : _a.gcmData;
|
|
129
|
-
try {
|
|
130
|
-
const notification = JSON.parse(dataJson);
|
|
131
|
-
if (!notification.ding) {
|
|
132
|
-
// We may get notification that are not for a ding/motion, which can be ignored
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
(_b = camerasById[notification.ding.doorbot_id]) === null || _b === void 0 ? void 0 : _b.processPushNotification(notification);
|
|
136
|
-
}
|
|
137
|
-
catch (e) {
|
|
138
|
-
(0, util_1.logError)(e);
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
try {
|
|
142
|
-
yield pushReceiver.connect();
|
|
110
|
+
},
|
|
111
|
+
});
|
|
143
112
|
}
|
|
144
113
|
catch (e) {
|
|
145
|
-
(0, util_1.logError)('Failed to connect push notification receiver');
|
|
146
114
|
(0, util_1.logError)(e);
|
|
147
115
|
}
|
|
148
116
|
});
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
117
|
+
pushReceiver.onNotification(({ message }) => {
|
|
118
|
+
const dataJson = message.data?.gcmData;
|
|
119
|
+
try {
|
|
120
|
+
const notification = JSON.parse(dataJson);
|
|
121
|
+
if (!notification.ding) {
|
|
122
|
+
// We may get notification that are not for a ding/motion, which can be ignored
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
camerasById[notification.ding.doorbot_id]?.processPushNotification(notification);
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
(0, util_1.logError)(e);
|
|
155
129
|
}
|
|
156
|
-
return rawLocations;
|
|
157
130
|
});
|
|
131
|
+
try {
|
|
132
|
+
await pushReceiver.connect();
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
(0, util_1.logError)('Failed to connect push notification receiver');
|
|
136
|
+
(0, util_1.logError)(e);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async fetchRawLocations() {
|
|
140
|
+
const { user_locations: rawLocations } = await this.restClient.request({ url: (0, rest_client_1.deviceApi)('locations') });
|
|
141
|
+
if (!rawLocations) {
|
|
142
|
+
throw new Error('The Ring account which you used to generate a refresh token does not have any associated locations. Please use an account that has access to at least one location.');
|
|
143
|
+
}
|
|
144
|
+
return rawLocations;
|
|
158
145
|
}
|
|
159
146
|
fetchAmazonKeyLocks() {
|
|
160
147
|
return this.restClient.request({
|
|
161
148
|
url: 'https://api.ring.com/integrations/amazonkey/v2/devices/lock_associations',
|
|
162
149
|
});
|
|
163
150
|
}
|
|
164
|
-
fetchAndBuildLocations() {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
(0, util_1.logError)(e);
|
|
181
|
-
});
|
|
182
|
-
return locations;
|
|
151
|
+
async fetchAndBuildLocations() {
|
|
152
|
+
const rawLocations = await this.fetchRawLocations(), { authorizedDoorbots, chimes, doorbots, allCameras, baseStations, beamBridges, } = await this.fetchRingDevices(), locationIdsWithHubs = [...baseStations, ...beamBridges].map((x) => x.location_id), cameras = allCameras.map((data) => new ring_camera_1.RingCamera(data, doorbots.includes(data) ||
|
|
153
|
+
authorizedDoorbots.includes(data) ||
|
|
154
|
+
data.kind.startsWith('doorbell'), this.restClient, this.options.avoidSnapshotBatteryDrain || false)), ringChimes = chimes.map((data) => new ring_chime_1.RingChime(data, this.restClient)), locations = rawLocations
|
|
155
|
+
.filter((location) => {
|
|
156
|
+
return (!Array.isArray(this.options.locationIds) ||
|
|
157
|
+
this.options.locationIds.includes(location.location_id));
|
|
158
|
+
})
|
|
159
|
+
.map((location) => new location_1.Location(location, cameras.filter((x) => x.data.location_id === location.location_id), ringChimes.filter((x) => x.data.location_id === location.location_id), {
|
|
160
|
+
hasHubs: locationIdsWithHubs.includes(location.location_id),
|
|
161
|
+
hasAlarmBaseStation: baseStations.some((station) => station.location_id === location.location_id),
|
|
162
|
+
locationModePollingSeconds: this.options.locationModePollingSeconds,
|
|
163
|
+
}, this.restClient));
|
|
164
|
+
this.listenForDeviceUpdates(cameras, ringChimes);
|
|
165
|
+
this.registerPushReceiver(cameras).catch((e) => {
|
|
166
|
+
(0, util_1.logError)(e);
|
|
183
167
|
});
|
|
168
|
+
return locations;
|
|
184
169
|
}
|
|
185
170
|
getLocations() {
|
|
186
171
|
if (!this.locationsPromise) {
|
|
@@ -188,11 +173,9 @@ class RingApi extends subscribed_1.Subscribed {
|
|
|
188
173
|
}
|
|
189
174
|
return this.locationsPromise;
|
|
190
175
|
}
|
|
191
|
-
getCameras() {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return locations.reduce((cameras, location) => [...cameras, ...location.cameras], []);
|
|
195
|
-
});
|
|
176
|
+
async getCameras() {
|
|
177
|
+
const locations = await this.getLocations();
|
|
178
|
+
return locations.reduce((cameras, location) => [...cameras, ...location.cameras], []);
|
|
196
179
|
}
|
|
197
180
|
getProfile() {
|
|
198
181
|
return this.restClient.request({
|
package/lib/api/device-data.js
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
3
|
exports.logDeviceData = void 0;
|
|
13
4
|
/* eslint-disable no-console */
|
|
@@ -59,28 +50,26 @@ function stripSensitiveFields(input) {
|
|
|
59
50
|
}
|
|
60
51
|
}
|
|
61
52
|
}
|
|
62
|
-
function logDeviceData() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
devices: devices.map((device) => device.data),
|
|
75
|
-
};
|
|
76
|
-
})), results = {
|
|
77
|
-
locations: locationsWithDevices,
|
|
78
|
-
amazonKeyLocks,
|
|
53
|
+
async function logDeviceData() {
|
|
54
|
+
console.log('This CLI will log data from you Ring Account to help debug issues and discovering new device types.');
|
|
55
|
+
console.log('The logged data is anonymized and should not compromise your account in any way.');
|
|
56
|
+
const refreshToken = await (0, refresh_token_1.acquireRefreshToken)(), ringApi = new api_1.RingApi({ refreshToken });
|
|
57
|
+
console.log('Successfully logged in. Fetching devices...');
|
|
58
|
+
const locations = await ringApi.getLocations(), amazonKeyLocks = await ringApi.fetchAmazonKeyLocks(), locationsWithDevices = await (0, util_1.mapAsync)(locations, async (location) => {
|
|
59
|
+
const devices = await location.getDevices();
|
|
60
|
+
return {
|
|
61
|
+
name: location.name,
|
|
62
|
+
cameras: location.cameras.map((camera) => camera.data),
|
|
63
|
+
chimes: location.chimes.map((chime) => chime.data),
|
|
64
|
+
devices: devices.map((device) => device.data),
|
|
79
65
|
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
66
|
+
}), results = {
|
|
67
|
+
locations: locationsWithDevices,
|
|
68
|
+
amazonKeyLocks,
|
|
69
|
+
};
|
|
70
|
+
stripSensitiveFields(results);
|
|
71
|
+
console.log('\nPlease copy and paste everything AFTER THIS LINE:\n\n');
|
|
72
|
+
console.log(JSON.stringify(results));
|
|
73
|
+
process.exit(0);
|
|
85
74
|
}
|
|
86
75
|
exports.logDeviceData = logDeviceData;
|