mirror of
https://github.com/tsightler/ring-mqtt.git
synced 2025-12-24 13:18:16 +08:00
* Use MQTT for start-stream debug messages * Fix ANSI colors * Refactor event URL management * Fix subscription detection * Improve event URL expiry handling by parsing Amazon S3 expire time * Convert to ESM/replace colors with chalk * Force colors for chalk * Migrate to ESM * Fix stop of keepalive stream * Add transcoded event selections * Update event URL on raw/trancoded toggle * Switch to per-camera livecall threads * Customized WebRTC functions Mostly copied from ring-client-api with port to pure Javascript, removal of unneeded features and additional debugging modified for use as worker thread with ring-mqtt. Allows easier testing with updated Werift versions. * Add nightlight enable/disable * Include nightlight state as attribute * Only pro versions have nightlight * Tweak battery level reporting for dual battery cameras * Release 5.1.0
271 lines
11 KiB
JavaScript
271 lines
11 KiB
JavaScript
import RingSocketDevice from './base-socket-device.js'
|
|
import { allAlarmStates, RingDeviceType } from 'ring-client-api'
|
|
import utils from '../lib/utils.js'
|
|
import state from '../lib/state.js'
|
|
|
|
export default class SecurityPanel extends RingSocketDevice {
|
|
constructor(deviceInfo) {
|
|
super(deviceInfo, 'alarm', 'alarmState')
|
|
this.deviceData.mdl = 'Alarm Control Panel'
|
|
this.deviceData.name = `${this.device.location.name} Alarm`
|
|
|
|
this.entity = {
|
|
...this.entity,
|
|
alarm: {
|
|
component: 'alarm_control_panel',
|
|
isLegacyEntity: true // Legacy compatibility
|
|
},
|
|
siren: {
|
|
component: 'switch',
|
|
icon: 'mdi:alarm-light',
|
|
name: `${this.device.location.name} Siren`
|
|
},
|
|
...utils.config().enable_panic ? {
|
|
police: {
|
|
component: 'switch',
|
|
name: `${this.device.location.name} Panic - Police`,
|
|
icon: 'mdi:police-badge'
|
|
},
|
|
fire: {
|
|
component: 'switch',
|
|
name: `${this.device.location.name} Panic - Fire`,
|
|
icon: 'mdi:fire'
|
|
}
|
|
} : {}
|
|
}
|
|
}
|
|
|
|
publishState(data) {
|
|
const isPublish = data === undefined ? true : false
|
|
if (isPublish) {
|
|
// Eventually remove this but for now this attempts to delete the old light component based volume control from Home Assistant
|
|
this.mqttPublish(`homeassistant/switch/${this.locationId}/${this.deviceId}_bypass/config`, '', false)
|
|
}
|
|
|
|
let alarmMode
|
|
const alarmInfo = this.device.data.alarmInfo ? this.device.data.alarmInfo : []
|
|
|
|
// If alarm is active report triggered or, if entry-delay, pending
|
|
if (allAlarmStates.includes(alarmInfo.state)) {
|
|
alarmMode = alarmInfo.state === 'entry-delay' ? 'pending' : 'triggered'
|
|
} else {
|
|
switch(this.device.data.mode) {
|
|
case 'none':
|
|
alarmMode = 'disarmed'
|
|
break;
|
|
case 'some':
|
|
alarmMode = 'armed_home'
|
|
break;
|
|
case 'all':
|
|
const exitDelayMs = this.device.data.transitionDelayEndTimestamp - Date.now()
|
|
if (exitDelayMs > 0) {
|
|
alarmMode = 'arming'
|
|
this.waitForExitDelay(exitDelayMs)
|
|
} else {
|
|
alarmMode = 'armed_away'
|
|
}
|
|
break;
|
|
default:
|
|
alarmMode = 'unknown'
|
|
}
|
|
}
|
|
this.mqttPublish(this.entity.alarm.state_topic, alarmMode)
|
|
|
|
const sirenState = (this.device.data.siren && this.device.data.siren.state === 'on') ? 'ON' : 'OFF'
|
|
this.mqttPublish(this.entity.siren.state_topic, sirenState)
|
|
|
|
if (utils.config().enable_panic) {
|
|
let policeState = 'OFF'
|
|
let fireState = 'OFF'
|
|
const alarmState = this.device.data.alarmInfo ? this.device.data.alarmInfo.state : ''
|
|
switch (alarmState) {
|
|
case 'burglar-alarm':
|
|
case 'user-verified-burglar-alarm':
|
|
case 'burglar-accelerated-alarm':
|
|
policeState = 'ON'
|
|
this.debug('Burgler alarm is active for '+this.device.location.name)
|
|
case 'fire-alarm':
|
|
case 'co-alarm':
|
|
case 'user-verified-co-or-fire-alarm':
|
|
case 'fire-accelerated-alarm':
|
|
fireState = 'ON'
|
|
this.debug('Fire alarm is active for '+this.device.location.name)
|
|
}
|
|
this.mqttPublish(this.entity.police.state_topic, policeState)
|
|
this.mqttPublish(this.entity.fire.state_topic, fireState)
|
|
}
|
|
|
|
this.publishAttributes()
|
|
}
|
|
|
|
async waitForExitDelay(exitDelayMs) {
|
|
await utils.msleep(exitDelayMs)
|
|
if (this.device.data.mode === 'all') {
|
|
exitDelayMs = this.device.data.transitionDelayEndTimestamp - Date.now()
|
|
if (exitDelayMs <= 0) {
|
|
// Publish device sensor state
|
|
this.mqttPublish(this.entity.alarm.state_topic, 'armed_away')
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process messages from MQTT command topic
|
|
processCommand(command, message) {
|
|
const entityKey = command.split('/')[0]
|
|
switch (command) {
|
|
case 'alarm/command':
|
|
this.setAlarmMode(message)
|
|
break;
|
|
case 'siren/command':
|
|
this.setSirenMode(message)
|
|
break;
|
|
case 'police/command':
|
|
if (this.entity.hasOwnProperty(entityKey)) {
|
|
this.setPoliceMode(message)
|
|
}
|
|
break;
|
|
case 'fire/command':
|
|
if (this.entity.hasOwnProperty(entityKey)) {
|
|
this.setFireMode(message)
|
|
}
|
|
break;
|
|
default:
|
|
this.debug(`Received message to unknown command topic: ${command}`)
|
|
}
|
|
}
|
|
|
|
// Set Alarm Mode on received MQTT command message
|
|
async setAlarmMode(message) {
|
|
this.debug(`Received set alarm mode ${message} for location ${this.device.location.name} (${this.locationId})`)
|
|
|
|
// Try to set alarm mode and retry after delay if mode set fails
|
|
// Performing initial arming attempt with no delay
|
|
let retries = 5
|
|
let setAlarmSuccess = false
|
|
while (retries-- > 0 && !(setAlarmSuccess)) {
|
|
const bypassDeviceIds = []
|
|
|
|
if (message.toLowerCase() !== 'disarm') {
|
|
// During arming, check for sensors that require bypass
|
|
// Get all devices that allow bypass
|
|
const bypassDevices = (await this.device.location.getDevices()).filter(device =>
|
|
device.deviceType === RingDeviceType.ContactSensor ||
|
|
device.deviceType === RingDeviceType.RetrofitZone ||
|
|
device.deviceType === RingDeviceType.MotionSensor ||
|
|
device.deviceType === RingDeviceType.TiltSensor ||
|
|
device.deviceType === RingDeviceType.GlassbreakSensor
|
|
),
|
|
savedStates = state.getAllSavedStates(),
|
|
bypassDeviceNames = []
|
|
|
|
// Loop through all bypass eligible devices and bypass based on settings/state
|
|
for (const device of bypassDevices) {
|
|
const bypassMode = savedStates[device.id]?.bypass_mode
|
|
if (bypassMode === 'Always' || (bypassMode === 'Faulted' && device.data.faulted)) {
|
|
bypassDeviceIds.push(device.id)
|
|
bypassDeviceNames.push(`${device.name} [${bypassMode}]`)
|
|
}
|
|
}
|
|
|
|
if (bypassDeviceIds.length > 0) {
|
|
this.debug(`The following sensors will be bypassed [Reason]: ${bypassDeviceNames.join(', ')}`)
|
|
} else {
|
|
this.debug('No sensors will be bypased')
|
|
}
|
|
}
|
|
|
|
setAlarmSuccess = await this.trySetAlarmMode(message, bypassDeviceIds)
|
|
|
|
// On failure delay 10 seconds for next set attempt
|
|
if (!setAlarmSuccess) { await utils.sleep(10) }
|
|
}
|
|
|
|
// Check the return status and print some debugging for failed states
|
|
if (!setAlarmSuccess) {
|
|
this.debug('Alarm could not enter proper arming mode after all retries...Giving up!')
|
|
} else if (setAlarmSuccess == 'unknown') {
|
|
this.debug('Unknown alarm arming mode requested.')
|
|
}
|
|
}
|
|
|
|
async trySetAlarmMode(message, bypassDeviceIds) {
|
|
let alarmTargetMode
|
|
this.debug(`Set alarm mode: ${message}`)
|
|
switch(message.toLowerCase()) {
|
|
case 'disarm':
|
|
this.device.location.disarm().catch(err => { this.debug(err) })
|
|
alarmTargetMode = 'none'
|
|
break
|
|
case 'arm_home':
|
|
this.device.location.armHome(bypassDeviceIds).catch(err => { this.debug(err) })
|
|
alarmTargetMode = 'some'
|
|
break
|
|
case 'arm_away':
|
|
this.device.location.armAway(bypassDeviceIds).catch(err => { this.debug(err) })
|
|
alarmTargetMode = 'all'
|
|
break
|
|
default:
|
|
this.debug('Cannot set alarm mode: Unknown')
|
|
return 'unknown'
|
|
}
|
|
|
|
// Sleep a few seconds and check if alarm entered requested mode
|
|
await utils.sleep(1);
|
|
if (this.device.data.mode == alarmTargetMode) {
|
|
this.debug(`Alarm for location ${this.device.location.name} successfully entered ${message} mode`)
|
|
return true
|
|
} else {
|
|
this.debug(`Alarm for location ${this.device.location.name} failed to enter requested arm/disarm mode!`)
|
|
return false
|
|
}
|
|
}
|
|
|
|
async setSirenMode(message) {
|
|
switch(message.toLowerCase()) {
|
|
case 'on':
|
|
this.debug(`Activating siren for ${this.device.location.name}`)
|
|
this.device.location.soundSiren().catch(err => { this.debug(err) })
|
|
break;
|
|
case 'off': {
|
|
this.debug(`Deactivating siren for ${this.device.location.name}`)
|
|
this.device.location.silenceSiren().catch(err => { this.debug(err) })
|
|
break;
|
|
}
|
|
default:
|
|
this.debug('Received invalid command for siren!')
|
|
}
|
|
}
|
|
|
|
async setPoliceMode(message) {
|
|
switch(message.toLowerCase()) {
|
|
case 'on':
|
|
this.debug(`Activating burglar alarm for ${this.device.location.name}`)
|
|
this.device.location.triggerBurglarAlarm().catch(err => { this.debug(err) })
|
|
break;
|
|
case 'off': {
|
|
this.debug(`Deactivating burglar alarm for ${this.device.location.name}`)
|
|
this.device.location.setAlarmMode('none').catch(err => { this.debug(err) })
|
|
break;
|
|
}
|
|
default:
|
|
this.debug('Received invalid command for panic!')
|
|
}
|
|
}
|
|
|
|
async setFireMode(message) {
|
|
switch(message.toLowerCase()) {
|
|
case 'on':
|
|
this.debug(`Activating fire alarm for ${this.device.location.name}`)
|
|
this.device.location.triggerFireAlarm().catch(err => { this.debug(err) })
|
|
break;
|
|
case 'off': {
|
|
this.debug(`Deactivating fire alarm for ${this.device.location.name}`)
|
|
this.device.location.setAlarmMode('none').catch(err => { this.debug(err) })
|
|
break;
|
|
}
|
|
default:
|
|
this.debug('Received invalid command for panic!')
|
|
}
|
|
}
|
|
}
|