UNPKG

homebridge-ring

Version:

Homebridge plugin for Ring doorbells, cameras, security alarm system and smart lighting

202 lines (201 loc) 9.18 kB
import { hap } from "./hap.js"; import { BaseDataAccessory } from "./base-data-accessory.js"; import { filter, map, switchMap, throttleTime } from 'rxjs/operators'; import { CameraSource } from "./camera-source.js"; import { TargetValueTimer } from "./target-value-timer.js"; import { delay, logError, logInfo } from 'ring-client-api/util'; import { firstValueFrom } from 'rxjs'; export class Camera extends BaseDataAccessory { inHomeDoorbellStatus; cameraSource; device; accessory; config; constructor(device, accessory, config) { super(); this.device = device; this.accessory = accessory; this.config = config; this.cameraSource = new CameraSource(this.device); if (!hap.CameraController) { const error = 'HAP CameraController not found. Please make sure you are on homebridge version 1.0.0 or newer'; logError(error); throw new Error(error); } const { Characteristic, Service } = hap, { ChargingState, StatusLowBattery } = Characteristic; accessory.configureController(this.cameraSource.controller); this.registerCharacteristic({ characteristicType: Characteristic.Mute, serviceType: Service.Microphone, getValue: () => false, }); this.registerCharacteristic({ characteristicType: Characteristic.Mute, serviceType: Service.Speaker, getValue: () => false, }); if (!config.hideCameraMotionSensor) { this.registerObservableCharacteristic({ characteristicType: Characteristic.MotionDetected, serviceType: Service.MotionSensor, onValue: device.onMotionDetected.pipe(switchMap((motion) => { if (!motion) { return Promise.resolve(false); } return this.loadSnapshotForEvent('Detected Motion', true); })), }); } if (device.isDoorbot) { this.registerObservableCharacteristic({ characteristicType: Characteristic.ProgrammableSwitchEvent, serviceType: Service.Doorbell, onValue: device.onDoorbellPressed.pipe(throttleTime(15000), switchMap(() => { return this.loadSnapshotForEvent('Doorbell Pressed', Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS); })), }); if (!config.hideDoorbellSwitch) { this.registerObservableCharacteristic({ characteristicType: Characteristic.ProgrammableSwitchEvent, serviceType: Service.StatelessProgrammableSwitch, onValue: device.onDoorbellPressed.pipe(map(() => Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS)), }); // Hide long and double press events by setting max value this.getService(Service.StatelessProgrammableSwitch) .getCharacteristic(Characteristic.ProgrammableSwitchEvent) .setProps({ maxValue: Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, }); } } if (device.hasLight && !config.hideCameraLight) { const lightTargetTimer = new TargetValueTimer(); this.registerCharacteristic({ characteristicType: Characteristic.On, serviceType: Service.Lightbulb, getValue: (data) => { const value = lightTargetTimer.hasTarget() ? lightTargetTimer.getTarget() : data.led_status === 'on'; return value; }, setValue: (value) => { // Allow 30 seconds for the light value to update in our status updates from Ring lightTargetTimer.setTarget(value, 30000); return device.setLight(value); }, requestUpdate: () => device.requestUpdate(), }); } if (device.hasSiren && !config.hideCameraSirenSwitch) { this.registerCharacteristic({ characteristicType: Characteristic.On, serviceType: Service.Switch, serviceSubType: 'Siren', name: device.name + ' Siren', getValue: (data) => { return Boolean(data.siren_status && data.siren_status.seconds_remaining); }, setValue: (value) => device.setSiren(value), requestUpdate: () => device.requestUpdate(), }); } if (device.hasInHomeDoorbell && !config.hideInHomeDoorbellSwitch) { this.device.onInHomeDoorbellStatus.subscribe((data) => { this.inHomeDoorbellStatus = data; }); this.registerObservableCharacteristic({ characteristicType: Characteristic.On, serviceType: Service.Switch, serviceSubType: 'In-Home Doorbell', name: device.name + ' In-Home Doorbell', onValue: device.onInHomeDoorbellStatus, setValue: (value) => device.setInHomeDoorbell(value), requestUpdate: () => device.requestUpdate(), }); } this.registerCharacteristic({ characteristicType: Characteristic.Manufacturer, serviceType: Service.AccessoryInformation, getValue: (data) => { if ('metadata' in data && 'third_party_manufacturer' in data.metadata) { return data.metadata.third_party_manufacturer; } return 'Ring'; }, }); this.registerCharacteristic({ characteristicType: Characteristic.Model, serviceType: Service.AccessoryInformation, getValue: (data) => { if ('metadata' in data && 'third_party_model' in data.metadata) { return data.metadata.third_party_model; } return `${device.model} (${data.kind})`; }, }); this.registerCharacteristic({ characteristicType: Characteristic.SerialNumber, serviceType: Service.AccessoryInformation, getValue: (data) => data.device_id, }); if (device.hasBattery) { this.registerCharacteristic({ characteristicType: Characteristic.StatusLowBattery, serviceType: Service.Battery, getValue: () => { return device.hasLowBattery ? StatusLowBattery.BATTERY_LEVEL_LOW : StatusLowBattery.BATTERY_LEVEL_NORMAL; }, }); this.registerCharacteristic({ characteristicType: Characteristic.ChargingState, serviceType: Service.Battery, getValue: () => { return device.isCharging ? ChargingState.CHARGING : ChargingState.NOT_CHARGING; }, }); this.registerObservableCharacteristic({ characteristicType: Characteristic.BatteryLevel, serviceType: Service.Battery, onValue: device.onBatteryLevel.pipe(map((batteryLevel) => { return batteryLevel === null ? 100 : batteryLevel; })), }); } } async loadSnapshotForEvent(eventDescription, characteristicValue) { let imageUuid = this.device.latestNotificationSnapshotUuid; /** * Battery cameras may receive an initial notification with no image uuid, * followed shortly by a second notification with the image uuid. We need to * wait for the second notification before we can load the snapshot. */ if (!this.device.canTakeSnapshotWhileRecording && !imageUuid) { await Promise.race([ firstValueFrom(this.device.onNewNotification.pipe(filter((notification) => Boolean(notification.img?.snapshot_uuid)))), // wait up to 2 seconds for the second notification delay(2000), ]); imageUuid = this.device.latestNotificationSnapshotUuid; if (!imageUuid) { // did not receive an image uuid and one can't be taken while recording. Proceed without a snapshot logInfo(this.device.name + ' ' + eventDescription); return characteristicValue; } } logInfo(this.device.name + ` ${eventDescription}. Loading snapshot before sending event to HomeKit`); try { await this.cameraSource.loadSnapshot(imageUuid); } catch { logInfo(this.device.name + ' Failed to load snapshot. Sending event to HomeKit without new snapshot'); } return characteristicValue; } }