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,59 +102,28 @@ 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.`)
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) {
debug('Successfully established connection to Ring API') debug('Successfully established connection to Ring API')
// Update the web app with current connected refresh token // Update the web app with current connected refresh token
const currentAuth = await ringClient.restClient.authPromise const currentAuth = await ring.client.restClient.authPromise
tokenApp.updateConnectedToken(currentAuth.refresh_token) tokenApp.updateConnectedToken(currentAuth.refresh_token)
// Subscribed to token update events and save new token // Subscribed to token update events and save new token
ringClient.onRefreshTokenUpdated.subscribe(({ newRefreshToken, oldRefreshToken }) => { ring.client.onRefreshTokenUpdated.subscribe(({ newRefreshToken, oldRefreshToken }) => {
state.updateToken(newRefreshToken, oldRefreshToken) state.updateToken(newRefreshToken, oldRefreshToken)
}) })
// Initiate connection to MQTT broker mqtt.init(ring, config.data)
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 { } else {
debug(colors.brightRed('Failed to connect to Ring API using the refresh token in the saved state file.')) debug(colors.brightRed('Failed to connect to Ring API using the refresh token in the saved state file.'))
if (config.runMode === 'docker') { if (config.runMode === 'docker') {
@@ -514,8 +131,9 @@ const main = async(generatedToken) => {
process.exit(2) 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(`Restart the ${this.runMode === 'addon' ? 'addon' : 'script'} or generate a new token using the Web UI.`))
if (config.runMode === 'standard' && !tokenApp.listener) { if (config.runMode === 'standard') {
tokenApp.start() startTokenApp(config.runMode)
}
} }
} }
} }