mirror of
https://github.com/tsightler/ring-mqtt.git
synced 2025-10-13 20:44:03 +08:00
Release 5.0.0
This commit is contained in:
@@ -2,13 +2,13 @@ const RingSocketDevice = require('./base-socket-device')
|
|||||||
const { RingDeviceType } = require('@tsightler/ring-client-api')
|
const { RingDeviceType } = require('@tsightler/ring-client-api')
|
||||||
|
|
||||||
class BeamOutdoorPlug extends RingSocketDevice {
|
class BeamOutdoorPlug extends RingSocketDevice {
|
||||||
constructor(deviceInfo, allDevices) {
|
constructor(deviceInfo) {
|
||||||
super(deviceInfo, 'lighting')
|
super(deviceInfo, 'lighting')
|
||||||
this.deviceData.mdl = 'Outdoor Smart Plug'
|
this.deviceData.mdl = 'Outdoor Smart Plug'
|
||||||
|
|
||||||
this.childDevices = {
|
this.childDevices = {
|
||||||
outlet1: allDevices.find(d => d.data.parentZid === this.device.id && d.deviceType === RingDeviceType.BeamsSwitch && d.data.relToParentZid === "1"),
|
outlet1: deviceInfo.allDevices.find(d => d.data.parentZid === this.device.id && d.deviceType === RingDeviceType.BeamsSwitch && d.data.relToParentZid === "1"),
|
||||||
outlet2: allDevices.find(d => d.data.parentZid === this.device.id && d.deviceType === RingDeviceType.BeamsSwitch && d.data.relToParentZid === "2")
|
outlet2: deviceInfo.allDevices.find(d => d.data.parentZid === this.device.id && d.deviceType === RingDeviceType.BeamsSwitch && d.data.relToParentZid === "2")
|
||||||
}
|
}
|
||||||
|
|
||||||
this.entity.outlet1 = {
|
this.entity.outlet1 = {
|
||||||
|
@@ -2,10 +2,10 @@ const RingSocketDevice = require('./base-socket-device')
|
|||||||
const { RingDeviceType } = require('@tsightler/ring-client-api')
|
const { RingDeviceType } = require('@tsightler/ring-client-api')
|
||||||
|
|
||||||
class CoAlarm extends RingSocketDevice {
|
class CoAlarm extends RingSocketDevice {
|
||||||
constructor(deviceInfo, allDevices) {
|
constructor(deviceInfo) {
|
||||||
super(deviceInfo, 'alarm')
|
super(deviceInfo, 'alarm')
|
||||||
this.deviceData.mdl = 'CO Alarm'
|
this.deviceData.mdl = 'CO Alarm'
|
||||||
const parentDevice = allDevices.find(d => d.id === this.device.data.parentZid && d.deviceType === RingDeviceType.SmokeAlarm)
|
const parentDevice = deviceInfo.allDevices.find(d => d.id === this.device.data.parentZid && d.deviceType === RingDeviceType.SmokeAlarm)
|
||||||
this.deviceData.mf = (parentDevice && parentDevice.data && parentDevice.data.manufacturerName)
|
this.deviceData.mf = (parentDevice && parentDevice.data && parentDevice.data.manufacturerName)
|
||||||
? parentDevice.data.manufacturerName
|
? parentDevice.data.manufacturerName
|
||||||
: 'Ring'
|
: 'Ring'
|
||||||
|
@@ -3,13 +3,13 @@ const { RingDeviceType } = require('@tsightler/ring-client-api')
|
|||||||
const utils = require( '../lib/utils' )
|
const utils = require( '../lib/utils' )
|
||||||
|
|
||||||
class Thermostat extends RingSocketDevice {
|
class Thermostat extends RingSocketDevice {
|
||||||
constructor(deviceInfo, allDevices) {
|
constructor(deviceInfo) {
|
||||||
super(deviceInfo, 'alarm')
|
super(deviceInfo, 'alarm')
|
||||||
this.deviceData.mdl = 'Thermostat'
|
this.deviceData.mdl = 'Thermostat'
|
||||||
|
|
||||||
this.childDevices = {
|
this.childDevices = {
|
||||||
operatingStatus: allDevices.find(d => d.data.parentZid === this.device.id && d.deviceType === 'thermostat-operating-status'),
|
operatingStatus: deviceInfo.allDevices.find(d => d.data.parentZid === this.device.id && d.deviceType === 'thermostat-operating-status'),
|
||||||
temperatureSensor: allDevices.find(d => d.data.parentZid === this.device.id && d.deviceType === RingDeviceType.TemperatureSensor)
|
temperatureSensor: deviceInfo.allDevices.find(d => d.data.parentZid === this.device.id && d.deviceType === RingDeviceType.TemperatureSensor)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.entity.thermostat = {
|
this.entity.thermostat = {
|
||||||
|
126
lib/mqtt.js
Normal file
126
lib/mqtt.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
const mqttApi = require('mqtt')
|
||||||
|
const debug = require('debug')('ring-mqtt')
|
||||||
|
const colors = require('colors/safe')
|
||||||
|
const utils = require('./utils.js')
|
||||||
|
|
||||||
|
class Mqtt {
|
||||||
|
constructor() {
|
||||||
|
this.connected = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(ring, config) {
|
||||||
|
this.ring = ring
|
||||||
|
this.config = config
|
||||||
|
// Initiate connection to MQTT broker
|
||||||
|
try {
|
||||||
|
debug('Starting connection to MQTT broker...')
|
||||||
|
this.client = await this.connect()
|
||||||
|
if (this.client.connected) {
|
||||||
|
this.connected = true
|
||||||
|
debug('MQTT connection established, sending config/state information in 5 seconds.')
|
||||||
|
}
|
||||||
|
// Monitor configured/default Home Assistant status topic
|
||||||
|
this.client.subscribe(this.config.hass_topic)
|
||||||
|
// Monitor legacy Home Assistant status topics
|
||||||
|
this.client.subscribe('hass/status')
|
||||||
|
this.client.subscribe('hassio/status')
|
||||||
|
this.start()
|
||||||
|
} catch (error) {
|
||||||
|
debug(error)
|
||||||
|
debug(colors.red('Couldn\'t authenticate to MQTT broker. Please check the broker and configuration settings.'))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate the connection to MQTT broker
|
||||||
|
connect() {
|
||||||
|
const mqtt_user = this.config.mqtt_user ? this.config.mqtt_user : null
|
||||||
|
const mqtt_pass = this.config.mqtt_pass ? this.config.mqtt_pass : null
|
||||||
|
const mqtt = mqttApi.connect({
|
||||||
|
host: this.config.host,
|
||||||
|
port: this.config.port,
|
||||||
|
username: mqtt_user,
|
||||||
|
password: mqtt_pass
|
||||||
|
});
|
||||||
|
return mqtt
|
||||||
|
}
|
||||||
|
|
||||||
|
// MQTT initialization successful, setup actions for MQTT events
|
||||||
|
start() {
|
||||||
|
const mqttLib = this
|
||||||
|
// On MQTT connect/reconnect send config/state information after delay
|
||||||
|
this.client.on('connect', async function () {
|
||||||
|
if (!mqttLib.connected) {
|
||||||
|
mqttLib.connected = true
|
||||||
|
debug('MQTT connection established, processing locations...')
|
||||||
|
}
|
||||||
|
mqttLib.ring.processLocations(mqttLib)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.client.on('reconnect', function () {
|
||||||
|
if (mqttLib.connected) {
|
||||||
|
debug('Connection to MQTT broker lost. Attempting to reconnect...')
|
||||||
|
} else {
|
||||||
|
debug('Attempting to reconnect to MQTT broker...')
|
||||||
|
}
|
||||||
|
mqttLib.connected = false
|
||||||
|
})
|
||||||
|
|
||||||
|
this.client.on('error', function (error) {
|
||||||
|
debug('Unable to connect to MQTT broker.', error.message)
|
||||||
|
mqttLib.connected = false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process MQTT messages from subscribed command topics
|
||||||
|
this.client.on('message', async function (topic, message) {
|
||||||
|
mqttLib.processMessage(topic, message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
connected() {
|
||||||
|
if (!this.connected) {
|
||||||
|
this.connected = true
|
||||||
|
debug('MQTT connection established, processing locations...')
|
||||||
|
}
|
||||||
|
this.ring.processLocations(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process received MQTT command
|
||||||
|
async processMessage(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') {
|
||||||
|
// Republish devices and state if restart of HA is detected
|
||||||
|
if (this.republishCount > 0) {
|
||||||
|
debug('Home Assisntat restart detected during existing republish cycle')
|
||||||
|
debug('Resetting device config/state republish count')
|
||||||
|
this.republishCount = 6
|
||||||
|
} else {
|
||||||
|
debug('Home Assistant restart detected, resending device config/state in 5 seconds')
|
||||||
|
await utils.sleep(5)
|
||||||
|
this.republishCount = 6
|
||||||
|
this.ring.processLocations(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Parse topic to get location/device ID
|
||||||
|
const ringTopicLevels = (this.config.data.ring_topic).split('/').length
|
||||||
|
splitTopic = topic.split('/')
|
||||||
|
const locationId = splitTopic[ringTopicLevels]
|
||||||
|
const deviceId = splitTopic[ringTopicLevels + 2]
|
||||||
|
|
||||||
|
// Find existing device by matching location & device ID
|
||||||
|
const cmdDevice = this.ring.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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Mqtt()
|
302
lib/ring.js
Normal file
302
lib/ring.js
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
const { RingApi, RingDeviceType, RingCamera, RingChime } = require('@tsightler/ring-client-api')
|
||||||
|
const debug = require('debug')('ring-mqtt')
|
||||||
|
const colors = require('colors/safe')
|
||||||
|
const utils = require('./utils.js')
|
||||||
|
const BaseStation = require('../devices/base-station')
|
||||||
|
const Beam = require('../devices/beam')
|
||||||
|
const BeamOutdoorPlug = require('../devices/beam-outdoor-plug')
|
||||||
|
const Bridge = require('../devices/bridge')
|
||||||
|
const Camera = require('../devices/camera')
|
||||||
|
const Chime = require('../devices/chime')
|
||||||
|
const CoAlarm = require('../devices/co-alarm')
|
||||||
|
const ContactSensor = require('../devices/contact-sensor')
|
||||||
|
const Fan = require('../devices/fan')
|
||||||
|
const FloodFreezeSensor = require('../devices/flood-freeze-sensor')
|
||||||
|
const Keypad = require('../devices/keypad')
|
||||||
|
const Lock = require('../devices/lock')
|
||||||
|
const ModesPanel = require('../devices/modes-panel')
|
||||||
|
const MotionSensor = require('../devices/motion-sensor')
|
||||||
|
const MultiLevelSwitch = require('../devices/multi-level-switch')
|
||||||
|
const RangeExtender = require('../devices/range-extender')
|
||||||
|
const SecurityPanel = require('../devices/security-panel')
|
||||||
|
const Siren = require('../devices/siren')
|
||||||
|
const SmokeAlarm = require('../devices/smoke-alarm')
|
||||||
|
const SmokeCoListener = require('../devices/smoke-co-listener')
|
||||||
|
const Switch = require('../devices/switch')
|
||||||
|
const TemperatureSensor = require('../devices/temperature-sensor')
|
||||||
|
const Thermostat = require('../devices/thermostat')
|
||||||
|
|
||||||
|
class Ring {
|
||||||
|
constructor() {
|
||||||
|
this.locations = new Array()
|
||||||
|
this.devices = new Array()
|
||||||
|
this.republishCount = 6 // Republish config/state this many times after startup or HA start/restart
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(ringAuth, config, rssLib, tokenSource) {
|
||||||
|
this.config = config
|
||||||
|
this.rss = rssLib
|
||||||
|
try {
|
||||||
|
debug(`Attempting connection to Ring API using ${tokenSource} refresh token.`)
|
||||||
|
this.client = new RingApi(ringAuth)
|
||||||
|
await this.client.getProfile()
|
||||||
|
} catch(error) {
|
||||||
|
this.client = false
|
||||||
|
debug(colors.brightYellow(error.message))
|
||||||
|
debug(colors.brightYellow('Failed to establed connection to Ring API using '+tokenSource+' refresh token.'))
|
||||||
|
}
|
||||||
|
return this.client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through each location and call publishLocation for supported/connected devices
|
||||||
|
async processLocations(mqttLib) {
|
||||||
|
this.mqtt = mqttLib
|
||||||
|
|
||||||
|
// Update Ring location and device data
|
||||||
|
await this.updateRingData()
|
||||||
|
|
||||||
|
// For each location get existing alarm & camera devices
|
||||||
|
this.locations.forEach(async location => {
|
||||||
|
const devices = await this.devices.filter(d => d.locationId == location.locationId)
|
||||||
|
// If location has devices publish them
|
||||||
|
if (devices && devices.length) {
|
||||||
|
if (location.hasHubs && !location.isSubscribed) {
|
||||||
|
// Location has an alarm or smart bridge so subscribe to websocket connection monitor
|
||||||
|
location.isSubscribed = true
|
||||||
|
location.onConnected.subscribe(async connected => {
|
||||||
|
if (connected) {
|
||||||
|
// Only publish if previous state was actually disconnected
|
||||||
|
if (!location.isConnected) {
|
||||||
|
location.isConnected = true
|
||||||
|
debug('Websocket for location id '+location.locationId+' is connected')
|
||||||
|
this.publishDevices(location)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Wait 30 seconds before setting devices offline in case disconnect is transient
|
||||||
|
// Keeps from creating "unknown" state for sensors if connection error is short lived
|
||||||
|
await utils.sleep(30)
|
||||||
|
if (!location.onConnected._value) {
|
||||||
|
location.isConnected = false
|
||||||
|
debug('Websocket for location id '+location.locationId+' is disconnected')
|
||||||
|
this.devices.forEach(device => {
|
||||||
|
if (device.locationId == location.locationId && !device.camera) {
|
||||||
|
device.offline()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.publishDevices(location)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug('No devices found for location ID '+location.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all Ring location/device data
|
||||||
|
async updateRingData() {
|
||||||
|
// Small delay makes debug output more readable
|
||||||
|
await utils.sleep(1)
|
||||||
|
|
||||||
|
// Get all Ring locations
|
||||||
|
const locations = await this.client.getLocations()
|
||||||
|
|
||||||
|
// Loop through each location and update stored locations/devices
|
||||||
|
for (const location of locations) {
|
||||||
|
let cameras = new Array()
|
||||||
|
let chimes = new Array()
|
||||||
|
const unsupportedDevices = new Array()
|
||||||
|
|
||||||
|
debug(colors.green('-'.repeat(80)))
|
||||||
|
// If new location, set custom properties and add to location list
|
||||||
|
if (this.locations.find(l => l.locationId == location.locationId)) {
|
||||||
|
debug(colors.white('Existing location: ')+colors.green(location.name)+colors.cyan(` (${location.id})`))
|
||||||
|
} else {
|
||||||
|
debug(colors.white('New location: ')+colors.green(location.name)+colors.cyan(` (${location.id})`))
|
||||||
|
location.isSubscribed = false
|
||||||
|
location.isConnected = false
|
||||||
|
this.locations.push(location)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all location devices and, if configured, cameras
|
||||||
|
const devices = await location.getDevices()
|
||||||
|
if (this.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())) {
|
||||||
|
allDevices.push({
|
||||||
|
deviceType: 'location.mode',
|
||||||
|
location: location,
|
||||||
|
id: location.locationId + '_mode',
|
||||||
|
onData: location.onLocationMode,
|
||||||
|
data: {
|
||||||
|
device_id: location.locationId + '_mode',
|
||||||
|
location_id: location.locationId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Ring devices for location
|
||||||
|
for (const device of allDevices) {
|
||||||
|
const deviceId = (device instanceof RingCamera || device instanceof RingChime) ? device.data.device_id : device.id
|
||||||
|
let foundMessage = ' New device: '
|
||||||
|
let ringDevice = this.devices.find(d => d.deviceId === deviceId && d.locationId === location.locationId)
|
||||||
|
if (ringDevice) {
|
||||||
|
foundMessage = ' Existing device: '
|
||||||
|
} else {
|
||||||
|
ringDevice = await this.getDevice(device, allDevices)
|
||||||
|
switch (ringDevice) {
|
||||||
|
case 'not-supported':
|
||||||
|
// Save unsupported device type
|
||||||
|
unsupportedDevices.push(device.deviceType)
|
||||||
|
case 'ignore':
|
||||||
|
ringDevice=false
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
this.devices.push(ringDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ringDevice) {
|
||||||
|
debug(colors.white(foundMessage)+colors.green(`${ringDevice.deviceData.name}`)+colors.cyan(' ('+ringDevice.deviceId+')'))
|
||||||
|
if (ringDevice.hasOwnProperty('childDevices')) {
|
||||||
|
const indent = ' '.repeat(foundMessage.length-4)
|
||||||
|
debug(colors.white(`${indent}│ `)+colors.gray(ringDevice.device.deviceType))
|
||||||
|
let keys = Object.keys(ringDevice.childDevices).length
|
||||||
|
Object.keys(ringDevice.childDevices).forEach(key => {
|
||||||
|
debug(colors.white(`${indent}${(keys > 1) ? '├─: ' : '└─: '}`)+colors.green(`${ringDevice.childDevices[key].name}`)+colors.cyan(` (${ringDevice.childDevices[key].id})`))
|
||||||
|
debug(colors.white(`${indent}${(keys > 1) ? '│ ' : ' '}`)+colors.gray(ringDevice.childDevices[key].deviceType))
|
||||||
|
keys--
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const indent = ' '.repeat(foundMessage.length)
|
||||||
|
debug(colors.gray(`${indent}${ringDevice.device.deviceType}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Output any unsupported devices to debug with warning
|
||||||
|
unsupportedDevices.forEach(deviceType => {
|
||||||
|
debug(colors.yellow(' Unsupported device: '+deviceType))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
debug(colors.green('-'.repeat(80)))
|
||||||
|
debug('Ring location/device data updated, sleeping for 5 seconds.')
|
||||||
|
await utils.sleep(2)
|
||||||
|
const cameras = await this.devices.filter(d => d.device instanceof RingCamera)
|
||||||
|
if (cameras.length > 0 && !rss.started) {
|
||||||
|
await this.rss.start(cameras)
|
||||||
|
}
|
||||||
|
await utils.sleep(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Return supported device
|
||||||
|
async getDevice(device, allDevices) {
|
||||||
|
const deviceInfo = {
|
||||||
|
device: device,
|
||||||
|
allDevices: allDevices,
|
||||||
|
mqttClient: this.mqtt.client,
|
||||||
|
config: this.config
|
||||||
|
}
|
||||||
|
if (device instanceof RingCamera) {
|
||||||
|
return new Camera(deviceInfo)
|
||||||
|
} else if (device instanceof RingChime) {
|
||||||
|
return new Chime(deviceInfo)
|
||||||
|
} else if (/^lock($|\.)/.test(device.deviceType)) {
|
||||||
|
return new Lock(deviceInfo)
|
||||||
|
}
|
||||||
|
switch (device.deviceType) {
|
||||||
|
case RingDeviceType.ContactSensor:
|
||||||
|
case RingDeviceType.RetrofitZone:
|
||||||
|
case RingDeviceType.TiltSensor:
|
||||||
|
case RingDeviceType.GlassbreakSensor:
|
||||||
|
return new ContactSensor(deviceInfo)
|
||||||
|
case RingDeviceType.MotionSensor:
|
||||||
|
return new MotionSensor(deviceInfo)
|
||||||
|
case RingDeviceType.FloodFreezeSensor:
|
||||||
|
return new FloodFreezeSensor(deviceInfo)
|
||||||
|
case RingDeviceType.SecurityPanel:
|
||||||
|
return new SecurityPanel(deviceInfo)
|
||||||
|
case RingDeviceType.SmokeAlarm:
|
||||||
|
return new SmokeAlarm(deviceInfo)
|
||||||
|
case RingDeviceType.CoAlarm:
|
||||||
|
return new CoAlarm(deviceInfo)
|
||||||
|
case RingDeviceType.SmokeCoListener:
|
||||||
|
return new SmokeCoListener(deviceInfo)
|
||||||
|
case RingDeviceType.BeamsMotionSensor:
|
||||||
|
case RingDeviceType.BeamsMultiLevelSwitch:
|
||||||
|
case RingDeviceType.BeamsTransformerSwitch:
|
||||||
|
case RingDeviceType.BeamsLightGroupSwitch:
|
||||||
|
return new Beam(deviceInfo)
|
||||||
|
case RingDeviceType.BeamsDevice:
|
||||||
|
return new BeamOutdoorPlug(deviceInfo)
|
||||||
|
case RingDeviceType.MultiLevelSwitch:
|
||||||
|
return (device.categoryId === 17) ? new Fan(deviceInfo) : new MultiLevelSwitch(deviceInfo)
|
||||||
|
case RingDeviceType.Switch:
|
||||||
|
return new Switch(deviceInfo)
|
||||||
|
case RingDeviceType.Keypad:
|
||||||
|
return new Keypad(deviceInfo)
|
||||||
|
case RingDeviceType.BaseStation:
|
||||||
|
return new BaseStation(deviceInfo)
|
||||||
|
case RingDeviceType.RangeExtender:
|
||||||
|
return new RangeExtender(deviceInfo)
|
||||||
|
case RingDeviceType.RingNetAdapter:
|
||||||
|
return new Bridge(deviceInfo)
|
||||||
|
case RingDeviceType.Sensor:
|
||||||
|
return newDevice = (device.name.toLowerCase().includes('motion'))
|
||||||
|
? new MotionSensor(deviceInfo)
|
||||||
|
: new ContactSensor(deviceInfo)
|
||||||
|
case 'location.mode':
|
||||||
|
return new ModesPanel(deviceInfo)
|
||||||
|
case 'siren':
|
||||||
|
case 'siren.outdoor-strobe':
|
||||||
|
return new Siren(deviceInfo)
|
||||||
|
case RingDeviceType.Thermostat:
|
||||||
|
return new Thermostat(deviceInfo)
|
||||||
|
case RingDeviceType.TemperatureSensor:
|
||||||
|
// If this is a thermostat component, ignore this device
|
||||||
|
if (allDevices.find(d => d.id === device.data.parentZid && d.deviceType === RingDeviceType.Thermostat)) {
|
||||||
|
return 'ignore'
|
||||||
|
} else {
|
||||||
|
return new TemperatureSensor(deviceInfo)
|
||||||
|
}
|
||||||
|
case RingDeviceType.BeamsSwitch:
|
||||||
|
case 'thermostat-operating-status':
|
||||||
|
case 'access-code':
|
||||||
|
case 'access-code.vault':
|
||||||
|
case 'adapter.sidewalk':
|
||||||
|
case 'adapter.zigbee':
|
||||||
|
case 'adapter.zwave':
|
||||||
|
return "ignore"
|
||||||
|
}
|
||||||
|
return "not-supported"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish devices/cameras for given location
|
||||||
|
async publishDevices(location) {
|
||||||
|
this.republishCount = (this.republishCount < 1) ? 1 : this.republishCount
|
||||||
|
while (this.republishCount > 0 && this.mqtt.connected) {
|
||||||
|
try {
|
||||||
|
const devices = await this.devices.filter(d => d.locationId == location.locationId)
|
||||||
|
if (devices && devices.length) {
|
||||||
|
devices.forEach(device => {
|
||||||
|
// Provide location websocket connection state to device
|
||||||
|
device.publish(location.onConnected._value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debug(error)
|
||||||
|
}
|
||||||
|
await utils.sleep(30)
|
||||||
|
this.republishCount--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Ring()
|
@@ -19,7 +19,6 @@ class State {
|
|||||||
this.file = (this.config.runMode === 'standard')
|
this.file = (this.config.runMode === 'standard')
|
||||||
? require('path').dirname(require.main.filename)+'/ring-state.json'
|
? require('path').dirname(require.main.filename)+'/ring-state.json'
|
||||||
: this.file = '/data/ring-state.json'
|
: this.file = '/data/ring-state.json'
|
||||||
|
|
||||||
await this.loadStateData()
|
await this.loadStateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -32,12 +32,13 @@ class TokenApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Super simple web service to acquire refresh tokens
|
// Super simple web service to acquire refresh tokens
|
||||||
async start() {
|
async start(runMode) {
|
||||||
|
this.runMode = runMode
|
||||||
const webdir = __dirname+'/../web'
|
const webdir = __dirname+'/../web'
|
||||||
let restClient
|
let restClient
|
||||||
|
|
||||||
this.listener = this.app.listen(55123, () => {
|
this.listener = this.app.listen(55123, () => {
|
||||||
if (!process.env.RUNMODE === 'addon') {
|
if (this.runMode === 'standard') {
|
||||||
debug('Go to http://<host_ip_address>:55123/ to generate a valid token.')
|
debug('Go to http://<host_ip_address>:55123/ to generate a valid token.')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -87,9 +88,13 @@ class TokenApp {
|
|||||||
res.sendFile('code.html', {root: webdir})
|
res.sendFile('code.html', {root: webdir})
|
||||||
}
|
}
|
||||||
if (generatedToken) {
|
if (generatedToken) {
|
||||||
if (process.env.RUNMODE === 'addon') {
|
if (this.runMode !== 'docker') {
|
||||||
res.sendFile('restart.html', {root: webdir})
|
res.sendFile('restart.html', {root: webdir})
|
||||||
this.token.generated = generatedToken.refresh_token
|
this.token.generated = generatedToken.refresh_token
|
||||||
|
if (this.runMode === 'standard') {
|
||||||
|
await this.listener.close()
|
||||||
|
this.listener = false
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
res.cookie('token', generatedToken.refresh_token, { maxAge: 1000, encode: String })
|
res.cookie('token', generatedToken.refresh_token, { maxAge: 1000, encode: String })
|
||||||
res.sendFile('token.html', {root: webdir})
|
res.sendFile('token.html', {root: webdir})
|
||||||
|
450
ring-mqtt.js
450
ring-mqtt.js
@@ -3,45 +3,16 @@
|
|||||||
// Defines
|
// Defines
|
||||||
const config = require('./lib/config')
|
const config = require('./lib/config')
|
||||||
const state = require('./lib/state')
|
const state = require('./lib/state')
|
||||||
const { RingApi, RingDeviceType, RingCamera, RingChime } = require('@tsightler/ring-client-api')
|
const ring = require('./lib/ring')
|
||||||
const mqttApi = require('mqtt')
|
const mqtt = require('./lib/mqtt')
|
||||||
const isOnline = require('is-online')
|
const isOnline = require('is-online')
|
||||||
const debug = require('debug')('ring-mqtt')
|
const debug = require('debug')('ring-mqtt')
|
||||||
const colors = require('colors/safe')
|
const colors = require('colors/safe')
|
||||||
const utils = require('./lib/utils.js')
|
const utils = require('./lib/utils.js')
|
||||||
const tokenApp = require('./lib/tokenapp.js')
|
const tokenApp = require('./lib/tokenapp.js')
|
||||||
const rss = require('./lib/rtsp-simple-server.js')
|
const rss = require('./lib/rtsp-simple-server.js')
|
||||||
const BaseStation = require('./devices/base-station')
|
|
||||||
const Beam = require('./devices/beam')
|
|
||||||
const BeamOutdoorPlug = require('./devices/beam-outdoor-plug')
|
|
||||||
const Bridge = require('./devices/bridge')
|
|
||||||
const Camera = require('./devices/camera')
|
|
||||||
const Chime = require('./devices/chime')
|
|
||||||
const CoAlarm = require('./devices/co-alarm')
|
|
||||||
const ContactSensor = require('./devices/contact-sensor')
|
|
||||||
const Fan = require('./devices/fan')
|
|
||||||
const FloodFreezeSensor = require('./devices/flood-freeze-sensor')
|
|
||||||
const Keypad = require('./devices/keypad')
|
|
||||||
const Lock = require('./devices/lock')
|
|
||||||
const ModesPanel = require('./devices/modes-panel')
|
|
||||||
const MotionSensor = require('./devices/motion-sensor')
|
|
||||||
const MultiLevelSwitch = require('./devices/multi-level-switch')
|
|
||||||
const RangeExtender = require('./devices/range-extender')
|
|
||||||
const SecurityPanel = require('./devices/security-panel')
|
|
||||||
const Siren = require('./devices/siren')
|
|
||||||
const SmokeAlarm = require('./devices/smoke-alarm')
|
|
||||||
const SmokeCoListener = require('./devices/smoke-co-listener')
|
|
||||||
const Switch = require('./devices/switch')
|
|
||||||
const TemperatureSensor = require('./devices/temperature-sensor')
|
|
||||||
const Thermostat = require('./devices/thermostat')
|
|
||||||
|
|
||||||
var ringLocations = new Array()
|
// Setup Exit Handlers
|
||||||
var ringDevices = new Array()
|
|
||||||
var mqttConnected = false
|
|
||||||
var republishCount = 6 // Republish config/state this many times after startup or HA start/restart
|
|
||||||
var republishDelay = 30 // Seconds
|
|
||||||
|
|
||||||
// Setup Exit Handwlers
|
|
||||||
process.on('exit', processExit.bind(null, 0))
|
process.on('exit', processExit.bind(null, 0))
|
||||||
process.on('SIGINT', processExit.bind(null, 0))
|
process.on('SIGINT', processExit.bind(null, 0))
|
||||||
process.on('SIGTERM', processExit.bind(null, 0))
|
process.on('SIGTERM', processExit.bind(null, 0))
|
||||||
@@ -70,10 +41,10 @@ async function processExit(exitCode) {
|
|||||||
await utils.sleep(1)
|
await utils.sleep(1)
|
||||||
debug('The ring-mqtt process is shutting down...')
|
debug('The ring-mqtt process is shutting down...')
|
||||||
rss.shutdown()
|
rss.shutdown()
|
||||||
if (ringDevices.length > 0) {
|
if (ring.devices.length > 0) {
|
||||||
debug('Setting all devices offline...')
|
debug('Setting all devices offline...')
|
||||||
await utils.sleep(1)
|
await utils.sleep(1)
|
||||||
ringDevices.forEach(ringDevice => {
|
ring.devices.forEach(ringDevice => {
|
||||||
if (ringDevice.availabilityState === 'online') {
|
if (ringDevice.availabilityState === 'online') {
|
||||||
ringDevice.shutdown = true
|
ringDevice.shutdown = true
|
||||||
ringDevice.offline()
|
ringDevice.offline()
|
||||||
@@ -85,348 +56,25 @@ async function processExit(exitCode) {
|
|||||||
process.exit()
|
process.exit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return supported device
|
async function startTokenApp(runMode) {
|
||||||
async function getDevice(device, mqttClient, allDevices) {
|
if (!tokenApp.listener) {
|
||||||
const deviceInfo = {
|
tokenApp.start(runMode)
|
||||||
device: device,
|
tokenApp.token.registerListener(function(generatedToken) {
|
||||||
mqttClient: mqttClient,
|
main(generatedToken)
|
||||||
config: config.data
|
|
||||||
}
|
|
||||||
if (device instanceof RingCamera) {
|
|
||||||
return new Camera(deviceInfo)
|
|
||||||
} else if (device instanceof RingChime) {
|
|
||||||
return new Chime(deviceInfo)
|
|
||||||
} else if (/^lock($|\.)/.test(device.deviceType)) {
|
|
||||||
return new Lock(deviceInfo)
|
|
||||||
}
|
|
||||||
switch (device.deviceType) {
|
|
||||||
case RingDeviceType.ContactSensor:
|
|
||||||
case RingDeviceType.RetrofitZone:
|
|
||||||
case RingDeviceType.TiltSensor:
|
|
||||||
case RingDeviceType.GlassbreakSensor:
|
|
||||||
return new ContactSensor(deviceInfo)
|
|
||||||
case RingDeviceType.MotionSensor:
|
|
||||||
return new MotionSensor(deviceInfo)
|
|
||||||
case RingDeviceType.FloodFreezeSensor:
|
|
||||||
return new FloodFreezeSensor(deviceInfo)
|
|
||||||
case RingDeviceType.SecurityPanel:
|
|
||||||
return new SecurityPanel(deviceInfo)
|
|
||||||
case RingDeviceType.SmokeAlarm:
|
|
||||||
return new SmokeAlarm(deviceInfo)
|
|
||||||
case RingDeviceType.CoAlarm:
|
|
||||||
return new CoAlarm(deviceInfo, allDevices)
|
|
||||||
case RingDeviceType.SmokeCoListener:
|
|
||||||
return new SmokeCoListener(deviceInfo)
|
|
||||||
case RingDeviceType.BeamsMotionSensor:
|
|
||||||
case RingDeviceType.BeamsMultiLevelSwitch:
|
|
||||||
case RingDeviceType.BeamsTransformerSwitch:
|
|
||||||
case RingDeviceType.BeamsLightGroupSwitch:
|
|
||||||
return new Beam(deviceInfo)
|
|
||||||
case RingDeviceType.BeamsDevice:
|
|
||||||
return new BeamOutdoorPlug(deviceInfo, allDevices)
|
|
||||||
case RingDeviceType.MultiLevelSwitch:
|
|
||||||
return newDevice = (device.categoryId === 17) ? new Fan(deviceInfo) : new MultiLevelSwitch(deviceInfo)
|
|
||||||
case RingDeviceType.Switch:
|
|
||||||
return new Switch(deviceInfo)
|
|
||||||
case RingDeviceType.Keypad:
|
|
||||||
return new Keypad(deviceInfo)
|
|
||||||
case RingDeviceType.BaseStation:
|
|
||||||
return new BaseStation(deviceInfo)
|
|
||||||
case RingDeviceType.RangeExtender:
|
|
||||||
return new RangeExtender(deviceInfo)
|
|
||||||
case RingDeviceType.RingNetAdapter:
|
|
||||||
return new Bridge(deviceInfo)
|
|
||||||
case RingDeviceType.Sensor:
|
|
||||||
return newDevice = (device.name.toLowerCase().includes('motion'))
|
|
||||||
? new MotionSensor(deviceInfo)
|
|
||||||
: new ContactSensor(deviceInfo)
|
|
||||||
case 'location.mode':
|
|
||||||
return new ModesPanel(deviceInfo)
|
|
||||||
case 'siren':
|
|
||||||
case 'siren.outdoor-strobe':
|
|
||||||
return new Siren(deviceInfo)
|
|
||||||
case RingDeviceType.Thermostat:
|
|
||||||
return new Thermostat(deviceInfo, allDevices)
|
|
||||||
case RingDeviceType.TemperatureSensor:
|
|
||||||
// If this is a thermostat component, ignore this device
|
|
||||||
if (allDevices.find(d => d.id === device.data.parentZid && d.deviceType === RingDeviceType.Thermostat)) {
|
|
||||||
return 'ignore'
|
|
||||||
} else {
|
|
||||||
return new TemperatureSensor(deviceInfo)
|
|
||||||
}
|
|
||||||
case RingDeviceType.BeamsSwitch:
|
|
||||||
case 'thermostat-operating-status':
|
|
||||||
case 'access-code':
|
|
||||||
case 'access-code.vault':
|
|
||||||
case 'adapter.sidewalk':
|
|
||||||
case 'adapter.zigbee':
|
|
||||||
case 'adapter.zwave':
|
|
||||||
return "ignore"
|
|
||||||
}
|
|
||||||
return "not-supported"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update all Ring location/device data
|
|
||||||
async function updateRingData(mqttClient, ringClient) {
|
|
||||||
// Small delay makes debug output more readable
|
|
||||||
await utils.sleep(1)
|
|
||||||
|
|
||||||
// Get all Ring locations
|
|
||||||
const locations = await ringClient.getLocations()
|
|
||||||
|
|
||||||
// Loop through each location and update stored locations/devices
|
|
||||||
for (const location of locations) {
|
|
||||||
let cameras = new Array()
|
|
||||||
let chimes = new Array()
|
|
||||||
const unsupportedDevices = new Array()
|
|
||||||
|
|
||||||
debug(colors.green('-'.repeat(80)))
|
|
||||||
// If new location, set custom properties and add to location list
|
|
||||||
if (ringLocations.find(l => l.locationId == location.locationId)) {
|
|
||||||
debug(colors.white('Existing location: ')+colors.green(location.name)+colors.cyan(` (${location.id})`))
|
|
||||||
} else {
|
|
||||||
debug(colors.white('New location: ')+colors.green(location.name)+colors.cyan(` (${location.id})`))
|
|
||||||
location.isSubscribed = false
|
|
||||||
location.isConnected = false
|
|
||||||
ringLocations.push(location)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all location devices and, if configured, cameras
|
|
||||||
const devices = await location.getDevices()
|
|
||||||
if (config.data.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 (config.data.enable_modes && (await location.supportsLocationModeSwitching())) {
|
|
||||||
allDevices.push({
|
|
||||||
deviceType: 'location.mode',
|
|
||||||
location: location,
|
|
||||||
id: location.locationId + '_mode',
|
|
||||||
onData: location.onLocationMode,
|
|
||||||
data: {
|
|
||||||
device_id: location.locationId + '_mode',
|
|
||||||
location_id: location.locationId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update Ring devices for location
|
|
||||||
for (const device of allDevices) {
|
|
||||||
const deviceId = (device instanceof RingCamera || device instanceof RingChime) ? device.data.device_id : device.id
|
|
||||||
let foundMessage = ' New device: '
|
|
||||||
let ringDevice = ringDevices.find(d => d.deviceId === deviceId && d.locationId === location.locationId)
|
|
||||||
if (ringDevice) {
|
|
||||||
foundMessage = ' Existing device: '
|
|
||||||
} else {
|
|
||||||
ringDevice = await getDevice(device, mqttClient, allDevices)
|
|
||||||
switch (ringDevice) {
|
|
||||||
case 'not-supported':
|
|
||||||
// Save unsupported device type
|
|
||||||
unsupportedDevices.push(device.deviceType)
|
|
||||||
case 'ignore':
|
|
||||||
ringDevice=false
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
ringDevices.push(ringDevice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ringDevice) {
|
|
||||||
debug(colors.white(foundMessage)+colors.green(`${ringDevice.deviceData.name}`)+colors.cyan(' ('+ringDevice.deviceId+')'))
|
|
||||||
if (ringDevice.hasOwnProperty('childDevices')) {
|
|
||||||
const indent = ' '.repeat(foundMessage.length-4)
|
|
||||||
debug(colors.white(`${indent}│ `)+colors.gray(ringDevice.device.deviceType))
|
|
||||||
let keys = Object.keys(ringDevice.childDevices).length
|
|
||||||
Object.keys(ringDevice.childDevices).forEach(key => {
|
|
||||||
debug(colors.white(`${indent}${(keys > 1) ? '├─: ' : '└─: '}`)+colors.green(`${ringDevice.childDevices[key].name}`)+colors.cyan(` (${ringDevice.childDevices[key].id})`))
|
|
||||||
debug(colors.white(`${indent}${(keys > 1) ? '│ ' : ' '}`)+colors.gray(ringDevice.childDevices[key].deviceType))
|
|
||||||
keys--
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const indent = ' '.repeat(foundMessage.length)
|
|
||||||
debug(colors.gray(`${indent}${ringDevice.device.deviceType}`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Output any unsupported devices to debug with warning
|
|
||||||
unsupportedDevices.forEach(deviceType => {
|
|
||||||
debug(colors.yellow(' Unsupported device: '+deviceType))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
debug(colors.green('-'.repeat(80)))
|
|
||||||
debug('Ring location/device data updated, sleeping for 5 seconds.')
|
|
||||||
await utils.sleep(2)
|
|
||||||
const cameras = await ringDevices.filter(d => d.device instanceof RingCamera)
|
|
||||||
if (cameras.length > 0 && !rss.started) {
|
|
||||||
await rss.start(cameras)
|
|
||||||
}
|
|
||||||
await utils.sleep(3)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish devices/cameras for given location
|
|
||||||
async function publishDevices(location) {
|
|
||||||
republishCount = (republishCount < 1) ? 1 : republishCount
|
|
||||||
while (republishCount > 0 && mqttConnected) {
|
|
||||||
try {
|
|
||||||
const devices = await ringDevices.filter(d => d.locationId == location.locationId)
|
|
||||||
if (devices && devices.length) {
|
|
||||||
devices.forEach(device => {
|
|
||||||
// Provide location websocket connection state to device
|
|
||||||
device.publish(location.onConnected._value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
debug(error)
|
|
||||||
}
|
|
||||||
await utils.sleep(republishDelay)
|
|
||||||
republishCount--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through each location and call publishLocation for supported/connected devices
|
|
||||||
async function processLocations(mqttClient, ringClient) {
|
|
||||||
// Update Ring location and device data
|
|
||||||
await updateRingData(mqttClient, ringClient)
|
|
||||||
|
|
||||||
// For each location get existing alarm & camera devices
|
|
||||||
ringLocations.forEach(async location => {
|
|
||||||
const devices = await ringDevices.filter(d => d.locationId == location.locationId)
|
|
||||||
// If location has devices publish them
|
|
||||||
if (devices && devices.length) {
|
|
||||||
if (location.hasHubs && !location.isSubscribed) {
|
|
||||||
// Location has an alarm or smart bridge so subscribe to websocket connection monitor
|
|
||||||
location.isSubscribed = true
|
|
||||||
location.onConnected.subscribe(async connected => {
|
|
||||||
if (connected) {
|
|
||||||
// Only publish if previous state was actually disconnected
|
|
||||||
if (!location.isConnected) {
|
|
||||||
location.isConnected = true
|
|
||||||
debug('Websocket for location id '+location.locationId+' is connected')
|
|
||||||
publishDevices(location)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Wait 30 seconds before setting devices offline in case disconnect is transient
|
|
||||||
// Keeps from creating "unknown" state for sensors if connection error is short lived
|
|
||||||
await utils.sleep(30)
|
|
||||||
if (!location.onConnected._value) {
|
|
||||||
location.isConnected = false
|
|
||||||
debug('Websocket for location id '+location.locationId+' is disconnected')
|
|
||||||
ringDevices.forEach(device => {
|
|
||||||
if (device.locationId == location.locationId && !device.camera) {
|
|
||||||
device.offline()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
publishDevices(location)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug('No devices found for location ID '+location.id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process received MQTT command
|
|
||||||
async function processMqttMessage(topic, message, mqttClient, ringClient) {
|
|
||||||
message = message.toString()
|
|
||||||
if (topic === config.data.hass_topic || topic === 'hass/status' || topic === 'hassio/status') {
|
|
||||||
debug('Home Assistant state topic '+topic+' received message: '+message)
|
|
||||||
if (message == 'online') {
|
|
||||||
// Republish devices and state if restart of HA is detected
|
|
||||||
if (republishCount > 0) {
|
|
||||||
debug('Home Assisntat restart detected during existing republish cycle')
|
|
||||||
debug('Resetting device config/state republish count')
|
|
||||||
republishCount = 6
|
|
||||||
} else {
|
|
||||||
debug('Home Assistant restart detected, resending device config/state in 5 seconds')
|
|
||||||
await utils.sleep(5)
|
|
||||||
republishCount = 6
|
|
||||||
processLocations(mqttClient, ringClient)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Parse topic to get location/device ID
|
|
||||||
const ringTopicLevels = (config.data.ring_topic).split('/').length
|
|
||||||
splitTopic = topic.split('/')
|
|
||||||
const locationId = splitTopic[ringTopicLevels]
|
|
||||||
const deviceId = splitTopic[ringTopicLevels + 2]
|
|
||||||
|
|
||||||
// Find existing device by matching location & device ID
|
|
||||||
const cmdDevice = ringDevices.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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initiate the connection to MQTT broker
|
|
||||||
function initMqtt() {
|
|
||||||
const mqtt_user = config.data.mqtt_user ? config.data.mqtt_user : null
|
|
||||||
const mqtt_pass = config.data.mqtt_pass ? config.data.mqtt_pass : null
|
|
||||||
const mqtt = mqttApi.connect({
|
|
||||||
host: config.data.host,
|
|
||||||
port: config.data.port,
|
|
||||||
username: mqtt_user,
|
|
||||||
password: mqtt_pass
|
|
||||||
});
|
|
||||||
return mqtt
|
|
||||||
}
|
|
||||||
|
|
||||||
// MQTT initialization successful, setup actions for MQTT events
|
|
||||||
function startMqtt(mqttClient, ringClient) {
|
|
||||||
// On MQTT connect/reconnect send config/state information after delay
|
|
||||||
mqttClient.on('connect', async function () {
|
|
||||||
if (!mqttConnected) {
|
|
||||||
mqttConnected = true
|
|
||||||
debug('MQTT connection established, processing locations...')
|
|
||||||
}
|
|
||||||
processLocations(mqttClient, ringClient)
|
|
||||||
})
|
|
||||||
|
|
||||||
mqttClient.on('reconnect', function () {
|
|
||||||
if (mqttConnected) {
|
|
||||||
debug('Connection to MQTT broker lost. Attempting to reconnect...')
|
|
||||||
} else {
|
|
||||||
debug('Attempting to reconnect to MQTT broker...')
|
|
||||||
}
|
|
||||||
mqttConnected = false
|
|
||||||
})
|
|
||||||
|
|
||||||
mqttClient.on('error', function (error) {
|
|
||||||
debug('Unable to connect to MQTT broker.', error.message)
|
|
||||||
mqttConnected = false
|
|
||||||
})
|
|
||||||
|
|
||||||
// Process MQTT messages from subscribed command topics
|
|
||||||
mqttClient.on('message', async function (topic, message) {
|
|
||||||
processMqttMessage(topic, message, mqttClient, ringClient)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/* End Functions */
|
/* End Functions */
|
||||||
|
|
||||||
// Main code loop
|
// Main code loop
|
||||||
const main = async(generatedToken) => {
|
const main = async(generatedToken) => {
|
||||||
let ringClient
|
|
||||||
let mqttClient
|
|
||||||
if (!state.valid) {
|
if (!state.valid) {
|
||||||
await state.init(config)
|
await state.init(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.runMode === 'addon' && !tokenApp.listener) {
|
if (config.runMode === 'addon') {
|
||||||
tokenApp.start()
|
startTokenApp(config.runMode)
|
||||||
tokenApp.token.registerListener(function(generatedToken) {
|
|
||||||
main(generatedToken)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If refresh token was generated via web UI, use it, otherwise attempt to get latest token from state file
|
// If refresh token was generated via web UI, use it, otherwise attempt to get latest token from state file
|
||||||
@@ -442,8 +90,8 @@ const main = async(generatedToken) => {
|
|||||||
process.exit(2)
|
process.exit(2)
|
||||||
} else {
|
} else {
|
||||||
debug(colors.brightRed('No refresh token was found in state file, generate a token using the Web UI.'))
|
debug(colors.brightRed('No refresh token was found in state file, generate a token using the Web UI.'))
|
||||||
if (config.runMode === 'standard' && !tokenApp.listener) {
|
if (config.runMode === 'standard') {
|
||||||
tokenApp.start()
|
startTokenApp(config.runMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -454,68 +102,38 @@ const main = async(generatedToken) => {
|
|||||||
await utils.sleep(10)
|
await utils.sleep(10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const tokenSource = generatedToken ? "generated" : "saved"
|
|
||||||
const ringAuth = {
|
const ringAuth = {
|
||||||
refreshToken: state.data.ring_token,
|
refreshToken: state.data.ring_token,
|
||||||
systemId: state.data.systemId,
|
systemId: state.data.systemId,
|
||||||
controlCenterDisplayName: (config.runMode === 'addon') ? 'ring-mqtt-addon' : 'ring-mqtt',
|
controlCenterDisplayName: (config.runMode === 'addon') ? 'ring-mqtt-addon' : 'ring-mqtt',
|
||||||
...config.data.enable_cameras ? { cameraStatusPollingSeconds: 20, cameraDingsPollingSeconds: 2 } : {},
|
...config.data.enable_cameras ? { cameraStatusPollingSeconds: 20, cameraDingsPollingSeconds: 2 } : {},
|
||||||
...config.data.enable_modes ? { locationModePollingSeconds: 20 } : {},
|
...config.data.enable_modes ? { locationModePollingSeconds: 20 } : {},
|
||||||
...(!(config.data.location_ids === undefined || config.data.location_ids == 0)) ? { locationIds: config.data.location_ids } : {}
|
...!(config.data.location_ids === undefined || config.data.location_ids == 0) ? { locationIds: config.data.location_ids } : {}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (await ring.init(ringAuth, config.data, rss, generatedToken ? 'generated' : 'saved')) {
|
||||||
debug(`Attempting connection to Ring API using ${generatedToken ? "generated" : "saved"} refresh token.`)
|
debug('Successfully established connection to Ring API')
|
||||||
ringClient = new RingApi(ringAuth)
|
|
||||||
await ringClient.getProfile()
|
|
||||||
} catch(error) {
|
|
||||||
ringClient = null
|
|
||||||
debug(colors.brightYellow(error.message))
|
|
||||||
debug(colors.brightYellow('Failed to establed connection to Ring API using '+tokenSource+' refresh token.'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ringClient) {
|
// Update the web app with current connected refresh token
|
||||||
debug('Successfully established connection to Ring API')
|
const currentAuth = await ring.client.restClient.authPromise
|
||||||
|
tokenApp.updateConnectedToken(currentAuth.refresh_token)
|
||||||
|
|
||||||
// Update the web app with current connected refresh token
|
// Subscribed to token update events and save new token
|
||||||
const currentAuth = await ringClient.restClient.authPromise
|
ring.client.onRefreshTokenUpdated.subscribe(({ newRefreshToken, oldRefreshToken }) => {
|
||||||
tokenApp.updateConnectedToken(currentAuth.refresh_token)
|
state.updateToken(newRefreshToken, oldRefreshToken)
|
||||||
|
})
|
||||||
|
|
||||||
// Subscribed to token update events and save new token
|
mqtt.init(ring, config.data)
|
||||||
ringClient.onRefreshTokenUpdated.subscribe(({ newRefreshToken, oldRefreshToken }) => {
|
|
||||||
state.updateToken(newRefreshToken, oldRefreshToken)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Initiate connection to MQTT broker
|
|
||||||
try {
|
|
||||||
debug('Starting connection to MQTT broker...')
|
|
||||||
mqttClient = await initMqtt()
|
|
||||||
if (mqttClient.connected) {
|
|
||||||
mqttConnected = true
|
|
||||||
debug('MQTT connection established, sending config/state information in 5 seconds.')
|
|
||||||
}
|
|
||||||
// Monitor configured/default Home Assistant status topic
|
|
||||||
mqttClient.subscribe(config.data.hass_topic)
|
|
||||||
// Monitor legacy Home Assistant status topics
|
|
||||||
mqttClient.subscribe('hass/status')
|
|
||||||
mqttClient.subscribe('hassio/status')
|
|
||||||
startMqtt(mqttClient, ringClient)
|
|
||||||
} catch (error) {
|
|
||||||
debug(error)
|
|
||||||
debug( colors.red('Couldn\'t authenticate to MQTT broker. Please check the broker and configuration settings.'))
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug(colors.brightRed('Failed to connect to Ring API using the refresh token in the saved state file.'))
|
|
||||||
if (config.runMode === 'docker') {
|
|
||||||
debug(colors.brightRed('Restart the container to try again or generate a new token using get-ring-token.js.'))
|
|
||||||
process.exit(2)
|
|
||||||
} else {
|
} else {
|
||||||
debug(colors.brightRed(`Restart the ${this.runMode === 'addon' ? 'addon' : 'script'} or generate a new token using the Web UI.`))
|
debug(colors.brightRed('Failed to connect to Ring API using the refresh token in the saved state file.'))
|
||||||
if (config.runMode === 'standard' && !tokenApp.listener) {
|
if (config.runMode === 'docker') {
|
||||||
tokenApp.start()
|
debug(colors.brightRed('Restart the container to try again or generate a new token using get-ring-token.js.'))
|
||||||
|
process.exit(2)
|
||||||
|
} else {
|
||||||
|
debug(colors.brightRed(`Restart the ${this.runMode === 'addon' ? 'addon' : 'script'} or generate a new token using the Web UI.`))
|
||||||
|
if (config.runMode === 'standard') {
|
||||||
|
startTokenApp(config.runMode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user