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.
@@ -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 && e.message.includes('Snapshot')) {
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 { filter, map, share, switchMap, throttleTime } from 'rxjs/operators';
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, isIntercomVideo = device.deviceType === 'intercom_handset_video';
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
- // For audio-only intercom the lock is the main function; for video intercom
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 — only for audio-only intercom; the video
268
- // variant already exposes Service.Doorbell which covers this.
269
- if (!isIntercomVideo) {
270
- const programableSwitchService = this.getService(Service.StatelessProgrammableSwitch);
271
- this.registerObservableCharacteristic({
272
- characteristicType: ProgrammableSwitchEvent,
273
- serviceType: programableSwitchService,
274
- onValue: onDoorbellPressed,
275
- });
276
- programableSwitchService
277
- .getCharacteristic(ProgrammableSwitchEvent)
278
- .setProps({
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: () => isIntercomVideo ? 'Ring Intercom Video' : 'Ring Intercom',
110
+ getValue: () => 'Intercom Handset Audio',
303
111
  });
304
112
  this.registerCharacteristic({
305
113
  characteristicType: Characteristic.SerialNumber,
@@ -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, isIntercomVideo = device instanceof RingIntercom &&
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
- // 'camera' forces bridged cameras from old plugin version to be seen as "stale"
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, isIntercomVideo, id, name, AccessoryClass, }) => {
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 category = isCamera
240
+ const accessory = new api.platformAccessory(displayName, uuid, isCamera
251
241
  ? 17 /* hap.Categories.CAMERA */
252
- : isIntercomVideo
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-ring-hksv",
3
3
  "displayName": "Homebridge Ring HKSV",
4
- "version": "14.3.6",
4
+ "version": "14.3.7",
5
5
  "description": "Homebridge plugin for Ring devices with HomeKit Secure Video support",
6
6
  "type": "module",
7
7
  "main": "./lib/index.js",