Implement event based engine

* First steps toward EventEmitter based core
This commit is contained in:
tsightler
2022-03-04 20:41:02 -05:00
parent 2ac92e0332
commit a702a98d96
31 changed files with 329 additions and 308 deletions

View File

@@ -1,8 +1,3 @@
const debug = {
mqtt: require('debug')('ring-mqtt'),
attr: require('debug')('ring-attr'),
disc: require('debug')('ring-disc')
}
const utils = require('../lib/utils')
const colors = require('colors/safe')
@@ -10,8 +5,6 @@ const colors = require('colors/safe')
class RingDevice {
constructor(deviceInfo, category, primaryAttribute, deviceId, locationId) {
this.device = deviceInfo.device
this.mqttClient = deviceInfo.mqttClient
this.config = deviceInfo.config
this.deviceId = deviceId
this.locationId = locationId
this.availabilityState = 'unpublished'
@@ -20,11 +13,10 @@ class RingDevice {
return this.availabilityState === 'online' ? true : false
}
this.debug = (message, debugType) => {
debugType = debugType ? debugType : 'mqtt'
debug[debugType](colors.green(`[${this.device.name}] `)+message)
utils.debug(debugType === 'disc' ? message : colors.green(`[${this.device.name}] `)+message, debugType ? debugType : 'mqtt')
}
// Build device base and availability topic
this.deviceTopic = `${this.config.ring_topic}/${this.locationId}/${category}/${this.deviceId}`
this.deviceTopic = `${utils.config.ring_topic}/${this.locationId}/${category}/${this.deviceId}`
this.availabilityTopic = `${this.deviceTopic}/status`
if (primaryAttribute !== 'disable') {
@@ -39,7 +31,7 @@ class RingDevice {
// and publishes this message to the Home Assistant config topic
async publishDiscovery() {
const debugMsg = (this.availabilityState === 'unpublished') ? 'Publishing new ' : 'Republishing existing '
debug.disc(debugMsg+'device id: '+this.deviceId, 'disc')
this.debug(debugMsg+'device id: '+this.deviceId, 'disc')
Object.keys(this.entity).forEach(entityKey => {
const entity = this.entity[entityKey]
@@ -102,8 +94,8 @@ class RingDevice {
? { icon: entity.icon }
: entityKey === "info"
? { icon: 'mdi:information-outline' } : {},
... entity.component === 'alarm_control_panel' && this.config.disarm_code
? { code: this.config.disarm_code.toString(),
... entity.component === 'alarm_control_panel' && utils.config.disarm_code
? { code: utils.config.disarm_code.toString(),
code_arm_required: false,
code_disarm_required: true } : {},
... entity.hasOwnProperty('brightness_scale')
@@ -149,9 +141,9 @@ class RingDevice {
}
const configTopic = `homeassistant/${entity.component}/${this.locationId}/${this.deviceId}_${entityKey}/config`
debug.disc(`HASS config topic: ${configTopic}`)
debug.disc(discoveryMessage)
this.publishMqtt(configTopic, JSON.stringify(discoveryMessage), false)
this.debug(`HASS config topic: ${configTopic}`, 'disc')
this.debug(discoveryMessage, 'disc')
this.mqttPublish(configTopic, JSON.stringify(discoveryMessage), false)
// On first publish store generated topics in entities object and subscribe to command topics
if (!this.entity[entityKey].hasOwnProperty('published')) {
@@ -159,7 +151,7 @@ class RingDevice {
Object.keys(discoveryMessage).filter(property => property.match('topic')).forEach(topic => {
this.entity[entityKey][topic] = discoveryMessage[topic]
if (topic.match('command_topic')) {
this.mqttClient.subscribe(discoveryMessage[topic])
utils.event.emit('mqttSubscribe', discoveryMessage[topic])
}
})
}
@@ -187,18 +179,18 @@ class RingDevice {
return filteredAttributes
}, {})
if (Object.keys(entityAttributes).length > 0) {
this.publishMqtt(this.entity[entityKey].json_attributes_topic, JSON.stringify(entityAttributes), 'attr')
this.mqttPublish(this.entity[entityKey].json_attributes_topic, JSON.stringify(entityAttributes), 'attr')
}
}
})
}
// Publish state messages with debug
publishMqtt(topic, message, debugType) {
mqttPublish(topic, message, debugType) {
if (debugType !== false) {
this.debug(colors.blue(`${topic} `)+colors.cyan(`${message}`), debugType)
}
this.mqttClient.publish(topic, (typeof message === 'number') ? message.toString() : message, { qos: 1 })
utils.event.emit('mqttPublish', topic, message)
}
// Set state topic online
@@ -206,7 +198,7 @@ class RingDevice {
if (this.shutdown) { return } // Supress any delayed online state messages if ring-mqtt is shutting down
const debugType = (this.availabilityState === 'online') ? false : 'mqtt'
this.availabilityState = 'online'
this.publishMqtt(this.availabilityTopic, this.availabilityState, debugType)
this.mqttPublish(this.availabilityTopic, this.availabilityState, debugType)
await utils.sleep(2)
}
@@ -214,7 +206,7 @@ class RingDevice {
offline() {
const debugType = (this.availabilityState === 'offline') ? false : 'mqtt'
this.availabilityState = 'offline'
this.publishMqtt(this.availabilityTopic, this.availabilityState, debugType)
this.mqttPublish(this.availabilityTopic, this.availabilityState, debugType)
}
}

View File

@@ -119,7 +119,7 @@ class RingSocketDevice extends RingDevice {
&& this.device.data.hasOwnProperty('networks') && this.device.data.networks.hasOwnProperty('wlan0')
? { wirelessNetwork: this.device.data.networks.wlan0.ssid, wirelessSignal: this.device.data.networks.wlan0.rssi } : {}
}
this.publishMqtt(this.entity.info.state_topic, JSON.stringify(attributes), 'attr')
this.mqttPublish(this.entity.info.state_topic, JSON.stringify(attributes), 'attr')
this.publishAttributeEntities(attributes)
}
}

View File

@@ -35,11 +35,11 @@ class BaseStation extends RingSocketDevice {
if (this.entity.hasOwnProperty('volume')) {
const currentVolume = (this.device.data.volume && !isNaN(this.device.data.volume) ? Math.round(100 * this.device.data.volume) : 0)
this.publishMqtt(this.entity.volume.state_topic, currentVolume.toString())
this.mqttPublish(this.entity.volume.state_topic, currentVolume.toString())
// Eventually remove this but for now this attempts to delete the old light component based volume control from Home Assistant
if (isPublish) {
this.publishMqtt('homeassistant/light/'+this.locationId+'/'+this.deviceId+'_audio/config', '', false)
this.mqttPublish('homeassistant/light/'+this.locationId+'/'+this.deviceId+'_audio/config', '', false)
}
}
this.publishAttributes()

View File

@@ -37,11 +37,11 @@ class BeamOutdoorPlug extends RingSocketDevice {
}
publishOutlet1State() {
this.publishMqtt(this.entity.outlet1.state_topic, this.childDevices.outlet1.data.on ? "ON" : "OFF")
this.mqttPublish(this.entity.outlet1.state_topic, this.childDevices.outlet1.data.on ? "ON" : "OFF")
}
publishOutlet2State() {
this.publishMqtt(this.entity.outlet2.state_topic, this.childDevices.outlet2.data.on ? "ON" : "OFF")
this.mqttPublish(this.entity.outlet2.state_topic, this.childDevices.outlet2.data.on ? "ON" : "OFF")
}
// Process messages from MQTT command topic

View File

@@ -51,8 +51,8 @@ class Beam extends RingSocketDevice {
icon: 'hass:timer'
}
if (this.config.hasOwnProperty('beam_duration') && this.config.beam_duration > 0) {
this.entity.beam_duration.state = this.config.beam_duration
if (utils.config.hasOwnProperty('beam_duration') && utils.config.beam_duration > 0) {
this.entity.beam_duration.state = utils.config.beam_duration
} else {
this.entity.beam_duration.state = this.device.data.hasOwnProperty('onDuration') ? this.device.data.onDuration : 0
}
@@ -61,16 +61,16 @@ class Beam extends RingSocketDevice {
publishData() {
if (this.entity.hasOwnProperty('motion') && this.entity.motion.hasOwnProperty('state_topic')) {
const motionState = this.device.data.motionStatus === 'faulted' ? 'ON' : 'OFF'
this.publishMqtt(this.entity.motion.state_topic, motionState)
this.mqttPublish(this.entity.motion.state_topic, motionState)
}
if (this.entity.hasOwnProperty('light') && this.entity.light.hasOwnProperty('state_topic')) {
const switchState = this.device.data.on ? 'ON' : 'OFF'
this.publishMqtt(this.entity.light.state_topic, switchState)
this.mqttPublish(this.entity.light.state_topic, switchState)
if (this.entity.light.hasOwnProperty('brightness_state_topic')) {
const switchLevel = (this.device.data.level && !isNaN(this.device.data.level) ? Math.round(100 * this.device.data.level) : 0)
this.publishMqtt(this.entity.light.brightness_state_topic, switchLevel.toString())
this.mqttPublish(this.entity.light.brightness_state_topic, switchLevel.toString())
}
this.publishMqtt(this.entity.beam_duration.state_topic, this.entity.beam_duration.state.toString())
this.mqttPublish(this.entity.beam_duration.state_topic, this.entity.beam_duration.state.toString())
}
if (!this.isLightGroup) {
this.publishAttributes()
@@ -145,7 +145,7 @@ class Beam extends RingSocketDevice {
this.debug('Light duration command received but out of range (0-32767)')
} else {
this.entity.beam_duration.state = parseInt(duration)
this.publishMqtt(this.entity.beam_duration.state_topic, this.entity.beam_duration.state.toString())
this.mqttPublish(this.entity.beam_duration.state_topic, this.entity.beam_duration.state.toString())
}
}
}

View File

@@ -1,7 +1,6 @@
const RingPolledDevice = require('./base-polled-device')
const utils = require( '../lib/utils' )
const colors = require('colors/safe')
const RingPolledDevice = require('./base-polled-device')
const { clientApi } = require('../node_modules/@tsightler/ring-client-api/lib/api/rest-client')
const P2J = require('pipe2jpeg')
const net = require('net');
const getPort = require('get-port')
@@ -47,8 +46,8 @@ class Camera extends RingPolledDevice {
status: 'inactive',
publishedStatus: '',
session: false,
rtspPublishUrl: (this.config.livestream_user && this.config.livestream_pass)
? `rtsp://${this.config.livestream_user}:${this.config.livestream_pass}@localhost:8554/${this.deviceId}_live`
rtspPublishUrl: (utils.config.livestream_user && utils.config.livestream_pass)
? `rtsp://${utils.config.livestream_user}:${utils.config.livestream_pass}@localhost:8554/${this.deviceId}_live`
: `rtsp://localhost:8554/${this.deviceId}_live`
},
event: {
@@ -60,8 +59,8 @@ class Camera extends RingPolledDevice {
recordingUrl: null,
recordingUrlExpire: null,
pollCycle: 0,
rtspPublishUrl: (this.config.livestream_user && this.config.livestream_pass)
? `rtsp://${this.config.livestream_user}:${this.config.livestream_pass}@localhost:8554/${this.deviceId}_event`
rtspPublishUrl: (utils.config.livestream_user && utils.config.livestream_pass)
? `rtsp://${utils.config.livestream_user}:${utils.config.livestream_pass}@localhost:8554/${this.deviceId}_event`
: `rtsp://localhost:8554/${this.deviceId}_event`
},
snapshot: {
@@ -93,10 +92,10 @@ class Camera extends RingPolledDevice {
} : {}
}
if (this.config.snapshot_mode.match(/^(motion|interval|all)$/)) {
this.data.snapshot.motion = (this.config.snapshot_mode.match(/^(motion|all)$/)) ? true : false
if (utils.config.snapshot_mode.match(/^(motion|interval|all)$/)) {
this.data.snapshot.motion = (utils.config.snapshot_mode.match(/^(motion|all)$/)) ? true : false
if (this.config.snapshot_mode.match(/^(interval|all)$/)) {
if (utils.config.snapshot_mode.match(/^(interval|all)$/)) {
this.data.snapshot.autoInterval = true
if (this.device.operatingOnBattery) {
if (this.device.data.settings.hasOwnProperty('lite_24x7') && this.device.data.settings.lite_24x7.enabled) {
@@ -252,8 +251,8 @@ class Camera extends RingPolledDevice {
// Set some helper attributes for streaming
this.data.stream.live.stillImageURL = `https://${stillImageUrlBase}:8123{{ states.camera.${this.device.name.toLowerCase().replace(" ","_")}_snapshot.attributes.entity_picture }}`,
this.data.stream.live.streamSource = (this.config.livestream_user && this.config.livestream_pass)
? `rtsp://${this.config.livestream_user}:${this.config.livestream_pass}@${streamSourceUrlBase}:8554/${this.deviceId}_live`
this.data.stream.live.streamSource = (utils.config.livestream_user && utils.config.livestream_pass)
? `rtsp://${utils.config.livestream_user}:${utils.config.livestream_pass}@${streamSourceUrlBase}:8554/${this.deviceId}_live`
: `rtsp://${streamSourceUrlBase}:8554/${this.deviceId}_live`
}
@@ -357,7 +356,7 @@ class Camera extends RingPolledDevice {
// Publish ding state and attributes
publishDingState(dingKind) {
const dingState = this.data[dingKind].active_ding ? 'ON' : 'OFF'
this.publishMqtt(this.entity[dingKind].state_topic, dingState)
this.mqttPublish(this.entity[dingKind].state_topic, dingState)
if (dingKind === 'motion') {
this.publishMotionAttributes()
@@ -376,7 +375,7 @@ class Camera extends RingPolledDevice {
this.data.motion.detection_enabled = this.device.data.settings.motion_detection_enabled
attributes.motionDetectionEnabled = this.data.motion.detection_enabled
}
this.publishMqtt(this.entity.motion.json_attributes_topic, JSON.stringify(attributes), 'attr')
this.mqttPublish(this.entity.motion.json_attributes_topic, JSON.stringify(attributes), 'attr')
}
publishDingAttributes() {
@@ -384,7 +383,7 @@ class Camera extends RingPolledDevice {
lastDing: this.data.ding.last_ding,
lastDingTime: this.data.ding.last_ding_time
}
this.publishMqtt(this.entity.ding.json_attributes_topic, JSON.stringify(attributes), 'attr')
this.mqttPublish(this.entity.ding.json_attributes_topic, JSON.stringify(attributes), 'attr')
}
// Publish camera state for polled attributes (light/siren state, etc)
@@ -395,14 +394,14 @@ class Camera extends RingPolledDevice {
const lightState = this.device.data.led_status === 'on' ? 'ON' : 'OFF'
if ((lightState !== this.data.light.state && Date.now()/1000 - this.data.light.setTime > 30) || isPublish) {
this.data.light.state = lightState
this.publishMqtt(this.entity.light.state_topic, this.data.light.state)
this.mqttPublish(this.entity.light.state_topic, this.data.light.state)
}
}
if (this.device.hasSiren) {
const sirenState = this.device.data.siren_status.seconds_remaining > 0 ? 'ON' : 'OFF'
if (sirenState !== this.data.siren.state || isPublish) {
this.data.siren.state = sirenState
this.publishMqtt(this.entity.siren.state_topic, this.data.siren.state)
this.mqttPublish(this.entity.siren.state_topic, this.data.siren.state)
}
}
@@ -430,14 +429,14 @@ class Camera extends RingPolledDevice {
}
attributes.stream_Source = this.data.stream.live.streamSource
attributes.still_Image_URL = this.data.stream.live.stillImageURL
this.publishMqtt(this.entity.info.state_topic, JSON.stringify(attributes), 'attr')
this.mqttPublish(this.entity.info.state_topic, JSON.stringify(attributes), 'attr')
this.publishAttributeEntities(attributes)
}
}
publishSnapshotInterval(isPublish) {
if (isPublish) {
this.publishMqtt(this.entity.snapshot_interval.state_topic, this.data.snapshot.interval.toString())
this.mqttPublish(this.entity.snapshot_interval.state_topic, this.data.snapshot.interval.toString())
} else {
// Update snapshot frequency in case it's changed
if (this.data.snapshot.autoInterval && this.data.snapshot.interval !== this.device.data.settings.lite_24x7.frequency_secs) {
@@ -445,7 +444,7 @@ class Camera extends RingPolledDevice {
clearTimeout(this.data.snapshot.intervalTimerId)
this.scheduleSnapshotRefresh()
}
this.publishMqtt(this.entity.snapshot_interval.state_topic, this.data.snapshot.interval.toString())
this.mqttPublish(this.entity.snapshot_interval.state_topic, this.data.snapshot.interval.toString())
}
}
@@ -455,13 +454,13 @@ class Camera extends RingPolledDevice {
const streamState = (this.data.stream[type].status === 'active' || this.data.stream[type].status === 'activating') ? 'ON' : 'OFF'
if (streamState !== this.data.stream[type].state || isPublish) {
this.data.stream[type].state = streamState
this.publishMqtt(this.entity[entityProp].state_topic, this.data.stream[type].state)
this.mqttPublish(this.entity[entityProp].state_topic, this.data.stream[type].state)
}
if (this.data.stream[type].publishedStatus !== this.data.stream[type].status || isPublish) {
this.data.stream[type].publishedStatus = this.data.stream[type].status
const attributes = { status: this.data.stream[type].status }
this.publishMqtt(this.entity[entityProp].json_attributes_topic, JSON.stringify(attributes), 'attr')
this.mqttPublish(this.entity[entityProp].json_attributes_topic, JSON.stringify(attributes), 'attr')
}
})
}
@@ -469,20 +468,20 @@ class Camera extends RingPolledDevice {
publishStreamSelectState(isPublish) {
if (this.data.event_select.state !== this.data.event_select.publishedState || isPublish) {
this.data.event_select.publishedState = this.data.event_select.state
this.publishMqtt(this.entity.event_select.state_topic, this.data.event_select.state)
this.mqttPublish(this.entity.event_select.state_topic, this.data.event_select.state)
}
const attributes = {
recordingUrl: this.data.stream.event.recordingUrl,
eventId: this.data.stream.event.dingId
}
this.publishMqtt(this.entity.event_select.json_attributes_topic, JSON.stringify(attributes), 'attr')
this.mqttPublish(this.entity.event_select.json_attributes_topic, JSON.stringify(attributes), 'attr')
}
// Publish snapshot image/metadata
publishSnapshot() {
this.debug(colors.blue(`${this.entity.snapshot.topic}`)+' '+colors.cyan('<binary_image_data>'))
this.publishMqtt(this.entity.snapshot.topic, this.data.snapshot.currentImage, false)
this.publishMqtt(this.entity.snapshot.json_attributes_topic, JSON.stringify({ timestamp: this.data.snapshot.timestamp }), 'attr')
this.mqttPublish(this.entity.snapshot.topic, this.data.snapshot.currentImage, false)
this.mqttPublish(this.entity.snapshot.json_attributes_topic, JSON.stringify({ timestamp: this.data.snapshot.timestamp }), 'attr')
}
// Refresh snapshot on scheduled interval
@@ -495,45 +494,42 @@ class Camera extends RingPolledDevice {
}
refreshSnapshot(type) {
if (this.device.operatingOnBattery) {
// Battery cameras can't take snapshots while streaming so check if there's an active stream
if (type === 'interval' && this.data.stream.live.status.match(/^(inactive|failed)$/)) {
// It's an interval snapshot and no active local stream, assume a standard snapshot will work
this.updateSnapshot(type)
} else {
// If a local live stream active, or it's a motion snapshot, grab an image from the stream
this.data.snapshot.update = true
if (type === 'motion') {
this.debug('Motion event detected on battery powered camera snapshot will be updated from live stream')
}
// If it's a motion event, and there's no active stream, start one so that snapshots can be grabbed from it
if (!this.data.stream.snapshot.active) {
this.startRtspReadStream('snapshot', this.data.stream.snapshot.duration)
}
}
} else {
// Line powered cameras can take snapshots all the time
if (!this.device.operatingOnBattery || (type === 'interval' && this.data.stream.live.status.match(/^(inactive|failed)$/))) {
// For line powered cameras, or battery cameras with no active stream,
// assume a regular snapshot update request will work
this.updateSnapshot(type)
} else {
this.data.snapshot.update = true
// Battery powered cameras can take a snapshot while recording so, if it's a motion
// event or there's an active local stream, grab a key frame to use for the snapshot
if (type === 'motion') {
this.debug('Motion event detected on battery powered camera, snapshot will be updated from live stream')
}
// If there's no existing active local stream, start one as it's required to get a snapshot
if (!this.data.stream.snapshot.active) {
this.startRtspReadStream('snapshot', this.data.stream.snapshot.duration)
}
}
}
async updateSnapshot(type) {
let newSnapshot
let newSnapshot = false
if (this.device.snapshotsAreBlocked) {
this.debug('Snapshots are unavailable, check if motion capture is disabled manually or via modes settings')
return
}
if (type === 'motion') {
this.debug('Motion event detected for line powered camera, forcing a non-cached snapshot update')
}
try {
this.debug('Requesting updated snapshot')
newSnapshot = (type === 'motion')
? await this.device.getNextSnapshot({ force: true })
: await this.device.getSnapshot()
if (type === 'motion') {
this.debug('Requesting an updated motion snapshot')
newSnapshot = await this.device.getNextSnapshot({ force: true })
} else {
this.debug('Requesting an updated interval snapshot')
newSnapshot = await this.device.getSnapshot()
}
} catch (error) {
this.debug(error)
this.debug('Failed to retrieve updated snapshot')
@@ -827,7 +823,7 @@ class Camera extends RingPolledDevice {
this.data.light.setTime = Math.floor(Date.now()/1000)
await this.device.setLight(command === 'on' ? true : false)
this.data.light.state = command === 'on' ? 'ON' : 'OFF'
this.publishMqtt(this.entity.light.state_topic, this.data.light.state)
this.mqttPublish(this.entity.light.state_topic, this.data.light.state)
break;
default:
this.debug('Received unknown command for light')

View File

@@ -69,25 +69,25 @@ class Chime extends RingPolledDevice {
// Polled states are published only if value changes or it's a device publish
if (volumeState !== this.data.volume || isPublish) {
this.publishMqtt(this.entity.volume.state_topic, volumeState.toString())
this.mqttPublish(this.entity.volume.state_topic, volumeState.toString())
this.data.volume = volumeState
}
if (snoozeState !== this.data.snooze || isPublish) {
this.publishMqtt(this.entity.snooze.state_topic, snoozeState)
this.mqttPublish(this.entity.snooze.state_topic, snoozeState)
this.data.snooze = snoozeState
}
if (snoozeMinutesRemaining !== this.data.snooze_minutes_remaining || isPublish) {
this.publishMqtt(this.entity.snooze.json_attributes_topic, JSON.stringify({ minutes_remaining: snoozeMinutesRemaining }), 'attr')
this.mqttPublish(this.entity.snooze.json_attributes_topic, JSON.stringify({ minutes_remaining: snoozeMinutesRemaining }), 'attr')
this.data.snooze_minutes_remaining = snoozeMinutesRemaining
}
// Local states are published only for publish/republish
if (isPublish) {
this.publishMqtt(this.entity.snooze_minutes.state_topic, this.data.snooze_minutes.toString())
this.publishMqtt(this.entity.play_ding_sound.state_topic, this.data.play_ding_sound)
this.publishMqtt(this.entity.play_motion_sound.state_topic, this.data.play_motion_sound)
this.mqttPublish(this.entity.snooze_minutes.state_topic, this.data.snooze_minutes.toString())
this.mqttPublish(this.entity.play_ding_sound.state_topic, this.data.play_ding_sound)
this.mqttPublish(this.entity.play_motion_sound.state_topic, this.data.play_motion_sound)
this.publishAttributes()
}
}
@@ -101,7 +101,7 @@ class Chime extends RingPolledDevice {
attributes.wirelessSignal = deviceHealth.latest_signal_strength
attributes.firmwareStatus = deviceHealth.firmware
attributes.lastUpdate = deviceHealth.updated_at.slice(0,-6)+"Z"
this.publishMqtt(this.entity.info.state_topic, JSON.stringify(attributes), 'attr')
this.mqttPublish(this.entity.info.state_topic, JSON.stringify(attributes), 'attr')
this.publishAttributeEntities(attributes)
}
}
@@ -156,7 +156,7 @@ class Chime extends RingPolledDevice {
this.debug('Snooze minutes command received but out of range (0-1440 minutes)')
} else {
this.data.snooze_minutes = parseInt(minutes)
this.publishMqtt(this.entity.snooze_minutes.state_topic, this.data.snooze_minutes.toString())
this.mqttPublish(this.entity.snooze_minutes.state_topic, this.data.snooze_minutes.toString())
}
}
@@ -179,10 +179,10 @@ class Chime extends RingPolledDevice {
switch(command) {
case 'on':
this.publishMqtt(this.entity[`play_${chimeType}_sound`].state_topic, 'ON')
this.mqttPublish(this.entity[`play_${chimeType}_sound`].state_topic, 'ON')
await this.device.playSound(chimeType)
await utils.sleep(5)
this.publishMqtt(this.entity[`play_${chimeType}_sound`].state_topic, 'OFF')
this.mqttPublish(this.entity[`play_${chimeType}_sound`].state_topic, 'OFF')
break;
case 'off': {
break;

View File

@@ -1,5 +1,4 @@
const RingSocketDevice = require('./base-socket-device')
const { RingDeviceType } = require('@tsightler/ring-client-api')
class CoAlarm extends RingSocketDevice {
constructor(deviceInfo) {
@@ -18,7 +17,7 @@ class CoAlarm extends RingSocketDevice {
publishData() {
const coState = this.device.data.alarmStatus === 'active' ? 'ON' : 'OFF'
this.publishMqtt(this.entity.co.state_topic, coState)
this.mqttPublish(this.entity.co.state_topic, coState)
this.publishAttributes()
}
}

View File

@@ -44,7 +44,7 @@ class ContactSensor extends RingSocketDevice {
publishData() {
const contactState = this.device.data.faulted ? 'ON' : 'OFF'
this.publishMqtt(this.entity[this.entityName].state_topic, contactState)
this.mqttPublish(this.entity[this.entityName].state_topic, contactState)
this.publishAttributes()
}
}

View File

@@ -33,13 +33,13 @@ class Fan extends RingSocketDevice {
// Publish device state
// targetFanPercent is a small hack to work around Home Assistant UI behavior
if (this.data.targetFanPercent && this.data.targetFanPercent !== fanPercent) {
this.publishMqtt(this.entity.fan.percentage_state_topic, this.data.targetFanPercent.toString())
this.mqttPublish(this.entity.fan.percentage_state_topic, this.data.targetFanPercent.toString())
this.data.targetFanPercent = undefined
} else {
this.publishMqtt(this.entity.fan.percentage_state_topic, fanPercent.toString())
this.mqttPublish(this.entity.fan.percentage_state_topic, fanPercent.toString())
}
this.publishMqtt(this.entity.fan.state_topic, fanState)
this.publishMqtt(this.entity.fan.preset_mode_state_topic, fanPreset)
this.mqttPublish(this.entity.fan.state_topic, fanState)
this.mqttPublish(this.entity.fan.preset_mode_state_topic, fanPreset)
// Publish device attributes (batterylevel, tamper status)
this.publishAttributes()

View File

@@ -20,8 +20,8 @@ class FloodFreezeSensor extends RingSocketDevice {
publishData() {
const floodState = this.device.data.flood && this.device.data.flood.faulted ? 'ON' : 'OFF'
const freezeState = this.device.data.freeze && this.device.data.freeze.faulted ? 'ON' : 'OFF'
this.publishMqtt(this.entity.flood.state_topic, floodState)
this.publishMqtt(this.entity.freeze.state_topic, freezeState)
this.mqttPublish(this.entity.flood.state_topic, floodState)
this.mqttPublish(this.entity.freeze.state_topic, freezeState)
this.publishAttributes()
}
}

View File

@@ -17,11 +17,11 @@ class Keypad extends RingSocketDevice {
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.publishMqtt('homeassistant/light/'+this.locationId+'/'+this.deviceId+'_audio/config', '', false)
this.mqttPublish('homeassistant/light/'+this.locationId+'/'+this.deviceId+'_audio/config', '', false)
}
const currentVolume = (this.device.data.volume && !isNaN(this.device.data.volume) ? Math.round(100 * this.device.data.volume) : 0)
this.publishMqtt(this.entity.volume.state_topic, currentVolume.toString())
this.mqttPublish(this.entity.volume.state_topic, currentVolume.toString())
this.publishAttributes()
}

View File

@@ -23,7 +23,7 @@ class Lock extends RingSocketDevice {
default:
lockState = 'UNKNOWN'
}
this.publishMqtt(this.entity.lock.state_topic, lockState)
this.mqttPublish(this.entity.lock.state_topic, lockState)
this.publishAttributes()
}

View File

@@ -37,7 +37,7 @@ class ModesPanel extends RingPolledDevice {
default:
mqttMode = 'disarmed'
}
this.publishMqtt(this.entity.mode.state_topic, mqttMode)
this.mqttPublish(this.entity.mode.state_topic, mqttMode)
}
}

View File

@@ -14,7 +14,7 @@ class MotionSensor extends RingSocketDevice {
publishData() {
const sensorState = this.device.data.faulted ? 'ON' : 'OFF'
this.publishMqtt(this.entity.motion.state_topic, sensorState)
this.mqttPublish(this.entity.motion.state_topic, sensorState)
this.publishAttributes()
}
}

View File

@@ -15,8 +15,8 @@ class MultiLevelSwitch extends RingSocketDevice {
publishData() {
const switchState = this.device.data.on ? "ON" : "OFF"
const switchLevel = (this.device.data.level && !isNaN(this.device.data.level) ? Math.round(100 * this.device.data.level) : 0)
this.publishMqtt(this.entity.light.state_topic, switchState)
this.publishMqtt(this.entity.light.brightness_state_topic, switchLevel.toString())
this.mqttPublish(this.entity.light.state_topic, switchState)
this.mqttPublish(this.entity.light.brightness_state_topic, switchLevel.toString())
this.publishAttributes()
}

View File

@@ -25,7 +25,7 @@ class SecurityPanel extends RingSocketDevice {
state: false,
icon: 'mdi:transit-skip'
},
...this.config.enable_panic ? {
...utils.config.enable_panic ? {
police: {
component: 'switch',
name: `${this.device.location.name} Panic - Police`,
@@ -68,15 +68,15 @@ class SecurityPanel extends RingSocketDevice {
alarmMode = 'unknown'
}
}
this.publishMqtt(this.entity.alarm.state_topic, alarmMode)
this.mqttPublish(this.entity.alarm.state_topic, alarmMode)
const sirenState = (this.device.data.siren && this.device.data.siren.state === 'on') ? 'ON' : 'OFF'
this.publishMqtt(this.entity.siren.state_topic, sirenState)
this.mqttPublish(this.entity.siren.state_topic, sirenState)
const bypassState = this.entity.bypass.state ? 'ON' : 'OFF'
this.publishMqtt(this.entity.bypass.state_topic, bypassState)
this.mqttPublish(this.entity.bypass.state_topic, bypassState)
if (this.config.enable_panic) {
if (utils.config.enable_panic) {
let policeState = 'OFF'
let fireState = 'OFF'
const alarmState = this.device.data.alarmInfo ? this.device.data.alarmInfo.state : ''
@@ -93,8 +93,8 @@ class SecurityPanel extends RingSocketDevice {
fireState = 'ON'
this.debug('Fire alarm is active for '+this.device.location.name)
}
this.publishMqtt(this.entity.police.state_topic, policeState)
this.publishMqtt(this.entity.fire.state_topic, fireState)
this.mqttPublish(this.entity.police.state_topic, policeState)
this.mqttPublish(this.entity.fire.state_topic, fireState)
}
this.publishAttributes()
@@ -106,7 +106,7 @@ class SecurityPanel extends RingSocketDevice {
exitDelayMs = this.device.data.transitionDelayEndTimestamp - Date.now()
if (exitDelayMs <= 0) {
// Publish device sensor state
this.publishMqtt(this.entity.alarm.state_topic, 'armed_away')
this.mqttPublish(this.entity.alarm.state_topic, 'armed_away')
}
}
}

View File

@@ -27,14 +27,14 @@ class Siren extends RingSocketDevice {
const isPublish = data === undefined ? true : false
if (isPublish) {
// Eventually remove this but for now this attempts to delete the old siren binary_sensor
this.publishMqtt('homeassistant/binary_sensor/'+this.locationId+'/'+this.deviceId+'_siren/config', '', false)
this.mqttPublish('homeassistant/binary_sensor/'+this.locationId+'/'+this.deviceId+'_siren/config', '', false)
}
const sirenState = this.device.data.sirenStatus === 'active' ? 'ON' : 'OFF'
this.publishMqtt(this.entity.siren.state_topic, sirenState)
this.mqttPublish(this.entity.siren.state_topic, sirenState)
if (this.entity.hasOwnProperty('volume')) {
const currentVolume = (this.device.data.volume && !isNaN(this.device.data.volume) ? Math.round(1 * this.device.data.volume) : 0)
this.publishMqtt(this.entity.volume.state_topic, currentVolume)
this.mqttPublish(this.entity.volume.state_topic, currentVolume)
}
this.publishAttributes()
}

View File

@@ -14,7 +14,7 @@ class SmokeAlarm extends RingSocketDevice {
publishData() {
const smokeState = this.device.data.alarmStatus === 'active' ? 'ON' : 'OFF'
this.publishMqtt(this.entity.smoke.state_topic, smokeState)
this.mqttPublish(this.entity.smoke.state_topic, smokeState)
this.publishAttributes()
}
}

View File

@@ -20,8 +20,8 @@ class SmokeCoListener extends RingSocketDevice {
publishData() {
const smokeState = this.device.data.smoke && this.device.data.smoke.alarmStatus === 'active' ? 'ON' : 'OFF'
const coState = this.device.data.co && this.device.data.co.alarmStatus === 'active' ? 'ON' : 'OFF'
this.publishMqtt(this.entity.smoke.state_topic, smokeState)
this.publishMqtt(this.entity.co.state_topic, coState)
this.mqttPublish(this.entity.smoke.state_topic, smokeState)
this.mqttPublish(this.entity.co.state_topic, coState)
this.publishAttributes()
}
}

View File

@@ -13,7 +13,7 @@ class Switch extends RingSocketDevice {
}
publishData() {
this.publishMqtt(this.entity[this.component].state_topic, this.device.data.on ? "ON" : "OFF")
this.mqttPublish(this.entity[this.component].state_topic, this.device.data.on ? "ON" : "OFF")
this.publishAttributes()
}

View File

@@ -15,7 +15,7 @@ class TemperatureSensor extends RingSocketDevice {
publishData() {
const temperature = this.device.data.celsius.toString()
this.publishMqtt(this.entity.temperature.state_topic, temperature)
this.mqttPublish(this.entity.temperature.state_topic, temperature)
this.publishAttributes()
}
}

View File

@@ -69,11 +69,11 @@ class Thermostat extends RingSocketDevice {
const mode = this.data.currentMode()
await this.clearStateOnAutoModeSwitch(mode)
this.publishMqtt(this.entity.thermostat.mode_state_topic, mode)
this.mqttPublish(this.entity.thermostat.mode_state_topic, mode)
this.data.publishedMode = mode
this.publishSetpoints(mode)
this.publishMqtt(this.entity.thermostat.fan_mode_state_topic, this.data.fanMode())
this.publishMqtt(this.entity.thermostat.aux_state_topic, this.data.auxMode())
this.mqttPublish(this.entity.thermostat.fan_mode_state_topic, this.data.fanMode())
this.mqttPublish(this.entity.thermostat.aux_state_topic, this.data.auxMode())
this.publishOperatingMode()
if (isPublish) { this.publishTemperature() }
@@ -116,20 +116,20 @@ class Thermostat extends RingSocketDevice {
// as soon as the update is completed
this.data.autoSetPoint.low = this.device.data.setPoint-this.device.data.deadBand
this.data.autoSetPoint.high = this.device.data.setPoint+this.device.data.deadBand
this.publishMqtt(this.entity.thermostat.temperature_low_state_topic, this.data.autoSetPoint.low)
this.publishMqtt(this.entity.thermostat.temperature_high_state_topic, this.data.autoSetPoint.high)
this.mqttPublish(this.entity.thermostat.temperature_low_state_topic, this.data.autoSetPoint.low)
this.mqttPublish(this.entity.thermostat.temperature_high_state_topic, this.data.autoSetPoint.high)
}
} else {
this.publishMqtt(this.entity.thermostat.temperature_state_topic, this.data.setPoint())
this.mqttPublish(this.entity.thermostat.temperature_state_topic, this.data.setPoint())
}
}
publishOperatingMode() {
this.publishMqtt(this.entity.thermostat.action_topic, this.data.operatingMode())
this.mqttPublish(this.entity.thermostat.action_topic, this.data.operatingMode())
}
publishTemperature() {
this.publishMqtt(this.entity.thermostat.current_temperature_topic, this.data.temperature())
this.mqttPublish(this.entity.thermostat.current_temperature_topic, this.data.temperature())
}
// Process messages from MQTT command topic
@@ -163,14 +163,14 @@ class Thermostat extends RingSocketDevice {
const mode = value.toLowerCase()
switch(mode) {
case 'off':
this.publishMqtt(this.entity.thermostat.action_topic, mode)
this.mqttPublish(this.entity.thermostat.action_topic, mode)
case 'cool':
case 'heat':
case 'auto':
case 'aux':
if (this.entity.thermostat.modes.map(e => e.toLocaleLowerCase()).includes(mode) || mode === 'aux') {
this.device.setInfo({ device: { v1: { mode } } })
this.publishMqtt(this.entity.thermostat.mode_state_topic, mode)
this.mqttPublish(this.entity.thermostat.mode_state_topic, mode)
}
break;
default:
@@ -195,7 +195,7 @@ class Thermostat extends RingSocketDevice {
} else {
this.debug(`Received set target temperature to ${value}`)
this.device.setInfo({ device: { v1: { setPoint: Number(value) } } })
this.publishMqtt(this.entity.thermostat.temperature_state_topic, value)
this.mqttPublish(this.entity.thermostat.temperature_state_topic, value)
}
}
}
@@ -235,8 +235,8 @@ class Thermostat extends RingSocketDevice {
}
this.device.setInfo({ device: { v1: { setPoint, deadBand } } })
this.publishMqtt(this.entity.thermostat.temperature_low_state_topic, this.data.autoSetPoint.low)
this.publishMqtt(this.entity.thermostat.temperature_high_state_topic, this.data.autoSetPoint.high)
this.mqttPublish(this.entity.thermostat.temperature_low_state_topic, this.data.autoSetPoint.low)
this.mqttPublish(this.entity.thermostat.temperature_high_state_topic, this.data.autoSetPoint.high)
this.data.setPointInProgress = false
}
}
@@ -254,7 +254,7 @@ class Thermostat extends RingSocketDevice {
const fanMode = value.toLowerCase()
if (this.entity.thermostat.fan_modes.map(e => e.toLocaleLowerCase()).includes(fanMode)) {
this.device.setInfo({ device: { v1: { fanMode }}})
this.publishMqtt(this.entity.thermostat.fan_mode_state_topic, fanMode.replace(/^./, str => str.toUpperCase()))
this.mqttPublish(this.entity.thermostat.fan_mode_state_topic, fanMode.replace(/^./, str => str.toUpperCase()))
} else {
this.debug('Received invalid fan mode command')
}
@@ -268,7 +268,7 @@ class Thermostat extends RingSocketDevice {
case 'off':
const mode = auxMode === 'on' ? 'aux' : 'heat'
this.device.setInfo({ device: { v1: { mode } } })
this.publishMqtt(this.entity.thermostat.aux_state_topic, auxMode.toUpperCase())
this.mqttPublish(this.entity.thermostat.aux_state_topic, auxMode.toUpperCase())
break;
default:
this.debug('Received invalid aux mode command')

View File

@@ -8,12 +8,10 @@ class Config {
switch (process.env.RUNMODE) {
case 'docker':
this.file = '/data/config.json'
this.runMode = 'docker'
this.loadConfigEnv()
break;
case 'addon':
this.file = '/data/options.json'
this.runMode = 'addon'
this.loadConfigFile()
// For addon always set MQTT values from environment (set from HA API via bashio)
this.data.host = process.env.MQTTHOST
@@ -27,8 +25,8 @@ class Config {
} else {
this.file = require('path').dirname(require.main.filename)+'/config.json'
}
this.runMode = 'standard'
this.loadConfigFile()
process.env.RUNMODE = 'standard'
}
// If there's still no configured settings, force some defaults.
@@ -91,7 +89,7 @@ class Config {
}
async updateConfig() {
if (this.runMode === 'standard' && this.data.hasOwnProperty('ring_token')) {
if (process.env.RUNMODE === 'standard' && this.data.hasOwnProperty('ring_token')) {
try {
debug ('Updating config file to remove legacy ring_token value...')
delete this.data.ring_token

90
lib/mqtt.js Normal file
View File

@@ -0,0 +1,90 @@
const mqttApi = require('mqtt')
const debug = require('debug')('ring-mqtt')
const colors = require('colors/safe')
const utils = require('./utils')
class Mqtt {
constructor() {
this.client = false
this.connected = false
}
async startEventListeners() {
utils.event.on('ringState', async (state) => {
if (!this.client && state === 'connected') {
// Ring API connected, short wait before starting MQTT client
await utils.sleep(2)
this.init()
}
})
utils.event.on('mqttPublish', (topic, message) => {
this.client.publish(topic, (typeof message === 'number') ? message.toString() : message, { qos: 1 })
})
utils.event.on('mqttSubscribe', (topic) => {
this.client.subscribe(topic)
})
}
async init() {
try {
debug('Attempting connection to MQTT broker...')
this.client = await this.connect()
this.startMqttListeners()
// Subscribe to configured/default/legacay Home Assistant status topics
this.client.subscribe(utils.config.hass_topic)
this.client.subscribe('hass/status')
this.client.subscribe('hassio/status')
} catch (error) {
debug(error)
debug(colors.red(`Could not authenticate to MQTT broker. Please check the broker and configuration settings.`))
process.exit(1)
}
}
// Initiate the connection to MQTT broker
async connect() {
const mqttClient = await mqttApi.connect({
username: utils.config.mqtt_user ? utils.config.mqtt_user : null,
password: utils.config.mqtt_pass ? utils.config.mqtt_pass : null,
host: utils.config.host,
port: utils.config.port
});
return mqttClient
}
startMqttListeners() {
// On MQTT connect/reconnect send config/state information after delay
this.client.on('connect', () => {
if (!this.connected) {
this.connected = true
utils.event.emit('mqttState', 'connected')
}
})
this.client.on('reconnect', () => {
if (this.connected) {
debug('Connection to MQTT broker lost. Attempting to reconnect...')
} else {
debug('Attempting to reconnect to MQTT broker...')
}
this.connected = false
utils.event.emit('mqttState', 'disconnected')
})
this.client.on('error', (error) => {
debug('Unable to connect to MQTT broker', error.message)
this.connected = false
utils.event.emit('mqttState', 'disconnected')
})
// Process MQTT messages from subscribed command topics
this.client.on('message', (topic, message) => {
utils.event.emit('mqttMessage', topic, message)
})
}
}
module.exports = new Mqtt()

View File

@@ -1,9 +1,9 @@
const { RingApi, RingDeviceType, RingCamera, RingChime } = require('@tsightler/ring-client-api')
const mqttApi = require('mqtt')
const mqtt = require('./mqtt')
const debug = require('debug')('ring-mqtt')
const colors = require('colors/safe')
const utils = require('./utils.js')
const rss = require('./rtsp-simple-server.js')
const utils = require('./utils')
const rss = require('./rtsp-simple-server')
const BaseStation = require('../devices/base-station')
const Beam = require('../devices/beam')
const BeamOutdoorPlug = require('../devices/beam-outdoor-plug')
@@ -34,18 +34,47 @@ class RingMqtt {
this.devices = new Array()
this.mqttConnected = false
this.republishCount = 6 // Republish config/state this many times after startup or HA start/restart
this.startEventListeners()
mqtt.startEventListeners()
}
async init(ringAuth, config, tokenSource) {
this.config = config
async startEventListeners() {
utils.event.on('mqttState', (state) => {
if (state === 'connected') {
this.mqttConnected = true
debug('MQTT connection established, processing Ring locations...')
this.processLocations()
} else {
this.mqttConnected = false
}
})
utils.event.on('mqttMessage', (topic, message) => {
this.processMqttCommand(topic, message)
})
}
async init(state, generatedToken) {
const ringAuth = {
refreshToken: generatedToken ? generatedToken : state.ring_token,
systemId: state.systemId,
controlCenterDisplayName: (process.env.RUNMODE === 'addon') ? 'ring-mqtt-addon' : 'ring-mqtt',
...utils.config.enable_cameras ? { cameraStatusPollingSeconds: 20, cameraDingsPollingSeconds: 2 } : {},
...utils.config.enable_modes ? { locationModePollingSeconds: 20 } : {},
...!(utils.config.location_ids === undefined || utils.config.location_ids == 0) ? { locationIds: utils.config.location_ids } : {}
}
try {
debug(`Attempting connection to Ring API using ${tokenSource} refresh token.`)
debug(`Attempting connection to Ring API using ${generatedToken ? 'generated' : 'saved'} refresh token`)
this.client = new RingApi(ringAuth)
await this.client.getProfile()
utils.event.emit('ringState', 'connected')
debug(`Successfully established connection to Ring API using ${generatedToken ? 'generated' : 'saved'} token`)
} catch(error) {
this.client = false
debug(colors.brightYellow(error.message))
debug(colors.brightYellow(`Failed to establish connection to Ring API using ${tokenSource} refresh token.`))
debug(colors.brightYellow(`Failed to establish connection to Ring API using ${generatedToken ? 'generated' : 'saved'} refresh token`))
}
return this.client
}
@@ -133,14 +162,14 @@ class RingMqtt {
// Get all location devices and, if camera support is enabled, cameras and chimes
const devices = await location.getDevices()
if (this.config.enable_cameras) {
if (utils.config.enable_cameras) {
cameras = await location.cameras
chimes = await location.chimes
}
const allDevices = [...devices, ...cameras, ...chimes]
// Add modes panel, if configured and the location supports it
if (this.config.enable_modes && (await location.supportsLocationModeSwitching())) {
if (utils.config.enable_modes && (await location.supportsLocationModeSwitching())) {
allDevices.push({
deviceType: 'location.mode',
location: location,
@@ -211,8 +240,6 @@ class RingMqtt {
async getDevice(device, allDevices) {
const deviceInfo = {
device: device,
mqttClient: this.mqttClient,
config: this.config,
...allDevices.filter(d => d.data.parentZid === device.id).length
? { childDevices: allDevices.filter(d => d.data.parentZid === device.id) } : {},
...(device.data && device.data.hasOwnProperty('parentZid'))
@@ -326,75 +353,23 @@ class RingMqtt {
}
}
async initMqtt() {
// Initiate connection to MQTT broker
try {
debug('Attempting connection to MQTT broker...')
async processMqttCommand(topic, message) {
message = message.toString()
if (topic === utils.config.hass_topic || topic === 'hass/status' || topic === 'hassio/status') {
this.republishDevices()
} else {
// Parse topic to get location/device ID
const deviceTopicLevels = (topic.substring(utils.config.ring_topic.length)).replace(/^\/+/, '').split('/')
const locationId = deviceTopicLevels[0]
const deviceId = deviceTopicLevels[2]
this.mqttClient = await mqttApi.connect({
username: this.config.mqtt_user ? this.config.mqtt_user : null,
password: this.config.mqtt_pass ? this.config.mqtt_pass : null,
host: this.config.host,
port: this.config.port
});
// Monitor configured/default Home Assistant status topic
this.mqttClient.subscribe(this.config.hass_topic)
// Monitor legacy Home Assistant status topics
this.mqttClient.subscribe('hass/status')
this.mqttClient.subscribe('hassio/status')
this.mqttClient.on('connect', async () => {
if (!this.mqttConnected) {
this.mqttConnected = true
debug('MQTT connection established, processing Ring location...')
}
this.processLocations()
})
this.mqttClient.on('reconnect', () => {
debug(this.mqttConnected
? 'Connection to MQTT broker lost. Attempting to reconnect...'
: 'Attempting to reconnect to MQTT broker...'
)
this.mqttConnected = false
})
this.mqttClient.on('error', (error) => {
debug('Unable to connect to MQTT broker.', error.message)
this.mqttConnected = false
})
// Process MQTT messages from subscribed command topics
this.mqttClient.on('message', async (topic, message) => {
message = message.toString()
if (topic === this.config.hass_topic || topic === 'hass/status' || topic === 'hassio/status') {
debug(`Home Assistant state topic ${topic} received message: ${message}`)
if (message === 'online') {
this.republishDevices()
}
} else {
// Parse topic to get location/device ID
const ringTopicLevels = (this.config.ring_topic).split('/').length
const splitTopic = topic.split('/')
const locationId = splitTopic[ringTopicLevels]
const deviceId = splitTopic[ringTopicLevels + 2]
// Find existing device by matching location & device ID
const cmdDevice = this.devices.find(d => (d.deviceId == deviceId && d.locationId == locationId))
if (cmdDevice) {
const componentCommand = topic.split("/").slice(-2).join("/")
cmdDevice.processCommand(message, componentCommand)
} else {
debug(`Received MQTT message for device Id ${deviceId} at location Id ${locationId} but could not find matching device`)
}
}
})
} catch (error) {
debug(error)
debug(colors.red(`Could not authenticate to MQTT broker. Please check the broker and configuration settings.`))
process.exit(1)
// Find existing device by matching location & device ID
const cmdDevice = this.devices.find(d => (d.deviceId == deviceId && d.locationId == locationId))
if (cmdDevice) {
cmdDevice.processCommand(message, topic.split("/").slice(-2).join("/"))
} else {
debug('Received MQTT message for device Id '+deviceId+' at location Id '+locationId+' but could not find matching device')
}
}
}

View File

@@ -1,6 +1,6 @@
const debug = require('debug')('ring-rtsp')
const colors = require('colors/safe')
const utils = require('./utils.js')
const utils = require('./utils')
const { spawn } = require('child_process')
const readline = require('readline')
const got = require('got')
@@ -72,11 +72,11 @@ class RtspSimpleServer {
runOnDemandRestart: false,
runOnDemandStartTimeout: '10s',
runOnDemandCloseAfter: '5s',
...(camera.config.livestream_user && camera.config.livestream_pass) ? {
publishUser: camera.config.livestream_user,
publishPass: camera.config.livestream_pass,
readUser: camera.config.livestream_user,
readPass: camera.config.livestream_pass
...(utils.config.livestream_user && utils.config.livestream_pass) ? {
publishUser: utils.config.livestream_user,
publishPass: utils.config.livestream_pass,
readUser: utils.config.livestream_user,
readPass: utils.config.livestream_pass
} : {}
}
@@ -93,11 +93,11 @@ class RtspSimpleServer {
runOnDemandRestart: false,
runOnDemandStartTimeout: '10s',
runOnDemandCloseAfter: '5s',
...(camera.config.livestream_user && camera.config.livestream_pass) ? {
publishUser: camera.config.livestream_user,
publishPass: camera.config.livestream_pass,
readUser: camera.config.livestream_user,
readPass: camera.config.livestream_pass
...(utils.config.livestream_user && utils.config.livestream_pass) ? {
publishUser: utils.config.livestream_user,
publishPass: utils.config.livestream_pass,
readUser: utils.config.livestream_user,
readPass: utils.config.livestream_pass
} : {}
}

View File

@@ -16,7 +16,7 @@ class State {
async init(config) {
this.config = config
this.file = (this.config.runMode === 'standard')
this.file = (process.env.RUNMODE === 'standard')
? require('path').dirname(require.main.filename)+'/ring-state.json'
: this.file = '/data/ring-state.json'
await this.loadStateData()
@@ -43,7 +43,7 @@ class State {
async initStateData() {
this.data.systemId = (createHash('sha256').update(randomBytes(32)).digest('hex'))
if (this.config.runMode === 'standard' && this.config.data.hasOwnProperty('ring_token') && this.config.data.ring_token) {
if (process.env.RUNMODE === 'standard' && this.config.data.hasOwnProperty('ring_token') && this.config.data.ring_token) {
debug(colors.brightYellow('State file '+this.file+' not found, creating new state file using existing ring_token from config file.'))
this.updateToken(this.config.data.ring_token, true)
await this.config.updateConfig()

View File

@@ -1,5 +1,6 @@
const { RingRestClient } = require('../node_modules/@tsightler/ring-client-api/lib/api/rest-client')
const debug = require('debug')('ring-mqtt')
const utils = require('./utils')
const express = require('express')
const bodyParser = require("body-parser")
@@ -7,27 +8,23 @@ class TokenApp {
constructor() {
this.app = express()
this.listener = false
this.ringConnected = false
// Helper property to pass values between main code and web server
this.token = {
connected: '',
generatedInternal: '',
generatedListener: function(val) {},
set generated(val) {
this.generatedInternal = val;
this.generatedListener(val);
},
get generated() {
return this.generatedInternal;
},
registerListener: function(listener) {
this.generatedListener = listener;
}
if (process.env.RUNMODE === 'addon') {
tokenApp.start()
}
utils.event.on('ringState', async (state) => {
if (state === 'connected') {
this.ringConnected = true
} else {
this.ringConnected = false
}
})
}
// Super simple web service to acquire refresh tokens
async start(runMode) {
async start() {
if (this.listener) {
return
}
@@ -36,15 +33,17 @@ class TokenApp {
let restClient
this.listener = this.app.listen(55123, () => {
if (runMode !== 'addon') {
debug('Use http://<host_ip_address>:55123/ to generate a valid token.')
if (process.env.RUNMODE === 'addon') {
debug('Open the Web UI from the addon Info tab to generate an authentication token.')
} else {
debug('Use the Web UI at http://<host_ip_address>:55123/ to generate an authentication token.')
}
})
this.app.use(bodyParser.urlencoded({ extended: false }))
this.app.get('/', (req, res) => {
if (!this.token.connected) {
if (!this.connected) {
res.sendFile('account.html', {root: webdir})
} else {
res.sendFile('connected.html', {root: webdir})
@@ -78,8 +77,8 @@ class TokenApp {
const code = req.body.code
try {
generatedToken = await restClient.getAuth(code)
} catch(_) {
generatedToken = ''
} catch(err) {
generatedToken = false
const errormsg = 'The 2FA code was not accepted, please verify the code and try again.'
debug(errormsg)
res.cookie('error', errormsg, { maxAge: 1000, encode: String })
@@ -87,7 +86,7 @@ class TokenApp {
}
if (generatedToken) {
res.sendFile('restart.html', {root: webdir})
this.token.generated = generatedToken.refresh_token
utils.event.emit('generatedToken', generatedToken.refresh_token)
}
})
}

View File

@@ -3,13 +3,17 @@ const debug = {
attr: require('debug')('ring-attr'),
disc: require('debug')('ring-disc')
}
const colors = require('colors/safe')
const config = require('./config')
const dns = require('dns')
const os = require('os')
const { promisify } = require('util')
const EventEmitter = require('events').EventEmitter
class Utils {
// Define a few helper variables for sharing
event = new EventEmitter()
config = config.data
class Utils
{
// Sleep function (seconds)
sleep(sec) {
return this.msleep(sec*1000)
@@ -45,20 +49,9 @@ class Utils
}
}
log(message, level, category) {
category = category ? category : 'mqtt'
switch (level) {
case 'info':
debug[category](colors.green(`[${this.deviceData.name}] `)+message)
break;
case 'warn':
debug[category](colors.brightYellow(`[${this.deviceData.name}] `)+message)
break;
case 'error':
debug[category](colors.brightRed(`[${this.deviceData.name}] `)+message)
default:
debug[category](message)
}
debug(message, debugType) {
debugType = debugType ? debugType : 'mqtt'
debug[debugType](message)
}
}

View File

@@ -1,13 +1,12 @@
#!/usr/bin/env node
const config = require('./lib/config')
const state = require('./lib/state')
const ring = require('./lib/ring')
const isOnline = require('is-online')
const debug = require('debug')('ring-mqtt')
const colors = require('colors/safe')
const utils = require('./lib/utils.js')
const tokenApp = require('./lib/tokenapp.js')
const utils = require('./lib/utils')
const tokenApp = require('./lib/tokenapp')
// Setup Exit Handlers
process.on('exit', processExit.bind(null, 0))
@@ -69,52 +68,32 @@ const main = async(generatedToken) => {
await utils.sleep(10)
}
const ringAuth = {
refreshToken: generatedToken ? generatedToken : state.data.ring_token,
systemId: state.data.systemId,
controlCenterDisplayName: (config.runMode === 'addon') ? 'ring-mqtt-addon' : 'ring-mqtt',
...config.data.enable_cameras ? { cameraStatusPollingSeconds: 20, cameraDingsPollingSeconds: 2 } : {},
...config.data.enable_modes ? { locationModePollingSeconds: 20 } : {},
...!(config.data.location_ids === undefined || config.data.location_ids == 0) ? { locationIds: config.data.location_ids } : {}
}
const tokenSource = generatedToken ? 'generated' : 'saved'
if (await ring.init(ringAuth, config.data, tokenSource)) {
debug(`Successfully established connection to Ring API using ${tokenSource} token`)
if (await ring.init(state.data, generatedToken)) {
// Subscribe to token update events and save new tokens to state file
ring.client.onRefreshTokenUpdated.subscribe(({ newRefreshToken, oldRefreshToken }) => {
state.updateToken(newRefreshToken, oldRefreshToken)
})
// Only leave the web UI active if this is the addon
if (config.runMode !== 'addon') {
if (process.env.RUNMODE === 'addon') {
tokenApp.stop()
}
// Connection to Ring API is successful, pause for a few seconds and then initialize MQTT connection
await utils.sleep(2)
ring.initMqtt()
} else {
debug(colors.brightRed('Failed to connect to Ring API using saved token, generate a new token using the Web UI.'))
debug(colors.brightRed('Authentication will be automatically retried in 60 seconds using the existing token.'))
tokenApp.start(config.runMode)
tokenApp.start()
await utils.sleep(60)
main()
}
} else {
// If a refresh token was not found, start Web UI for token generator
debug(colors.brightRed('No refresh token was found in state file, generate a token using the Web UI.'))
tokenApp.start(config.runMode)
tokenApp.start()
}
}
if (config.runMode === 'addon') {
tokenApp.start(config.runMode)
}
// Subscribe to token updates from token Web UI
tokenApp.token.registerListener(function(generatedToken) {
// Listen for token updates from token Web UI
utils.event.on('generatedToken', (generatedToken) => {
main(generatedToken)
})