Skip to content

Commit

Permalink
ring: add support for locks (#634)
Browse files Browse the repository at this point in the history
  • Loading branch information
leedsalex authored Mar 17, 2023
1 parent 544dfb3 commit bf00ba0
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 49 deletions.
13 changes: 12 additions & 1 deletion plugins/ring/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,25 @@ 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
- Ring Spotlight Cam (Wired and Battery)
- 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
Expand Down
14 changes: 7 additions & 7 deletions plugins/ring/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions plugins/ring/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@
"@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"
},
"optionalDependencies": {
"got": "11.8.6",
"socket.io-client": "^2.5.0"
},
"version": "0.0.98"
"version": "0.0.99"
}
133 changes: 94 additions & 39 deletions plugins/ring/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<void> {
return this.device.sendCommand('lock.lock');
}

async unlock(): Promise<void> {
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;
Expand All @@ -673,6 +717,7 @@ class RingSensor extends ScryptedDeviceBase implements TamperSensor, Battery, En

export class RingLocationDevice extends ScryptedDeviceBase implements DeviceProvider, SecuritySystem {
devices = new Map<string, any>();
locationDevices = new Map<string, RingDevice>();

constructor(public plugin: RingPlugin, nativeId: string) {
super(nativeId);
Expand Down Expand Up @@ -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<RingDevice> {
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);
Expand Down Expand Up @@ -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,
Expand All @@ -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({
Expand Down

0 comments on commit bf00ba0

Please sign in to comment.