UNPKG

@koush/ring-client-api

Version:

Unofficial API for Ring doorbells, cameras, security alarm system and smart lighting

388 lines (352 loc) 12.4 kB
import { RingApi, RingCamera, RingCameraKind, RingChime, RingDevice, RingDeviceCategory, RingDeviceType, } from '../api' import { hap } from './hap' import { API, DynamicPlatformPlugin, Logging, PlatformAccessory, PlatformConfig, } from 'homebridge' import { SecurityPanel } from './security-panel' import { BaseStation } from './base-station' import { Chime } from './chime' import { Keypad } from './keypad' import { ContactSensor } from './contact-sensor' import { MotionSensor } from './motion-sensor' import { Lock } from './lock' import { SmokeAlarm } from './smoke-alarm' import { CoAlarm } from './co-alarm' import { SmokeCoListener } from './smoke-co-listener' import { getSystemId, RingPlatformConfig, updateHomebridgeConfig, } from './config' import { Beam } from './beam' import { MultiLevelSwitch } from './multi-level-switch' import { Fan } from './fan' import { Outlet } from './outlet' import { Switch } from './switch' import { Camera } from './camera' import { PanicButtons } from './panic-buttons' import { RefreshTokenAuth } from '../api/rest-client' import { useLogger } from '../api/util' import { BaseAccessory } from './base-accessory' import { FloodFreezeSensor } from './flood-freeze-sensor' import { FreezeSensor } from './freeze-sensor' import { TemperatureSensor } from './temperature-sensor' import { WaterSensor } from './water-sensor' import { LocationModeSwitch } from './location-mode-switch' import { Thermostat } from './thermostat' import { UnknownZWaveSwitchSwitch } from './unknown-zwave-switch' import { generateMacAddress } from './util' const debug = __filename.includes('release-homebridge'), unsupportedDeviceTypes: ( | RingDeviceType | RingCameraKind | RingChime['deviceType'] )[] = [RingDeviceType.BaseStation, RingDeviceType.Keypad], ignoreHiddenDeviceTypes: string[] = [ RingDeviceType.RingNetAdapter, RingDeviceType.ZigbeeAdapter, RingDeviceType.CodeVault, RingDeviceType.SecurityAccessCode, RingDeviceType.ZWaveAdapter, RingDeviceType.ZWaveExtender, RingDeviceType.BeamsDevice, RingDeviceType.PanicButton, ] export const platformName = 'Ring' export const pluginName = 'homebridge-ring' process.env.RING_DEBUG = debug ? 'true' : '' function getAccessoryClass( device: RingDevice ): (new (...args: any[]) => BaseAccessory<RingDevice>) | null { const { deviceType } = device if (device.data.status === 'disabled') { return null } switch (deviceType) { case RingDeviceType.ContactSensor: case RingDeviceType.RetrofitZone: case RingDeviceType.TiltSensor: case RingDeviceType.GlassbreakSensor: return ContactSensor case RingDeviceType.MotionSensor: return MotionSensor case RingDeviceType.FloodFreezeSensor: return FloodFreezeSensor case RingDeviceType.FreezeSensor: return FreezeSensor case RingDeviceType.SecurityPanel: return SecurityPanel case RingDeviceType.BaseStation: return BaseStation case RingDeviceType.Keypad: return Keypad case RingDeviceType.SmokeAlarm: return SmokeAlarm case RingDeviceType.CoAlarm: return CoAlarm case RingDeviceType.SmokeCoListener: return SmokeCoListener case RingDeviceType.BeamsMotionSensor: case RingDeviceType.BeamsSwitch: case RingDeviceType.BeamsMultiLevelSwitch: case RingDeviceType.BeamsTransformerSwitch: case RingDeviceType.BeamsLightGroupSwitch: return Beam case RingDeviceType.MultiLevelSwitch: return device instanceof RingDevice && device.categoryId === RingDeviceCategory.Fans ? Fan : MultiLevelSwitch case RingDeviceType.MultiLevelBulb: return MultiLevelSwitch case RingDeviceType.Switch: return device instanceof RingDevice && device.categoryId === RingDeviceCategory.Outlets ? Outlet : Switch case RingDeviceType.TemperatureSensor: return TemperatureSensor case RingDeviceType.WaterSensor: return WaterSensor case RingDeviceType.Thermostat: return Thermostat case RingDeviceType.UnknownZWave: return UnknownZWaveSwitchSwitch } if (/^lock($|\.)/.test(deviceType)) { return Lock } if (deviceType === RingDeviceType.Sensor) { // Generic sensor that could be any type of sensor, but should at least have `faulted` if (device.name.toLowerCase().includes('motion')) { return MotionSensor } return ContactSensor } return null } export class RingPlatform implements DynamicPlatformPlugin { private readonly homebridgeAccessories: { [uuid: string]: PlatformAccessory } = {} constructor( public log: Logging, public config: PlatformConfig & RingPlatformConfig & RefreshTokenAuth, public api: API ) { useLogger({ logInfo(message) { log.info(message) }, logError(message) { log.error(message) }, }) if (!config) { this.log.info('No configuration found for platform Ring') return } config.cameraStatusPollingSeconds = config.cameraStatusPollingSeconds ?? 20 config.cameraDingsPollingSeconds = config.cameraDingsPollingSeconds ?? 2 config.locationModePollingSeconds = config.locationModePollingSeconds ?? 20 this.api.on('didFinishLaunching', () => { this.log.debug('didFinishLaunching') if (config.refreshToken) { this.connectToApi().catch((e) => { this.log.error('Error connecting to API') this.log.error(e) }) } else { this.log.warn( 'Plugin is not configured. Visit https://github.com/dgreif/ring/tree/master/homebridge#homebridge-configuration for more information.' ) } }) this.homebridgeAccessories = {} } configureAccessory(accessory: PlatformAccessory) { this.log.info( `Configuring cached accessory ${accessory.UUID} ${accessory.displayName}` ) this.log.debug('%j', accessory) this.homebridgeAccessories[accessory.UUID] = accessory } async connectToApi() { const { api, config } = this, systemId = getSystemId(api), ringApi = new RingApi({ controlCenterDisplayName: 'homebridge-ring', ...config, systemId, }), locations = await ringApi.getLocations(), cachedAccessoryIds = Object.keys(this.homebridgeAccessories), platformAccessories: PlatformAccessory[] = [], activeAccessoryIds: string[] = [] this.log.info('Found the following locations:') locations.forEach((location) => { this.log.info(` locationId: ${location.id} - ${location.name}`) }) await Promise.all( locations.map(async (location) => { const devices = await location.getDevices(), cameras = location.cameras, chimes = location.chimes, allDevices = [...devices, ...cameras, ...chimes], securityPanel = devices.find( (x) => x.deviceType === RingDeviceType.SecurityPanel ), debugPrefix = debug ? 'TEST ' : '', hapDevices = allDevices.map((device) => { const isCamera = device instanceof RingCamera, cameraIdDifferentiator = isCamera ? 'camera' : '', // this forces bridged cameras from old version of the plugin to be seen as "stale" AccessoryClass = ( device instanceof RingCamera ? Camera : device instanceof RingChime ? Chime : getAccessoryClass(device) ) as (new (...args: any[]) => BaseAccessory<any>) | null return { deviceType: device.deviceType as string, device: device as any, isCamera, id: device.id.toString() + cameraIdDifferentiator, name: device.name, AccessoryClass, } }), hideDeviceIds = config.hideDeviceIds || [], onlyDeviceTypes = config.onlyDeviceTypes?.length ? config.onlyDeviceTypes : undefined if (config.showPanicButtons && securityPanel) { hapDevices.push({ deviceType: securityPanel.deviceType, device: securityPanel, isCamera: false, id: securityPanel.id.toString() + 'panic', name: 'Panic Buttons', AccessoryClass: PanicButtons, }) } if ( config.locationModePollingSeconds && (await location.supportsLocationModeSwitching()) ) { hapDevices.push({ deviceType: 'location.mode', device: location, isCamera: false, id: location.id + 'mode', name: location.name + ' Mode', AccessoryClass: LocationModeSwitch, }) } this.log.info( `Configuring ${cameras.length} cameras and ${hapDevices.length} devices for location "${location.name}" - locationId: ${location.id}` ) hapDevices.forEach( ({ deviceType, device, isCamera, id, name, AccessoryClass }) => { const uuid = hap.uuid.generate(debugPrefix + id), displayName = debugPrefix + name if ( !AccessoryClass || (config.hideLightGroups && deviceType === RingDeviceType.BeamsLightGroupSwitch) || (config.hideUnsupportedServices && unsupportedDeviceTypes.includes(deviceType as any)) || hideDeviceIds.includes(uuid) || (onlyDeviceTypes && !onlyDeviceTypes.includes(deviceType)) ) { if (!ignoreHiddenDeviceTypes.includes(deviceType)) { this.log.info( `Hidden accessory ${uuid} ${deviceType} ${displayName}` ) } return } const createHomebridgeAccessory = () => { const accessory = new api.platformAccessory( displayName, uuid, isCamera ? hap.Categories.CAMERA : hap.Categories.SECURITY_SYSTEM ) this.log.info( `Adding new accessory ${uuid} ${deviceType} ${displayName}` ) platformAccessories.push(accessory) if ( isCamera && typeof hap.Accessory.cleanupAccessoryData === 'function' ) { // This is a one-time cleanup that will remove persist files for old external accessories from before camera bridging in version 8 hap.Accessory.cleanupAccessoryData( generateMacAddress(accessory.UUID) ) } return accessory }, homebridgeAccessory = this.homebridgeAccessories[uuid] || createHomebridgeAccessory(), accessory = new AccessoryClass( device as any, homebridgeAccessory, this.log, config ) accessory.initBase() this.homebridgeAccessories[uuid] = homebridgeAccessory activeAccessoryIds.push(uuid) } ) }) ) if (platformAccessories.length) { api.registerPlatformAccessories( pluginName, platformName, platformAccessories ) } const staleAccessories = cachedAccessoryIds .filter((cachedId) => !activeAccessoryIds.includes(cachedId)) .map((id) => this.homebridgeAccessories[id]) staleAccessories.forEach((staleAccessory) => { this.log.info( `Removing stale cached accessory ${staleAccessory.UUID} ${staleAccessory.displayName}` ) }) if (staleAccessories.length) { this.api.unregisterPlatformAccessories( pluginName, platformName, staleAccessories ) } ringApi.onRefreshTokenUpdated.subscribe( ({ oldRefreshToken, newRefreshToken }) => { if (!oldRefreshToken) { return } updateHomebridgeConfig(this.api, (configContents) => { return configContents.replace(oldRefreshToken, newRefreshToken) }) } ) } }