Release 5.0.0

This commit is contained in:
tsightler
2022-02-21 23:40:57 -05:00
parent 41a45cb93e
commit 84f3cf4fe1
8 changed files with 478 additions and 428 deletions

View File

@@ -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 = {

View File

@@ -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'

View File

@@ -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
View 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
View 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()

View File

@@ -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()
} }

View File

@@ -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})

View File

@@ -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)
}
} }
} }
} }