From bf00ba0adc4de4f2186986cb2e3735b0013599ad Mon Sep 17 00:00:00 2001 From: Alex Leeds Date: Thu, 16 Mar 2023 21:32:20 -0400 Subject: [PATCH] ring: add support for locks (#634) --- plugins/ring/README.md | 13 +++- plugins/ring/package-lock.json | 14 ++-- plugins/ring/package.json | 4 +- plugins/ring/src/main.ts | 133 +++++++++++++++++++++++---------- 4 files changed, 115 insertions(+), 49 deletions(-) diff --git a/plugins/ring/README.md b/plugins/ring/README.md index 02c092092d..cfb4c663dd 100644 --- a/plugins/ring/README.md +++ b/plugins/ring/README.md @@ -9,7 +9,9 @@ Do not enable prebuffer on Ring cameras and doorbells. * The persistent live stream will drain the battery faster than it can charge. * The persistent live stream will also count against ISP bandwidth limits. -## Supported Cameras +## Supported Devices + +### Cameras - Ring Video Doorbell Wired, Pro, Pro 2, 4, 3, 2nd Gen - Ring Floodlight Cam Wired Plus - Ring Floodlight Cam Wired Pro @@ -17,6 +19,15 @@ Do not enable prebuffer on Ring cameras and doorbells. - Ring Indoor Cam - Ring Stick-Up Cam (Wired and Battery) +### Other Devices +- Security Panel +- Location Modes +- Contact Sensor / Retrofit Alarm Zones / Tilt Sensor +- Motion Sensor +- Flood / Freeze Sensor +- Water Sensor +- Smart Locks + ## Problems and Solutions I can see artifacts in HKSV recordings diff --git a/plugins/ring/package-lock.json b/plugins/ring/package-lock.json index 56ed45eda9..b49c5477b5 100644 --- a/plugins/ring/package-lock.json +++ b/plugins/ring/package-lock.json @@ -1,17 +1,17 @@ { "name": "@scrypted/ring", - "version": "0.0.98", + "version": "0.0.99", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@scrypted/ring", - "version": "0.0.98", + "version": "0.0.99", "dependencies": { "@koush/ring-client-api": "file:../../external/ring-client-api", "@scrypted/common": "file:../../common", "@scrypted/sdk": "file:../../sdk", - "@types/node": "^18.14.5", + "@types/node": "^18.15.3", "axios": "^1.3.4", "rxjs": "^7.8.0" }, @@ -49,7 +49,7 @@ }, "../../sdk": { "name": "@scrypted/sdk", - "version": "0.2.82", + "version": "0.2.85", "license": "ISC", "dependencies": { "@babel/preset-typescript": "^7.18.6", @@ -148,9 +148,9 @@ } }, "node_modules/@types/node": { - "version": "18.14.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.5.tgz", - "integrity": "sha512-CRT4tMK/DHYhw1fcCEBwME9CSaZNclxfzVMe7GsO6ULSwsttbj70wSiX6rZdIjGblu93sTJxLdhNIT85KKI7Qw==" + "version": "18.15.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.3.tgz", + "integrity": "sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==" }, "node_modules/@types/responselike": { "version": "1.0.0", diff --git a/plugins/ring/package.json b/plugins/ring/package.json index ff9c30d099..00e1541e7b 100644 --- a/plugins/ring/package.json +++ b/plugins/ring/package.json @@ -36,7 +36,7 @@ "@koush/ring-client-api": "file:../../external/ring-client-api", "@scrypted/common": "file:../../common", "@scrypted/sdk": "file:../../sdk", - "@types/node": "^18.14.5", + "@types/node": "^18.15.3", "axios": "^1.3.4", "rxjs": "^7.8.0" }, @@ -44,5 +44,5 @@ "got": "11.8.6", "socket.io-client": "^2.5.0" }, - "version": "0.0.98" + "version": "0.0.99" } diff --git a/plugins/ring/src/main.ts b/plugins/ring/src/main.ts index a57f2faa20..317bc50007 100644 --- a/plugins/ring/src/main.ts +++ b/plugins/ring/src/main.ts @@ -3,15 +3,15 @@ import { RefreshPromise } from "@scrypted/common/src/promise-utils"; import { connectRTCSignalingClients } from '@scrypted/common/src/rtc-signaling'; import { RtspServer } from '@scrypted/common/src/rtsp-server'; import { addTrackControls, parseSdp, replacePorts } from '@scrypted/common/src/sdp-utils'; +import sdk, { Battery, BinarySensor, Camera, Device, DeviceProvider, EntrySensor, FFmpegInput, FloodSensor, Lock, LockState, MediaObject, MediaStreamUrl, MotionSensor, OnOff, PictureOptions, RequestMediaStreamOptions, RequestPictureOptions, ResponseMediaStreamOptions, RTCAVSignalingSetup, RTCSessionControl, RTCSignalingChannel, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, SecuritySystem, SecuritySystemMode, Setting, Settings, SettingValue, TamperSensor, VideoCamera } from '@scrypted/sdk'; import { StorageSettings } from '@scrypted/sdk/storage-settings'; -import sdk, { Battery, BinarySensor, Camera, Device, DeviceProvider, EntrySensor, FFmpegInput, FloodSensor, MediaObject, MediaStreamUrl, MotionSensor, OnOff, PictureOptions, RequestMediaStreamOptions, RequestPictureOptions, ResponseMediaStreamOptions, RTCAVSignalingSetup, RTCSessionControl, RTCSignalingChannel, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, SecuritySystem, SecuritySystemMode, Setting, Settings, SettingValue, TamperSensor, VideoCamera } from '@scrypted/sdk'; import child_process, { ChildProcess } from 'child_process'; import dgram from 'dgram'; import { RtcpReceiverInfo, RtcpRrPacket } from '../../../external/werift/packages/rtp/src/rtcp/rr'; import { RtpPacket } from '../../../external/werift/packages/rtp/src/rtp/rtp'; import { ProtectionProfileAes128CmHmacSha1_80 } from '../../../external/werift/packages/rtp/src/srtp/const'; import { SrtcpSession } from '../../../external/werift/packages/rtp/src/srtp/srtcp'; -import { Location, LocationMode, RingDevice, isStunMessage, RtpDescription, SipSession, BasicPeerConnection, CameraData, clientApi, generateUuid, RingBaseApi, RingRestClient, rxjs, SimpleWebRtcSession, StreamingSession, RingDeviceType, RingDeviceData } from './ring-client-api'; +import { BasicPeerConnection, CameraData, clientApi, generateUuid, isStunMessage, Location, LocationMode, RingBaseApi, RingDevice, RingDeviceData, RingDeviceType, RingRestClient, RtpDescription, rxjs, SimpleWebRtcSession, SipSession, StreamingSession } from './ring-client-api'; import { encodeSrtpOptions, getPayloadType, getSequenceNumber, isRtpMessagePayloadType } from './srtp-utils'; const STREAM_TIMEOUT = 120000; @@ -661,7 +661,51 @@ class RingCameraDevice extends ScryptedDeviceBase implements DeviceProvider, Cam } } +class RingLock extends ScryptedDeviceBase implements Battery, Lock { + device: RingDevice + + constructor(nativeId: string, device: RingDevice) { + super(nativeId); + this.device = device; + device.onData.subscribe(async (data: RingDeviceData) => { + this.updateState(data); + }); + } + + async lock(): Promise { + return this.device.sendCommand('lock.lock'); + } + + async unlock(): Promise { + return this.device.sendCommand('lock.unlock'); + } + + updateState(data: RingDeviceData) { + this.batteryLevel = data.batteryLevel; + switch (data.locked) { + case 'locked': + this.lockState = LockState.Locked; + break; + case 'unlocked': + this.lockState = LockState.Unlocked; + break; + case 'jammed': + this.lockState = LockState.Jammed; + break; + default: + this.lockState = undefined; + } + } +} + class RingSensor extends ScryptedDeviceBase implements TamperSensor, Battery, EntrySensor, MotionSensor, FloodSensor { + constructor(nativeId: string, device: RingDevice) { + super(nativeId); + device.onData.subscribe(async (data: RingDeviceData) => { + this.updateState(data); + }); + } + updateState(data: RingDeviceData) { this.tampered = data.tamperStatus === 'tamper'; this.batteryLevel = data.batteryLevel; @@ -673,6 +717,7 @@ class RingSensor extends ScryptedDeviceBase implements TamperSensor, Battery, En export class RingLocationDevice extends ScryptedDeviceBase implements DeviceProvider, SecuritySystem { devices = new Map(); + locationDevices = new Map(); constructor(public plugin: RingPlugin, nativeId: string) { super(nativeId); @@ -762,11 +807,21 @@ export class RingLocationDevice extends ScryptedDeviceBase implements DeviceProv return this.plugin.locations.find(l => l.id === this.nativeId); } + async findRingDeviceAtLocation(id: string): Promise { + const location = this.findLocation(); + return (await location.getDevices()).find((x) => x.id === id); + } + async getDevice(nativeId: string) { if (!this.devices.has(nativeId)) { if (nativeId.endsWith('-sensor')) { - const sensor = new RingSensor(nativeId); - this.devices.set(nativeId, sensor); + const ringRevice = await this.findRingDeviceAtLocation(nativeId.replace('-sensor', '')); + const device = new RingSensor(nativeId, ringRevice); + this.devices.set(nativeId, device); + } else if (nativeId.endsWith('-lock')) { + const ringRevice = await this.findRingDeviceAtLocation(nativeId.replace('-lock', '')); + const device = new RingLock(nativeId, ringRevice); + this.devices.set(nativeId, device); } else { const camera = new RingCameraDevice(this.plugin, this, nativeId); this.devices.set(nativeId, camera); @@ -1028,41 +1083,52 @@ class RingPlugin extends ScryptedDeviceBase implements DeviceProvider, Settings }); } - const sensors = (await location.getDevices()).filter(x => { - const supportedSensors = [ - RingDeviceType.ContactSensor, - RingDeviceType.RetrofitZone, - RingDeviceType.TiltSensor, - RingDeviceType.MotionSensor, - RingDeviceType.FloodFreezeSensor, - RingDeviceType.WaterSensor, - ] - return x.data.status !== 'disabled' && (supportedSensors.includes(x.data.deviceType)) - }); - for (const sensor of sensors) { - const nativeId = sensor.id.toString() + '-sensor'; - const data: RingDeviceData = sensor.data; + // add location devices + const locationDevices = await location.getDevices(); + for (const locationDevice of locationDevices) { + const data: RingDeviceData = locationDevice.data; + let nativeId: string; + let type: ScryptedDeviceType; + let interfaces: ScryptedInterface[] = []; - const interfaces = [ScryptedInterface.TamperSensor]; - switch (data.deviceType){ + if (data.status === 'disabled') { + continue; + } + + switch (data.deviceType) { case RingDeviceType.ContactSensor: case RingDeviceType.RetrofitZone: case RingDeviceType.TiltSensor: - interfaces.push(ScryptedInterface.EntrySensor); + nativeId = locationDevice.id.toString() + '-sensor'; + type = ScryptedDeviceType.Sensor + interfaces.push(ScryptedInterface.TamperSensor, ScryptedInterface.EntrySensor); break; case RingDeviceType.MotionSensor: - interfaces.push(ScryptedInterface.MotionSensor); + nativeId = locationDevice.id.toString() + '-sensor'; + type = ScryptedDeviceType.Sensor + interfaces.push(ScryptedInterface.TamperSensor, ScryptedInterface.MotionSensor); break; case RingDeviceType.FloodFreezeSensor: case RingDeviceType.WaterSensor: - interfaces.push(ScryptedInterface.FloodSensor); + nativeId = locationDevice.id.toString() + '-sensor'; + type = ScryptedDeviceType.Sensor + interfaces.push(ScryptedInterface.TamperSensor, ScryptedInterface.FloodSensor); break; - default: break; + default: + if (/^lock($|\.)/.test(data.deviceType)) { + nativeId = locationDevice.id.toString() + '-lock'; + type = ScryptedDeviceType.Lock + interfaces.push(ScryptedInterface.Lock); + break; + } else { + this.console.debug(`discovered and ignoring unsupported '${locationDevice.deviceType}' device: '${locationDevice.name}'`) + continue; + } } - + if (data.batteryStatus !== 'none') interfaces.push(ScryptedInterface.Battery); - + const device: Device = { info: { model: data.deviceType, @@ -1071,22 +1137,11 @@ class RingPlugin extends ScryptedDeviceBase implements DeviceProvider, Settings }, providerNativeId: location.id, nativeId: nativeId, - name: sensor.name, - type: ScryptedDeviceType.Sensor, + name: locationDevice.name, + type: type, interfaces, }; devices.push(device); - - const getScryptedDevice = async () => { - const locationDevice = await this.getDevice(location.id); - const scryptedDevice = await locationDevice?.getDevice(nativeId); - return scryptedDevice as RingSensor; - } - - sensor.onData.subscribe(async (data: RingDeviceData) => { - const scryptedDevice = await getScryptedDevice(); - scryptedDevice?.updateState(data) - }); } await deviceManager.onDevicesChanged({