Skip to content

Commit

Permalink
feat(homebridge): panic buttons for burglar and fire
Browse files Browse the repository at this point in the history
  • Loading branch information
dgreif committed Aug 10, 2019
1 parent 60773a2 commit c87a83a
Show file tree
Hide file tree
Showing 10 changed files with 242 additions and 24 deletions.
50 changes: 46 additions & 4 deletions api/location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import {
skip,
take
} from 'rxjs/operators'
import { delay, logError, logInfo } from './util'
import { delay, generateRandomId, logError, logInfo } from './util'
import {
AccountMonitoringStatus,
AlarmMode,
AssetSession,
deviceTypesWithVolume,
DispatchSignalType,
LocationEvent,
MessageDataType,
MessageType,
Expand All @@ -25,7 +27,7 @@ import {
TicketAsset,
UserLocation
} from './ring-types'
import { clientApi, RingRestClient } from './rest-client'
import { appApi, clientApi, RingRestClient } from './rest-client'
import { RingCamera } from './ring-camera'

const deviceListMessageType = 'DeviceInfoDocGetList'
Expand Down Expand Up @@ -243,8 +245,7 @@ export class Location {
host: string
ticket: string
}>({
url:
'https://app.ring.com/api/v1/clap/tickets?locationID=' + this.locationId
url: appApi('clap/tickets?locationID=' + this.locationId)
})
this.assets = assets
this.receivedAssetDeviceLists.length = 0
Expand Down Expand Up @@ -457,4 +458,45 @@ export class Location {
)
})
}

getAccountMonitoringStatus() {
return this.restClient.request<AccountMonitoringStatus>({
url: appApi('rs/monitoring/accounts/' + this.locationId)
})
}

private triggerAlarm(signalType: DispatchSignalType) {
const now = Date.now(),
alarmSessionUuid = generateRandomId(),
baseStationAsset =
this.assets && this.assets.find(x => x.kind === 'base_station_v1')

if (!baseStationAsset) {
throw new Error(
'Cannot dispatch panic events without an alarm base station'
)
}

return this.restClient.request<AccountMonitoringStatus>({
method: 'POST',
url: appApi(
`rs/monitoring/accounts/${this.locationId}/assets/${baseStationAsset.uuid}/userAlarm`
),
json: true,
data: {
alarmSessionUuid,
currentTsMs: now,
eventOccurredTime: now,
signalType
}
})
}

triggerBurglarAlarm() {
return this.triggerAlarm(DispatchSignalType.Burglar)
}

triggerFireAlarm() {
return this.triggerAlarm(DispatchSignalType.Fire)
}
}
5 changes: 5 additions & 0 deletions api/rest-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ const ringErrorCodes: { [code: number]: string } = {
7063: 'MAINTENANCE'
},
clientApiBaseUrl = 'https://api.ring.com/clients_api/',
appApiBaseUrl = 'https://app.ring.com/api/v1/',
apiVersion = 11,
hardwareId = generateRandomId()

export function clientApi(path: string) {
return clientApiBaseUrl + path
}

export function appApi(path: string) {
return appApiBaseUrl + path
}

export interface ExtendedResponse {
responseTimestamp: number
}
Expand Down
36 changes: 36 additions & 0 deletions api/ring-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,18 @@ export type AlarmState =
| 'burglar-accelerated-alarm' // Alarming - Police Response Requested
| 'fire-accelerated-alarm' // Alarming - Fire Department Response Requested

export const allAlarmStates: AlarmState[] = [
'burglar-alarm',
'entry-delay',
'fire-alarm',
'co-alarm',
'panic',
'user-verified-burglar-alarm',
'user-verified-co-or-fire-alarm',
'burglar-accelerated-alarm',
'fire-accelerated-alarm'
]

export interface RingDeviceData {
zid: string
name: string
Expand Down Expand Up @@ -466,3 +478,27 @@ export interface SessionResponse {
tfa_phone_number: null | string
}
}

export interface AccountMonitoringStatus {
accountUuid: string
externalServiceConfigType: 'rrms' | string
accountState: 'PROFESSIONAL' | string
eligibleForDispatch: boolean
addressComplete: boolean
contactsComplete: boolean
codewordComplete: boolean
alarmSignalSent: boolean
professionallyMonitored: boolean
userAcceptDispatch: boolean
installationDate: number
externalId: string
vrRequired: false
vrUserOptIn: false
cmsMonitoringType: 'full' | string
dispatchSetupComplete: boolean
}

export enum DispatchSignalType {
Burglar = 'user-verified-burglar-xa',
Fire = 'user-verified-fire-xa'
}
6 changes: 6 additions & 0 deletions homebridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Option | Default | Explanation
`hideCameraMotionSensor` | `false` | If `true`, hides the motion sensor for Ring cameras in HomeKit.
`hideCameraSirenSwitch` | `false` | If `true`, hides the siren switch for Ring cameras in HomeKit.
`hideAlarmSirenSwitch` | `false` | If you have a Ring Alarm, you will see both the alarm and a "Siren" switch in HomeKit. The siren switch can sometimes get triggered by Siri commands by accident, which is loud and annoying. Set this option to `true` to hide the siren switch.
`showPanicButtons` | `false` | Creates a new `Panic Buttons` device in HomeKit with `Burglar Alarm` and `Fire Alarm` switches. **Use these at your own risk. I do not guarantee functionality in case of emergency, nor do I take responsibility for any false alarms**. These function just like the SOS sliders in the Ring app.
`cameraStatusPollingSeconds` | `20` | How frequently to poll for updates to your cameras. Information like light/siren status do not update in real time and need to be requested periodically.
`cameraDingsPollingSeconds` | `2` | How frequently to poll for new events from your cameras. These include motion and doorbell presses.
`locationIds` | All Locations | Use this option if you only want a subset of your locations to appear in HomeKit. If this option is not included, all of your locations will be added to HomeKit (which is what most users will want to do).
Expand Down Expand Up @@ -154,6 +155,11 @@ If you are having issues with your cameras in the Home app, please see the [Came
* Hue/Sat/Color Temp are _possible_, but currently not supported.
Please open an issue if you have a device that you would be able to
test these on.
* Panic Buttons
* These can be added by setting `showPanicButtons: true` in your config
* Creates `Burglar Alarm` and `Fire Alarm` switches in a new `Panic Buttons` device in HomeKit
* Use these at your own risk. **I do not guarantee functionality in case of emergency, nor do I take responsibility for any false alarms**
* If either switch is turned on, you will receive a call from Ring monitoring to verify the emergency, and then authorities will be dispatched

### Alarm Modes

Expand Down
18 changes: 14 additions & 4 deletions homebridge/base-accessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,24 @@ export abstract class BaseAccessory<T extends RingDevice | RingCamera> {
this.pruneUnusedServices()
}

getService(serviceType: HAP.Service, name = this.device.name) {
getService(
serviceType: HAP.Service,
name = this.device.name,
subType?: string
) {
if (typeof (serviceType as any) === 'object') {
return serviceType
}

if (process.env.RING_DEBUG) {
name = 'TEST ' + name
}

const service =
this.accessory.getService(serviceType) ||
this.accessory.addService(serviceType, name)
const existingService = subType
? this.accessory.getServiceByUUIDAndSubType(serviceType, subType)
: this.accessory.getService(serviceType),
service =
existingService || this.accessory.addService(serviceType, name, subType)

if (!this.servicesInUse.includes(service)) {
this.servicesInUse.push(service)
Expand Down
6 changes: 6 additions & 0 deletions homebridge/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
"type": "boolean",
"description": "Hides switch that allows you to turn on the siren of the Ring Alarm. Enable this is your Siri commands keep setting off the siren and you don't care about having the switch"
},
"showPanicButtons": {
"title": "Show Panic Buttons",
"type": "boolean",
"description": "Creates a new `Panic Buttons` device in HomeKit with `Burglar Alarm` and `Fire Alarm` switches. **Use these at your own risk. I do not guarantee functionality in case of emergency, nor do I take responsibility for any false alarms**. These function just like the SOS sliders in the Ring app."
},
"beamDurationSeconds": {
"title": "Ring Smart Lighting Timer",
"type": "integer",
Expand Down Expand Up @@ -120,6 +125,7 @@
"hideCameraMotionSensor",
"hideCameraSirenSwitch",
"hideAlarmSirenSwitch",
"showPanicButtons",
"beamDurationSeconds",
"cameraStatusPollingSeconds",
"cameraDingsPollingSeconds",
Expand Down
1 change: 1 addition & 0 deletions homebridge/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export interface RingPlatformConfig extends RingApiOptions {
hideCameraMotionSensor?: boolean
hideCameraSirenSwitch?: boolean
hideAlarmSirenSwitch?: boolean
showPanicButtons?: boolean
}
88 changes: 88 additions & 0 deletions homebridge/panic-buttons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { RingDevice, RingDeviceData, AlarmState } from '../api'
import { HAP, hap } from './hap'
import { RingPlatformConfig } from './config'
import { BaseAccessory } from './base-accessory'

const burglarStates: AlarmState[] = [
'burglar-alarm',
'user-verified-burglar-alarm',
'burglar-accelerated-alarm'
],
fireStates: AlarmState[] = [
'fire-alarm',
'user-verified-co-or-fire-alarm',
'fire-accelerated-alarm'
]

function matchesAnyAlarmState(
{ alarmInfo }: RingDeviceData,
targetStates: AlarmState[]
) {
return Boolean(alarmInfo && targetStates.includes(alarmInfo.state))
}

export class PanicButtons extends BaseAccessory<RingDevice> {
constructor(
public readonly device: RingDevice,
public readonly accessory: HAP.Accessory,
public readonly logger: HAP.Log,
public readonly config: RingPlatformConfig
) {
super()

const { Characteristic, Service } = hap,
locationName = device.location.locationDetails.name

this.registerCharacteristic(
Characteristic.On,
this.getService(Service.Switch, 'Burglar Alarm', 'Burglar'),
data => matchesAnyAlarmState(data, burglarStates),
on => {
if (on) {
this.logger.info(`Burglar Alarm activated for ${locationName}`)
return this.device.location.triggerBurglarAlarm()
}

this.logger.info(`Burglar Alarm turned off for ${locationName}`)
return this.device.location.setAlarmMode('none')
}
)

this.registerCharacteristic(
Characteristic.On,
this.getService(Service.Switch, 'Fire Alarm', 'Fire'),
data => matchesAnyAlarmState(data, fireStates),
on => {
if (on) {
this.logger.info(`Fire Alarm activated for ${locationName}`)
return this.device.location.triggerFireAlarm()
}

this.logger.info(`Fire Alarm turned off for ${locationName}`)
return this.device.location.setAlarmMode('none')
}
)
}

initBase() {
const { Characteristic, Service } = hap

this.registerCharacteristic(
Characteristic.Manufacturer,
Service.AccessoryInformation,
data => data.manufacturerName || 'Ring'
)
this.registerCharacteristic(
Characteristic.Model,
Service.AccessoryInformation,
() => 'Panic Buttons for ' + this.device.location.locationDetails.name
)
this.registerCharacteristic(
Characteristic.SerialNumber,
Service.AccessoryInformation,
() => 'None'
)

super.initBase()
}
}
50 changes: 37 additions & 13 deletions homebridge/ring-platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { MultiLevelSwitch } from './multi-level-switch'
import { Fan } from './fan'
import { Switch } from './switch'
import { Camera } from './camera'
import { PanicButtons } from './panic-buttons'
import { RingAuth } from '../api/rest-client'
import { platformName, pluginName } from './plugin-info'
import { useLogger } from '../api/util'
Expand All @@ -29,7 +30,7 @@ const debug = __filename.includes('release-homebridge')

process.env.RING_DEBUG = debug ? 'true' : ''

function getAccessoryClass(device: RingDevice | RingCamera) {
function getAccessoryClass(device: RingDevice | RingCamera): any {
const { deviceType } = device

switch (deviceType) {
Expand Down Expand Up @@ -129,18 +130,42 @@ export class RingPlatform {
locations.map(async location => {
const devices = await location.getDevices(),
cameras = location.cameras,
allDevices = [...devices, ...cameras]
allDevices = [...devices, ...cameras],
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"

return {
device,
isCamera,
id: device.id.toString() + cameraIdDifferentiator,
name: device.name,
AccessoryClass: isCamera ? Camera : getAccessoryClass(device)
}
})

hapDevices.length = 0

if (this.config.showPanicButtons && securityPanel) {
hapDevices.push({
device: securityPanel,
isCamera: false,
id: securityPanel.id.toString() + 'panic',
name: 'Panic Buttons',
AccessoryClass: PanicButtons
})
}

this.log.info(
`Configuring ${cameras.length} cameras and ${devices.length} devices for locationId ${location.locationId} (${location.locationDetails.name})`
`Configuring ${cameras.length} cameras and ${hapDevices.length} devices for location "${location.locationDetails.name}" - locationId: ${location.locationId}`
)
allDevices.forEach(device => {
const isCamera = device instanceof RingCamera,
AccessoryClass = isCamera ? Camera : getAccessoryClass(device),
debugPrefix = debug ? 'TEST ' : '',
cameraIdDifferentiator = isCamera ? 'camera' : '', // this forces bridged cameras from old version of the plugin to be seen as "stale"
id = debugPrefix + device.id.toString() + cameraIdDifferentiator,
uuid = hap.UUIDGen.generate(id)
hapDevices.forEach(({ device, isCamera, id, name, AccessoryClass }) => {
const uuid = hap.UUIDGen.generate(debugPrefix + id),
displayName = debugPrefix + name

if (
!AccessoryClass ||
Expand All @@ -152,16 +177,15 @@ export class RingPlatform {

const createHomebridgeAccessory = () => {
const accessory = new hap.PlatformAccessory(
debugPrefix + device.name,
displayName,
uuid,
isCamera
? hap.AccessoryCategories.CAMERA
: hap.AccessoryCategories.SECURITY_SYSTEM
)

this.log.info(
`Adding new accessory ${device.deviceType} ${debugPrefix +
device.name}`
`Adding new accessory ${device.deviceType} ${displayName}`
)

if (isCamera) {
Expand Down
Loading

0 comments on commit c87a83a

Please sign in to comment.