From fac67696a9cff2af1d5bca990f73f662c7cdfc9f Mon Sep 17 00:00:00 2001 From: Alex Leeds Date: Sun, 19 Mar 2023 17:59:14 -0400 Subject: [PATCH] ring: add lights, switches & outlets (#642) --- plugins/ring/README.md | 2 + plugins/ring/package-lock.json | 4 +- plugins/ring/package.json | 2 +- plugins/ring/src/location.ts | 127 ++++++++++++++++++++++++++-- plugins/ring/src/ring-client-api.ts | 2 +- 5 files changed, 125 insertions(+), 12 deletions(-) diff --git a/plugins/ring/README.md b/plugins/ring/README.md index 6f3fc81aa3..3749559283 100644 --- a/plugins/ring/README.md +++ b/plugins/ring/README.md @@ -32,6 +32,8 @@ Do not enable prebuffer on Ring cameras and doorbells. - Water Sensor - Mailbox Sensor - Smart Locks +- Ring Smart Lights (Flood/Path/Step/Spot Lights, Bulbs, Transformer) +- Lights, Switches & Outlets ## Problems and Solutions diff --git a/plugins/ring/package-lock.json b/plugins/ring/package-lock.json index 45f59b0945..594549ea39 100644 --- a/plugins/ring/package-lock.json +++ b/plugins/ring/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/ring", - "version": "0.0.103", + "version": "0.0.104", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@scrypted/ring", - "version": "0.0.103", + "version": "0.0.104", "dependencies": { "@koush/ring-client-api": "file:../../external/ring-client-api", "@scrypted/common": "file:../../common", diff --git a/plugins/ring/package.json b/plugins/ring/package.json index bcd38cfb58..c336d5ad8c 100644 --- a/plugins/ring/package.json +++ b/plugins/ring/package.json @@ -44,5 +44,5 @@ "got": "11.8.6", "socket.io-client": "^2.5.0" }, - "version": "0.0.103" + "version": "0.0.104" } diff --git a/plugins/ring/src/location.ts b/plugins/ring/src/location.ts index fba56f44c8..8eda7da15b 100644 --- a/plugins/ring/src/location.ts +++ b/plugins/ring/src/location.ts @@ -1,7 +1,7 @@ -import sdk, { Battery, Device, DeviceProvider, EntrySensor, FloodSensor, Lock, LockState, MotionSensor, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, SecuritySystem, SecuritySystemMode, TamperSensor } from '@scrypted/sdk'; +import sdk, { Battery, Brightness, Device, DeviceProvider, EntrySensor, FloodSensor, Lock, LockState, MotionSensor, OnOff, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, SecuritySystem, SecuritySystemMode, TamperSensor } from '@scrypted/sdk'; import { RingCameraDevice } from './camera'; import RingPlugin from './main'; -import { Location, LocationMode, RingCamera, RingDevice, RingDeviceData, RingDeviceType } from './ring-client-api'; +import { Location, LocationMode, RingCamera, RingDevice, RingDeviceCategory, RingDeviceData, RingDeviceType } from './ring-client-api'; const { deviceManager } = sdk; @@ -11,6 +11,8 @@ class RingLock extends ScryptedDeviceBase implements Battery, Lock { constructor(nativeId: string, device: RingDevice) { super(nativeId); this.device = device; + this.updateState(device.data); + device.onData.subscribe(async (data: RingDeviceData) => { this.updateState(data); }); @@ -42,7 +44,7 @@ class RingLock extends ScryptedDeviceBase implements Battery, Lock { } } -class RingSensor extends ScryptedDeviceBase implements TamperSensor, Battery, EntrySensor, MotionSensor, FloodSensor { +class RingLight extends ScryptedDeviceBase implements Battery, TamperSensor, MotionSensor, OnOff, Brightness { device: RingDevice; data: RingDeviceData; @@ -58,16 +60,93 @@ class RingSensor extends ScryptedDeviceBase implements TamperSensor, Battery, En }); } + private isBeamDevice() { + return [RingDeviceType.BeamsMultiLevelSwitch, RingDeviceType.BeamsSwitch, RingDeviceType.BeamsTransformerSwitch].includes(this.device.deviceType); + } + updateState(data: RingDeviceData) { + this.batteryLevel = data.batteryLevel; this.tampered = data.tamperStatus === 'tamper'; + this.motionDetected = data.motionStatus === 'faulted'; + this.on = data.on; + this.brightness = data.level && !isNaN(data.level) ? 100 * data.level : 0; + } + + turnOff(): Promise { + if (this.isBeamDevice()) { + this.device.sendCommand('light-mode.set', { lightMode: 'default' }); + return; + } else { + return this.device.setInfo({ device: { v1: { on: false } } }); + } + } + + turnOn(): Promise { + if (this.isBeamDevice()) { + this.device.sendCommand('light-mode.set', { lightMode: 'on' }); + return; + } else { + return this.device.setInfo({ device: { v1: { on: true } } }); + } + } + + setBrightness(brightness: number): Promise { + return this.device.setInfo({ + device: { v1: { level: brightness / 100 } }, + }); + } +} + +class RingSwitch extends ScryptedDeviceBase implements OnOff { + device: RingDevice; + data: RingDeviceData; + + constructor(nativeId: string, device: RingDevice) { + super(nativeId); + this.device = device; + this.updateState(device.data); + + device.onData.subscribe(async (data: RingDeviceData) => { + this.updateState(data); + }); + } + + updateState(data: RingDeviceData) { + this.on = data.on; + } + + turnOff(): Promise { + return this.device.setInfo({ device: { v1: { on: false } } }); + } + + turnOn(): Promise { + return this.device.setInfo({ device: { v1: { on: true } } }); + } +} + +class RingSensor extends ScryptedDeviceBase implements TamperSensor, Battery, EntrySensor, MotionSensor, FloodSensor { + device: RingDevice; + + constructor(nativeId: string, device: RingDevice) { + super(nativeId); + this.device = device; + this.updateState(device.data); + + device.onData.subscribe(async (data: RingDeviceData) => { + this.updateState(data); + }); + } + + updateState(data: RingDeviceData) { this.batteryLevel = data.batteryLevel; + this.tampered = data.tamperStatus === 'tamper'; this.entryOpen = data.faulted; this.motionDetected = this.device.deviceType === RingDeviceType.BeamsMotionSensor ? data.motionStatus === 'faulted' : data.faulted; this.flooded = data.flood?.faulted || data.faulted; } isBypassable() { - return (this.device.deviceType === RingDeviceType.ContactSensor || this.device.deviceType === RingDeviceType.RetrofitZone) && this.data.faulted; + return (this.device.deviceType === RingDeviceType.ContactSensor || this.device.deviceType === RingDeviceType.RetrofitZone) && this.device.data.faulted; } } @@ -168,25 +247,51 @@ export class RingLocationDevice extends ScryptedDeviceBase implements DeviceProv case RingDeviceType.TiltSensor: case RingDeviceType.GlassbreakSensor: nativeId = locationDevice.id.toString() + '-sensor'; - type = ScryptedDeviceType.Sensor + type = ScryptedDeviceType.Sensor; interfaces.push(ScryptedInterface.TamperSensor, ScryptedInterface.EntrySensor); break; case RingDeviceType.MotionSensor: case RingDeviceType.BeamsMotionSensor: nativeId = locationDevice.id.toString() + '-sensor'; - type = ScryptedDeviceType.Sensor + type = ScryptedDeviceType.Sensor; interfaces.push(ScryptedInterface.TamperSensor, ScryptedInterface.MotionSensor); break; case RingDeviceType.FloodFreezeSensor: case RingDeviceType.WaterSensor: nativeId = locationDevice.id.toString() + '-sensor'; - type = ScryptedDeviceType.Sensor + type = ScryptedDeviceType.Sensor; interfaces.push(ScryptedInterface.TamperSensor, ScryptedInterface.FloodSensor); break; + case RingDeviceType.BeamsMultiLevelSwitch: + case RingDeviceType.BeamsSwitch: + case RingDeviceType.BeamsTransformerSwitch: + case RingDeviceType.MultiLevelBulb: + nativeId = locationDevice.id.toString() + '-light'; + type = ScryptedDeviceType.Light; + interfaces.push(ScryptedInterface.OnOff); + if (data.level !== undefined) + interfaces.push(ScryptedInterface.Brightness) + if (data.motionStatus !== undefined && !!data.motionSensorEnabled) + interfaces.push(ScryptedInterface.TamperSensor, ScryptedInterface.MotionSensor); + break; + case RingDeviceType.MultiLevelSwitch: + if (data.categoryId === RingDeviceCategory.Lights) { + nativeId = locationDevice.id.toString() + '-light'; + type = ScryptedDeviceType.Light; + interfaces.push(ScryptedInterface.OnOff); + if (data.level !== undefined) + interfaces.push(ScryptedInterface.Brightness) + break; + } + case RingDeviceType.Switch: + nativeId = locationDevice.id.toString() + '-switch'; + type = data.categoryId === RingDeviceCategory.Outlets ? ScryptedDeviceType.Outlet : ScryptedDeviceType.Switch; + interfaces.push(ScryptedInterface.OnOff); + break; default: if (/^lock($|\.)/.test(data.deviceType)) { nativeId = locationDevice.id.toString() + '-lock'; - type = ScryptedDeviceType.Lock + type = ScryptedDeviceType.Lock; interfaces.push(ScryptedInterface.Lock); break; } else { @@ -228,6 +333,12 @@ export class RingLocationDevice extends ScryptedDeviceBase implements DeviceProv } else if (nativeId.endsWith('-lock')) { const device = new RingLock(nativeId, this.locationDevices.get(nativeId) as RingDevice); this.devices.set(nativeId, device); + } else if (nativeId.endsWith('-light')) { + const device = new RingLight(nativeId, this.locationDevices.get(nativeId) as RingDevice); + this.devices.set(nativeId, device); + } else if (nativeId.endsWith('-switch')) { + const device = new RingSwitch(nativeId, this.locationDevices.get(nativeId) as RingDevice); + this.devices.set(nativeId, device); } else { const device = new RingCameraDevice(this.plugin.api, nativeId, this.locationDevices.get(nativeId) as RingCamera); this.devices.set(nativeId, device); diff --git a/plugins/ring/src/ring-client-api.ts b/plugins/ring/src/ring-client-api.ts index 82665603ca..007bddc276 100644 --- a/plugins/ring/src/ring-client-api.ts +++ b/plugins/ring/src/ring-client-api.ts @@ -9,6 +9,6 @@ export { BasicPeerConnection } from '@koush/ring-client-api/packages/ring-client export { SimpleWebRtcSession } from '@koush/ring-client-api/packages/ring-client-api/streaming/simple-webrtc-session'; export { StreamingSession } from '@koush/ring-client-api/packages/ring-client-api/streaming/streaming-session'; export { generateUuid } from '@koush/ring-client-api/packages/ring-client-api/util'; -export { RingDeviceType, RingDeviceData } from '@koush/ring-client-api/packages/ring-client-api/ring-types'; +export { RingDeviceType, RingDeviceData, RingDeviceCategory } from '@koush/ring-client-api/packages/ring-client-api/ring-types'; export { RingDevice } from '@koush/ring-client-api/packages/ring-client-api/ring-device'; export * as rxjs from '@koush/ring-client-api/node_modules/rxjs'; \ No newline at end of file