homebridge-ring-hksv 14.3.6 → 14.3.7
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/lib/camera-source.js +1 -1
- package/lib/intercom.js +16 -208
- package/lib/ring-platform.js +6 -23
- package/package.json +1 -1
package/lib/camera-source.js
CHANGED
|
@@ -434,7 +434,7 @@ export class CameraSource {
|
|
|
434
434
|
this.cachedSnapshot = undefined;
|
|
435
435
|
logDebug(`Failed to cache snapshot for ${this.ringCamera.name} (${getDurationSeconds(start)}s), The camera currently reports that it is ${this.ringCamera.isOffline ? 'offline' : 'online'}`);
|
|
436
436
|
// log additioanl snapshot error message if one is present
|
|
437
|
-
if (e.message
|
|
437
|
+
if (e.message.includes('Snapshot')) {
|
|
438
438
|
logDebug(e.message);
|
|
439
439
|
}
|
|
440
440
|
}
|
package/lib/intercom.js
CHANGED
|
@@ -1,125 +1,7 @@
|
|
|
1
|
-
import { RingCamera } from 'ring-client-api';
|
|
2
1
|
import { hap } from "./hap.js";
|
|
3
2
|
import { BaseDataAccessory } from "./base-data-accessory.js";
|
|
4
3
|
import { logError, logInfo } from 'ring-client-api/util';
|
|
5
|
-
import {
|
|
6
|
-
import { interval, merge, Subject } from 'rxjs';
|
|
7
|
-
import { CameraSource } from "./camera-source.js";
|
|
8
|
-
/**
|
|
9
|
-
* Creates a RingCamera-compatible proxy for a Ring Intercom Video device.
|
|
10
|
-
* Ring Intercom Video is classified as RingIntercom by ring-client-api but has a
|
|
11
|
-
* real camera. We use Object.create(RingCamera.prototype) so that startLiveCall()
|
|
12
|
-
* and createStreamingConnection() are inherited and work with the intercom's
|
|
13
|
-
* restClient and device id (Ring's signalling server uses the same doorbot_id
|
|
14
|
-
* scheme for video intercoms as for cameras).
|
|
15
|
-
*/
|
|
16
|
-
function createIntercomVideoProxy(device) {
|
|
17
|
-
const proxy = Object.create(RingCamera.prototype);
|
|
18
|
-
const defineValue = (key, value) => Object.defineProperty(proxy, key, {
|
|
19
|
-
value,
|
|
20
|
-
writable: true,
|
|
21
|
-
configurable: true,
|
|
22
|
-
enumerable: true,
|
|
23
|
-
});
|
|
24
|
-
const defineGetter = (key, get) => Object.defineProperty(proxy, key, {
|
|
25
|
-
get,
|
|
26
|
-
configurable: true,
|
|
27
|
-
enumerable: true,
|
|
28
|
-
});
|
|
29
|
-
defineValue('id', device.id);
|
|
30
|
-
defineGetter('name', () => device.name);
|
|
31
|
-
defineGetter('deviceType', () => device.deviceType);
|
|
32
|
-
defineGetter('data', () => ({
|
|
33
|
-
...device.data,
|
|
34
|
-
kind: device.deviceType,
|
|
35
|
-
device_id: String(device.id),
|
|
36
|
-
metadata: device.data.metadata ?? {},
|
|
37
|
-
settings: {
|
|
38
|
-
live_view_disabled: false,
|
|
39
|
-
motion_detection_enabled: true,
|
|
40
|
-
...device.data.settings,
|
|
41
|
-
},
|
|
42
|
-
}));
|
|
43
|
-
// restClient is technically private in TypeScript but public in compiled JS
|
|
44
|
-
defineValue('restClient', device.restClient);
|
|
45
|
-
defineGetter('isRingEdgeEnabled', () => false);
|
|
46
|
-
defineGetter('hasBattery', () => device.batteryLevel !== null);
|
|
47
|
-
defineGetter('isOffline', () => device.isOffline);
|
|
48
|
-
defineGetter('snapshotsAreBlocked', () => false);
|
|
49
|
-
defineGetter('canTakeSnapshotWhileRecording', () => false);
|
|
50
|
-
defineGetter('latestNotificationSnapshotUuid', () => undefined);
|
|
51
|
-
defineValue('onNewNotification', new Subject());
|
|
52
|
-
defineValue('onMotionDetected', new Subject());
|
|
53
|
-
defineValue('onDoorbellPressed', device.onDing);
|
|
54
|
-
defineValue('onBatteryLevel', device.onBatteryLevel);
|
|
55
|
-
defineValue('onInHomeDoorbellStatus', new Subject());
|
|
56
|
-
defineGetter('hasLowBattery', () => Boolean(device.data.alerts?.battery === 'low'));
|
|
57
|
-
defineGetter('isCharging', () => false);
|
|
58
|
-
defineValue('isDoorbot', true);
|
|
59
|
-
defineValue('hasLight', false);
|
|
60
|
-
defineValue('hasSiren', false);
|
|
61
|
-
defineValue('hasInHomeDoorbell', false);
|
|
62
|
-
defineValue('model', 'Ring Intercom Video');
|
|
63
|
-
defineValue('snapshotLifeTime', 55000); // camera stays active ~1 min after a ring
|
|
64
|
-
// Pre-warm support: start the WebRTC call the moment a ding is detected so
|
|
65
|
-
// the camera is already streaming when HKSV requests the recording.
|
|
66
|
-
let preWarmedCall = null;
|
|
67
|
-
let preWarmCleanup = null;
|
|
68
|
-
let lastSnapshotAt = 0;
|
|
69
|
-
const realStartLiveCall = () => RingCamera.prototype.startLiveCall.call(proxy);
|
|
70
|
-
defineValue('startLiveCall', () => {
|
|
71
|
-
if (preWarmedCall) {
|
|
72
|
-
const call = preWarmedCall;
|
|
73
|
-
preWarmedCall = null;
|
|
74
|
-
if (preWarmCleanup) {
|
|
75
|
-
clearTimeout(preWarmCleanup);
|
|
76
|
-
preWarmCleanup = null;
|
|
77
|
-
}
|
|
78
|
-
logInfo(`${device.name}: using pre-warmed camera session for recording`);
|
|
79
|
-
return call;
|
|
80
|
-
}
|
|
81
|
-
return realStartLiveCall();
|
|
82
|
-
});
|
|
83
|
-
defineValue('preWarmCamera', () => {
|
|
84
|
-
if (preWarmedCall)
|
|
85
|
-
return;
|
|
86
|
-
logInfo(`${device.name}: pre-warming camera connection`);
|
|
87
|
-
preWarmedCall = realStartLiveCall();
|
|
88
|
-
// Clean up if HKSV doesn't claim it within 20 seconds
|
|
89
|
-
preWarmCleanup = setTimeout(async () => {
|
|
90
|
-
if (preWarmedCall) {
|
|
91
|
-
try {
|
|
92
|
-
const session = await preWarmedCall;
|
|
93
|
-
session.stop();
|
|
94
|
-
}
|
|
95
|
-
catch (e) {
|
|
96
|
-
logError(`Failed to stop pre-warmed camera session for ${device.name}`);
|
|
97
|
-
logError(e);
|
|
98
|
-
}
|
|
99
|
-
finally {
|
|
100
|
-
preWarmedCall = null;
|
|
101
|
-
preWarmCleanup = null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}, 20000);
|
|
105
|
-
});
|
|
106
|
-
// Override getSnapshot: intercom uses the same doorbots snapshot endpoint as cameras.
|
|
107
|
-
defineGetter('hasSnapshotWithinLifetime', () => Date.now() - lastSnapshotAt < proxy.snapshotLifeTime);
|
|
108
|
-
defineValue('getSnapshot', async (_options) => {
|
|
109
|
-
try {
|
|
110
|
-
const snapshot = await device.restClient.request({
|
|
111
|
-
url: device.doorbotUrl('snapshot'),
|
|
112
|
-
responseType: 'buffer',
|
|
113
|
-
});
|
|
114
|
-
lastSnapshotAt = Date.now();
|
|
115
|
-
return snapshot;
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
throw new Error(`Snapshot not available for ${device.name}`);
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
return proxy;
|
|
122
|
-
}
|
|
4
|
+
import { map, throttleTime } from 'rxjs/operators';
|
|
123
5
|
export class Intercom extends BaseDataAccessory {
|
|
124
6
|
unlocking = false;
|
|
125
7
|
unlockTimeout;
|
|
@@ -131,64 +13,7 @@ export class Intercom extends BaseDataAccessory {
|
|
|
131
13
|
this.device = device;
|
|
132
14
|
this.accessory = accessory;
|
|
133
15
|
this.config = config;
|
|
134
|
-
const { Characteristic, Service } = hap,
|
|
135
|
-
// Set up camera streaming for Ring Intercom Video before other services
|
|
136
|
-
let intercomVideoProxy = null;
|
|
137
|
-
if (isIntercomVideo) {
|
|
138
|
-
const cameraProxy = createIntercomVideoProxy(device);
|
|
139
|
-
intercomVideoProxy = cameraProxy;
|
|
140
|
-
const cameraSource = new CameraSource(cameraProxy, config);
|
|
141
|
-
accessory.configureController(cameraSource.controller);
|
|
142
|
-
this.registerCharacteristic({
|
|
143
|
-
characteristicType: Characteristic.Mute,
|
|
144
|
-
serviceType: Service.Microphone,
|
|
145
|
-
getValue: () => false,
|
|
146
|
-
});
|
|
147
|
-
this.registerCharacteristic({
|
|
148
|
-
characteristicType: Characteristic.Mute,
|
|
149
|
-
serviceType: Service.Speaker,
|
|
150
|
-
getValue: () => false,
|
|
151
|
-
});
|
|
152
|
-
logInfo(`Camera streaming enabled for Ring Intercom Video: ${device.name}`);
|
|
153
|
-
}
|
|
154
|
-
const lockService = this.getService(Service.LockMechanism), { LockCurrentState, LockTargetState, ProgrammableSwitchEvent } = Characteristic,
|
|
155
|
-
// Ring Intercom Video ding events may arrive as camera-style FCM push
|
|
156
|
-
// notifications (different category than RingIntercom.onDing expects),
|
|
157
|
-
// so onDing never fires. Poll Ring's active-dings endpoint as a reliable
|
|
158
|
-
// fallback — it reflects the current ring in real time (unlike the history
|
|
159
|
-
// endpoint which can lag by ~60 seconds).
|
|
160
|
-
onDingDetected = (() => {
|
|
161
|
-
if (!isIntercomVideo) {
|
|
162
|
-
return device.onDing.pipe(share());
|
|
163
|
-
}
|
|
164
|
-
let lastDingId;
|
|
165
|
-
const polled = interval(3000).pipe(switchMap(async () => {
|
|
166
|
-
try {
|
|
167
|
-
const active = await device.restClient.request({
|
|
168
|
-
url: 'https://api.ring.com/clients_api/dings/active',
|
|
169
|
-
});
|
|
170
|
-
return Array.isArray(active) ? active : [];
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
return [];
|
|
174
|
-
}
|
|
175
|
-
}), filter((active) => {
|
|
176
|
-
const ding = active.find((d) => String(d.doorbot_id) === String(device.id) &&
|
|
177
|
-
d.kind === 'ding');
|
|
178
|
-
if (!ding)
|
|
179
|
-
return false;
|
|
180
|
-
if (String(ding.id) === lastDingId)
|
|
181
|
-
return false;
|
|
182
|
-
lastDingId = String(ding.id);
|
|
183
|
-
return true;
|
|
184
|
-
}), map(() => undefined));
|
|
185
|
-
// share() makes this hot: one polling interval, one lastDingId,
|
|
186
|
-
// emission fans out to all subscribers that rely on ding events.
|
|
187
|
-
return merge(device.onDing, polled).pipe(throttleTime(3000), share());
|
|
188
|
-
})(), onDoorbellPressed = onDingDetected.pipe(throttleTime(15000), map(() => {
|
|
189
|
-
logInfo(`Doorbell pressed on ${device.name} — sending HomeKit event`);
|
|
190
|
-
return ProgrammableSwitchEvent.SINGLE_PRESS;
|
|
191
|
-
})), syncLockState = () => {
|
|
16
|
+
const { Characteristic, Service } = hap, lockService = this.getService(Service.LockMechanism), { LockCurrentState, LockTargetState, ProgrammableSwitchEvent } = Characteristic, programableSwitchService = this.getService(Service.StatelessProgrammableSwitch), onDoorbellPressed = device.onDing.pipe(throttleTime(15000), map(() => ProgrammableSwitchEvent.SINGLE_PRESS)), syncLockState = () => {
|
|
192
17
|
const state = this.getLockState();
|
|
193
18
|
lockService
|
|
194
19
|
.getCharacteristic(Characteristic.LockCurrentState)
|
|
@@ -243,42 +68,25 @@ export class Intercom extends BaseDataAccessory {
|
|
|
243
68
|
}
|
|
244
69
|
},
|
|
245
70
|
});
|
|
246
|
-
|
|
247
|
-
// the camera controller owns the primary-service role so HomeKit routes
|
|
248
|
-
// doorbell events (and HomePod chimes) correctly.
|
|
249
|
-
if (!isIntercomVideo) {
|
|
250
|
-
lockService.setPrimaryService(true);
|
|
251
|
-
}
|
|
252
|
-
// Pre-warm the camera WebRTC connection on every ding so HKSV recording
|
|
253
|
-
// gets live video instead of black frames.
|
|
254
|
-
if (intercomVideoProxy) {
|
|
255
|
-
const proxy = intercomVideoProxy;
|
|
256
|
-
onDingDetected.subscribe(() => {
|
|
257
|
-
;
|
|
258
|
-
proxy.preWarmCamera?.();
|
|
259
|
-
});
|
|
260
|
-
}
|
|
71
|
+
lockService.setPrimaryService(true);
|
|
261
72
|
// Doorbell Service
|
|
262
73
|
this.registerObservableCharacteristic({
|
|
263
74
|
characteristicType: ProgrammableSwitchEvent,
|
|
264
75
|
serviceType: Service.Doorbell,
|
|
265
76
|
onValue: onDoorbellPressed,
|
|
266
77
|
});
|
|
267
|
-
// Programmable Switch Service
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
maxValue: ProgrammableSwitchEvent.SINGLE_PRESS,
|
|
280
|
-
});
|
|
281
|
-
}
|
|
78
|
+
// Programmable Switch Service
|
|
79
|
+
this.registerObservableCharacteristic({
|
|
80
|
+
characteristicType: ProgrammableSwitchEvent,
|
|
81
|
+
serviceType: programableSwitchService,
|
|
82
|
+
onValue: onDoorbellPressed,
|
|
83
|
+
});
|
|
84
|
+
// Hide long and double press events by setting max value
|
|
85
|
+
programableSwitchService
|
|
86
|
+
.getCharacteristic(ProgrammableSwitchEvent)
|
|
87
|
+
.setProps({
|
|
88
|
+
maxValue: ProgrammableSwitchEvent.SINGLE_PRESS,
|
|
89
|
+
});
|
|
282
90
|
// Battery Service
|
|
283
91
|
if (device.batteryLevel !== null) {
|
|
284
92
|
this.registerObservableCharacteristic({
|
|
@@ -299,7 +107,7 @@ export class Intercom extends BaseDataAccessory {
|
|
|
299
107
|
this.registerCharacteristic({
|
|
300
108
|
characteristicType: Characteristic.Model,
|
|
301
109
|
serviceType: Service.AccessoryInformation,
|
|
302
|
-
getValue: () =>
|
|
110
|
+
getValue: () => 'Intercom Handset Audio',
|
|
303
111
|
});
|
|
304
112
|
this.registerCharacteristic({
|
|
305
113
|
characteristicType: Characteristic.SerialNumber,
|
package/lib/ring-platform.js
CHANGED
|
@@ -170,17 +170,10 @@ export class RingPlatform {
|
|
|
170
170
|
});
|
|
171
171
|
await Promise.all(locations.map(async (location) => {
|
|
172
172
|
const devices = await location.getDevices(), { cameras, chimes, intercoms } = location, allDevices = [...devices, ...cameras, ...chimes, ...intercoms], securityPanel = devices.find((x) => x.deviceType === RingDeviceType.SecurityPanel), debugPrefix = debug ? 'TEST ' : '', hapDevices = allDevices.map((device) => {
|
|
173
|
-
const isCamera = device instanceof RingCamera,
|
|
174
|
-
device.deviceType === 'intercom_handset_video', cameraIdentitySalt = isCamera && config.externalCameraIdSalt
|
|
173
|
+
const isCamera = device instanceof RingCamera, cameraIdentitySalt = isCamera && config.externalCameraIdSalt
|
|
175
174
|
? `-${config.externalCameraIdSalt}`
|
|
176
|
-
: '',
|
|
177
|
-
|
|
178
|
-
// 'intercam' gives Ring Intercom Video a distinct external-accessory UUID
|
|
179
|
-
cameraIdDifferentiator = isCamera
|
|
180
|
-
? 'camera'
|
|
181
|
-
: isIntercomVideo
|
|
182
|
-
? 'intercam'
|
|
183
|
-
: '', AccessoryClass = (device instanceof RingCamera
|
|
175
|
+
: '', cameraIdDifferentiator = isCamera ? 'camera' : '', // this forces bridged cameras from old version of the plugin to be seen as "stale"
|
|
176
|
+
AccessoryClass = (device instanceof RingCamera
|
|
184
177
|
? Camera
|
|
185
178
|
: device instanceof RingChime
|
|
186
179
|
? Chime
|
|
@@ -191,7 +184,6 @@ export class RingPlatform {
|
|
|
191
184
|
deviceType: device.deviceType,
|
|
192
185
|
device: device,
|
|
193
186
|
isCamera,
|
|
194
|
-
isIntercomVideo,
|
|
195
187
|
id: device.id.toString() +
|
|
196
188
|
cameraIdDifferentiator +
|
|
197
189
|
cameraIdentitySalt +
|
|
@@ -207,7 +199,6 @@ export class RingPlatform {
|
|
|
207
199
|
deviceType: securityPanel.deviceType,
|
|
208
200
|
device: securityPanel,
|
|
209
201
|
isCamera: false,
|
|
210
|
-
isIntercomVideo: false,
|
|
211
202
|
id: securityPanel.id.toString() + 'panic' + accessoryIdSuffix,
|
|
212
203
|
name: 'Panic Buttons' + accessoryNameSuffix,
|
|
213
204
|
AccessoryClass: PanicButtons,
|
|
@@ -219,14 +210,13 @@ export class RingPlatform {
|
|
|
219
210
|
deviceType: 'location.mode',
|
|
220
211
|
device: location,
|
|
221
212
|
isCamera: false,
|
|
222
|
-
isIntercomVideo: false,
|
|
223
213
|
id: location.id + 'mode' + accessoryIdSuffix,
|
|
224
214
|
name: location.name + ' Mode' + accessoryNameSuffix,
|
|
225
215
|
AccessoryClass: LocationModeSwitch,
|
|
226
216
|
});
|
|
227
217
|
}
|
|
228
218
|
logInfo(`Configuring ${cameras.length} cameras and ${hapDevices.length} devices for location "${location.name}" - locationId: ${location.id}`);
|
|
229
|
-
hapDevices.forEach(({ deviceType, device, isCamera,
|
|
219
|
+
hapDevices.forEach(({ deviceType, device, isCamera, id, name, AccessoryClass }) => {
|
|
230
220
|
const uuid = hap.uuid.generate(debugPrefix + id), displayName = debugPrefix + name;
|
|
231
221
|
if (!AccessoryClass ||
|
|
232
222
|
(config.hideLightGroups &&
|
|
@@ -247,20 +237,13 @@ export class RingPlatform {
|
|
|
247
237
|
delete this.homebridgeAccessories[uuid];
|
|
248
238
|
}
|
|
249
239
|
const createHomebridgeAccessory = () => {
|
|
250
|
-
const
|
|
240
|
+
const accessory = new api.platformAccessory(displayName, uuid, isCamera
|
|
251
241
|
? 17 /* hap.Categories.CAMERA */
|
|
252
|
-
:
|
|
253
|
-
? 18 /* hap.Categories.VIDEO_DOORBELL */
|
|
254
|
-
: 11 /* hap.Categories.SECURITY_SYSTEM */;
|
|
255
|
-
const accessory = new api.platformAccessory(displayName, uuid, category);
|
|
242
|
+
: 11 /* hap.Categories.SECURITY_SYSTEM */);
|
|
256
243
|
if (isCamera) {
|
|
257
244
|
logInfo(`Configured camera ${uuid} ${deviceType} ${displayName}`);
|
|
258
245
|
externalAccessories.push(accessory);
|
|
259
246
|
}
|
|
260
|
-
else if (isIntercomVideo) {
|
|
261
|
-
logInfo(`Configured Ring Intercom Video ${uuid} ${deviceType} ${displayName}`);
|
|
262
|
-
externalAccessories.push(accessory);
|
|
263
|
-
}
|
|
264
247
|
else {
|
|
265
248
|
logInfo(`Adding new accessory ${uuid} ${deviceType} ${displayName}`);
|
|
266
249
|
platformAccessories.push(accessory);
|
package/package.json
CHANGED