Release v5.4.0 (#650)

* Display system name in token UI
* Implement startup event cache
* Bump bashio version
* Use new event history API
* Fix person event lookup
* Use device history for event stream URL
* Implement armedBy/disarmedBy attributes
* Implement pending state for transcoded event stream
* Switch transcoding to downloads API
* Never use cached snapshot for UUID
This commit is contained in:
tsightler
2023-06-24 14:20:58 -04:00
committed by GitHub
parent 55bdc1bfe5
commit 411c9adc3f
17 changed files with 506 additions and 255 deletions

View File

@@ -10,7 +10,7 @@ ENV LANG="C.UTF-8" \
COPY . /app/ring-mqtt
RUN S6_VERSION="v3.1.5.0" && \
BASHIO_VERSION="v0.14.3" && \
BASHIO_VERSION="v0.15.0" && \
GO2RTC_VERSION="v1.5.0" && \
APK_ARCH="$(apk --print-arch)" && \
apk add --no-cache tar xz git libcrypto3 libssl3 musl-utils musl bash curl jq tzdata nodejs npm mosquitto-clients && \

View File

@@ -60,4 +60,33 @@ export default class RingPolledDevice extends RingDevice {
await utils.sleep(20)
this.monitorHeartbeat()
}
async getDeviceHistory(options) {
try {
const response = await this.device.restClient.request({
method: 'GET',
url: `https://api.ring.com/evm/v2/history/devices/${this.device.id}${this.getSearchQueryString({
capabilities: 'offline_event',
...options,
})}`
})
return response
} catch (err) {
this.debug(err)
this.debug('Failed to retrieve device event history from Ring API')
}
}
getSearchQueryString(options) {
const queryString = Object.entries(options)
.map(([key, value]) => {
if (value === undefined) {
return '';
}
return `${key}=${value}`;
})
.filter((x) => x)
.join('&');
return queryString.length ? `?${queryString}` : '';
}
}

View File

@@ -235,6 +235,15 @@ export default class RingDevice {
state.setDeviceSavedState(this.deviceId, stateData)
}
async getUserInfo(userId) {
const response = await this.device.location.restClient.request({
url: `https://app.ring.com/api/v1/rs/users/summaries?locationId=${this.locationId}`,
method: 'POST',
json: [userId]
})
return (Array.isArray(response) && response.length > 0) ? response[0] : false
}
// Set state topic online
async online() {
if (this.shutdown) { return } // Supress any delayed online state messages if ring-mqtt is shutting down

View File

@@ -3,10 +3,11 @@ import utils from '../lib/utils.js'
import pathToFfmpeg from 'ffmpeg-for-homebridge'
import { Worker } from 'worker_threads'
import { spawn } from 'child_process'
import { parseISO, addSeconds } from 'date-fns';
import chalk from 'chalk'
export default class Camera extends RingPolledDevice {
constructor(deviceInfo) {
constructor(deviceInfo, events) {
super(deviceInfo, 'camera')
const savedState = this.getSavedState()
@@ -23,7 +24,9 @@ export default class Camera extends RingPolledDevice {
last_ding_expires: 0,
last_ding_time: 'none',
is_person: false,
detection_enabled: null
detection_enabled: null,
events: events.filter(event => event.event_type === 'motion'),
latestEventId: ''
},
...this.device.isDoorbot ? {
ding: {
@@ -32,8 +35,10 @@ export default class Camera extends RingPolledDevice {
publishedDurations: false,
last_ding: 0,
last_ding_expires: 0,
last_ding_time: 'none'
}
last_ding_time: 'none',
events: events.filter(event => event.event_type === 'ding'),
latestEventId: ''
}
} : {},
snapshot: {
mode: savedState?.snapshot?.mode
@@ -276,30 +281,57 @@ export default class Camera extends RingPolledDevice {
}
}
// Get most recent motion event data
const lastMotionEvent = (await this.device.getEvents({ limit: 1, kind: 'motion'})).events[0]
const lastMotionDate = (lastMotionEvent?.created_at) ? new Date(lastMotionEvent.created_at) : false
this.data.motion.last_ding = lastMotionDate ? Math.floor(lastMotionDate/1000) : 0
this.data.motion.last_ding_time = lastMotionDate ? utils.getISOTime(lastMotionDate) : ''
if (lastMotionEvent?.cv_properties) {
this.data.motion.is_person = (lastMotionEvent.cv_properties.detection_type === 'human') ? true : false
// If no motion events in device event cache, request recent motion events
if (this.data.motion.events.length === 0) {
const response = await this.getDeviceHistory({limit: 5, event_types: 'motion'})
if (Array.isArray(response?.items) && response.items.length > 0) {
this.data.motion.events = response.items
}
}
if (this.data.motion.events.length > 0) {
const lastMotionEvent = this.data.motion.events[0]
const lastMotionDate = lastMotionEvent?.start_time ? new Date(lastMotionEvent.start_time) : false
this.data.motion.last_ding = lastMotionDate ? Math.floor(lastMotionDate/1000) : 0
this.data.motion.last_ding_time = lastMotionDate ? utils.getISOTime(lastMotionDate) : ''
this.data.motion.is_person = lastMotionEvent?.cv?.person_detected ? true : false
this.data.motion.latestEventId = lastMotionEvent.event_id
// Try to get URL for most recent motion event, if it fails, assume there's no subscription
let recordingUrl = false
const recordingEvent = this.data.motion.events.find(e => e.recording_status === 'ready')
if (recordingEvent && Array.isArray(recordingEvent.visualizations?.cloud_media_visualization?.media)) {
recordingUrl = (recordingEvent.visualizations.cloud_media_visualization.media.find(e => e.file_type === 'VIDEO')).url
}
if (!recordingUrl) {
this.debug('Could not retrieve recording URL for any motion event, assuming no Ring Protect subscription')
delete this.entity.event_stream
delete this.entity.event_select
}
} else {
this.debug('Unable to retrieve most recent motion event for this camera')
}
// Get most recent ding event data
if (this.device.isDoorbot) {
const lastDingEvent = (await this.device.getEvents({ limit: 1, kind: 'ding'})).events[0]
const lastDingDate = (lastDingEvent?.created_at) ? new Date(lastDingEvent.created_at) : false
this.data.ding.last_ding = lastDingDate ? Math.floor(lastDingDate/1000) : 0
this.data.ding.last_ding_time = lastDingDate ? utils.getISOTime(lastDingDate) : ''
}
// If no ding events in device event cache, request recent ding events
if (this.data.ding.events.length === 0) {
const response = await this.getDeviceHistory({limit: 5, event_types: 'ding'})
if (Array.isArray(response?.items) && response.items.length > 0) {
this.data.ding.events = response.items
}
}
// Try to get URL for most recent motion event, if it fails, assume there's no subscription
const events = await(this.getRecordedEvents('motion', 1))
const recordingUrl = await this.device.getRecordingUrl(events[0].event_id, { transcoded: false })
if (!recordingUrl) {
this.debug('Could not retrieve recording URL for any motion event, assuming no Ring Protect subscription')
delete this.entity.event_stream
delete this.entity.event_select
if (this.data.ding.events.length > 0) {
const lastDingEvent = this.data.ding.events[0]
const lastDingDate = lastDingEvent?.start_time ? new Date(lastDingEvent.start_time) : false
this.data.ding.last_ding = lastDingDate ? Math.floor(lastDingDate/1000) : 0
this.data.ding.last_ding_time = lastDingDate ? utils.getISOTime(lastDingDate) : ''
this.data.ding.latestEventId = lastDingEvent.event_id
} else {
this.debug('Unable to retrieve most recent ding event for this doorbell')
}
}
let stillImageUrlBase = 'localhost'
@@ -681,7 +713,7 @@ export default class Camera extends RingPolledDevice {
case 'motion':
if (image_uuid) {
this.debug(`Requesting motion snapshot using notification image UUID: ${image_uuid}`)
newSnapshot = await this.device.getSnapshot({ uuid: image_uuid })
newSnapshot = await this.device.getNextSnapshot({ uuid: image_uuid })
} else if (!this.device.operatingOnBattery) {
this.debug('Requesting an updated motion snapshot')
newSnapshot = await this.device.getSnapshot()
@@ -749,8 +781,8 @@ export default class Camera extends RingPolledDevice {
const eventType = eventSelect[0].toLowerCase().replace('-', '_')
const eventNumber = eventSelect[1]
if (this.data.event_select.recordingUrl === '<No Valid URL>') {
this.debug(`No valid recording was found for the ${(eventNumber==1?"":eventNumber==2?"2nd ":eventNumber==3?"3rd ":eventNumber+"th ")}most recent ${eventType} event!`)
if (this.data.event_select.recordingUrl.match(/Recording Not Found|Transcoding in Progress/)) {
this.debug(`No recording available for the ${(eventNumber==1?"":eventNumber==2?"2nd ":eventNumber==3?"3rd ":eventNumber+"th ")}most recent ${eventType} event!`)
this.data.stream.event.status = 'failed'
this.data.stream.event.session = false
this.publishStreamState()
@@ -865,24 +897,25 @@ export default class Camera extends RingPolledDevice {
const eventType = eventSelect[0].toLowerCase().replace('-', '_')
const eventNumber = eventSelect[1]
const transcoded = eventSelect[2] === '(Transcoded)' ? true : false
const urlExpired = Math.floor(Date.now()/1000) - this.data.event_select.recordingUrlExpire > 0 ? true : false
const urlExpired = this.data.event_select.recordingUrlExpire < Date.now()
let selectedEvent
let recordingUrl
let recordingUrl = false
try {
const events = await(this.getRecordedEvents(eventType, eventNumber))
selectedEvent = events[eventNumber-1]
if (selectedEvent) {
if (events.length >= eventNumber) {
selectedEvent = events[eventNumber-1]
if (selectedEvent.event_id !== this.data.event_select.eventId || this.data.event_select.transcoded !== transcoded) {
if (this.data.event_select.recordingUrl) {
this.debug(`New ${this.data.event_select.state} event detected, updating the recording URL`)
}
recordingUrl = await this.device.getRecordingUrl(selectedEvent.event_id, { transcoded })
recordingUrl = await this.getRecordingUrl(selectedEvent, transcoded)
} else if (urlExpired) {
this.debug(`Previous ${this.data.event_select.state} URL has expired, updating the recording URL`)
recordingUrl = await this.device.getRecordingUrl(selectedEvent.event_id, { transcoded })
recordingUrl = await this.getRecordingUrl(selectedEvent, transcoded)
}
} else {
this.debug(`No event recording corresponding to ${this.data.event_select.state} was found in device event history`)
}
} catch(error) {
this.debug(error)
@@ -894,21 +927,18 @@ export default class Camera extends RingPolledDevice {
this.data.event_select.transcoded = transcoded
this.data.event_select.eventId = selectedEvent.event_id
// Try to parse URL parameters to set expire time
const urlSearch = new URLSearchParams(recordingUrl)
const amzExpires = Number(urlSearch.get('X-Amz-Expires'))
const amzDate = urlSearch.get('X-Amz-Date')
if (amzDate && amzExpires && amzExpires !== 'NaN') {
const [_, year, month, day, hour, min, sec] = amzDate.match(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/)
this.data.event_select.recordingUrlExpire = Math.floor(Date.UTC(year, month-1, day, hour, min, sec)/1000)+amzExpires-75
} else {
this.data.event_select.recordingUrlExpire = Math.floor(Date.now()/1000) + 600
try {
const urlSearch = new URLSearchParams(recordingUrl)
const amzExpires = Number(urlSearch.get('X-Amz-Expires'))
const amzDate = parseISO(urlSearch.get('X-Amz-Date'))
this.data.event_select.recordingUrlExpire = Date.parse(addSeconds(amzDate, amzExpires/3*2))
} catch {
this.data.event_select.recordingUrlExpire = Date.now() + 600000
}
} else if (urlExpired || !selectedEvent) {
this.data.event_select.recordingUrl = '<No Valid URL>'
this.data.event_select.recordingUrl = '<Recording Not Found>'
this.data.event_select.transcoded = transcoded
this.data.event_select.eventId = '0'
return false
}
return recordingUrl
@@ -916,32 +946,98 @@ export default class Camera extends RingPolledDevice {
async getRecordedEvents(eventType, eventNumber) {
let events = []
let paginationKey = false
let loop = eventType === 'person' ? 4 : 1
try {
if (eventType !== 'person') {
events = ((await this.device.getEvents({
limit: eventNumber+2,
kind: eventType
})).events).filter(event => event.recording_status === 'ready')
} else {
let loop = 0
while (loop <= 3 && events.length < eventNumber) {
events = ((await this.device.getEvents({
limit: 50,
kind: 'motion'
})).events).filter(event => event.recording_status === 'ready' && event.cv_properties.detection_type === 'human')
loop++
await utils.msleep(100)
while (loop > 0) {
const history = await this.getDeviceHistory({
...paginationKey ? { pagination_key: paginationKey }: {},
event_types: eventType === 'person' ? 'motion' : eventType,
limit: eventType === 'person' ? 50 : eventNumber
})
if (Array.isArray(history.items) && history.items.length > 0) {
const newEvents = eventType === 'person'
? history.items.filter(i => i.recording_status === 'ready' && i.cv.person_detected)
: history.items.filter(i => i.recording_status === 'ready')
events = [...events, ...newEvents]
}
// Remove base64 padding characters from pagination key
paginationKey = history.pagination_key ? history.pagination_key.replace(/={1,2}$/, '') : false
// If we have enough events, break the loop, otherwise decrease the loop counter
loop = (events.length >= eventNumber || !history.paginationKey) ? 0 : loop-1
}
} catch(error) {
this.debug(error)
}
if (events.length === 0) {
this.debug(`No recording corresponding to ${this.data.event_select.state} was found in event history`)
return events
}
async getRecordingUrl(event, transcoded) {
let recordingUrl
if (transcoded) {
recordingUrl = await this.getTranscodedUrl(event)
} else {
if (event && Array.isArray(event.visualizations?.cloud_media_visualization?.media)) {
recordingUrl = (event.visualizations.cloud_media_visualization.media.find(e => e.file_type === 'VIDEO')).url
}
}
return recordingUrl
}
async getTranscodedUrl(event) {
let response
let loop = 30
try {
response = await this.device.restClient.request({
method: 'POST',
url: 'https://api.ring.com/share_service/v2/transcodings/downloads',
json: {
'ding_id': event.event_id,
'file_type': 'VIDEO',
'send_push_notification': false
}
})
if (response?.status === 'pending') {
this.data.event_select.recordingUrl = '<Transcoding in Progress>'
this.publishEventSelectState()
}
} catch(err) {
this.debug(err)
this.debug('Request to generate transcoded video failed')
return false
}
return events
while (response?.status === 'pending' && loop > 0) {
try {
response = await this.device.restClient.request({
method: 'GET',
url: `https://api.ring.com/share_service/v2/transcodings/downloads/${event.event_id}?file_type=VIDEO`
})
} catch(err) {
this.debug(err)
this.debug('Request for transcoded video status failed')
}
await utils.sleep(1)
loop--
}
if (response?.status === 'done') {
return response.result_url
} else {
if (loop < 1) {
this.debug('Timeout waiting for transcoded video to be processed')
} else {
this.debug('Failed to retrieve transcoded video URL')
}
return false
}
}
// Process messages from MQTT command topic
@@ -1171,9 +1267,11 @@ export default class Camera extends RingPolledDevice {
async setEventSelect(message) {
this.debug(`Received set event stream to ${message}`)
if (this.entity.event_select.options.includes(message)) {
// Kill any active event streams
if (this.data.stream.event.session) {
this.data.stream.event.session.kill()
}
// Set the new value and save the state
this.data.event_select.state = message
this.updateDeviceState()
await this.updateEventStreamUrl()

View File

@@ -28,21 +28,13 @@ export default class Keypad extends RingSocketDevice {
}
}
// Ugly, but this listens to the raw data updates for all devices and
// picks out proximity detection events for this keypad.
// Listen to raw data updates for all devices and pick out
// proximity detection events for this keypad.
this.device.location.onDataUpdate.subscribe((message) => {
if (message.datatype === 'DeviceInfoDocType' &&
Boolean(message.body) &&
Array.isArray(message.body) &&
message.body[0]?.general?.v2?.zid === this.deviceId &&
message.body[0]?.impulse?.v1 &&
Boolean(message.body[0].impulse.v1) &&
Array.isArray(message.body[0].impulse.v1)
) {
if (message.body[0].impulse.v1[0].impulseType === 'keypad.motion') {
this.processMotion()
}
}
if (message.datatype === 'DeviceInfoDocType' &&
message.body?.[0]?.general?.v2?.zid === this.deviceId &&
message.body[0].impulse?.v1?.[0]?.impulseType === 'keypad.motion'
) { this.processMotion() }
})
}

View File

@@ -8,11 +8,21 @@ export default class SecurityPanel extends RingSocketDevice {
super(deviceInfo, 'alarm', 'alarmState')
this.deviceData.mdl = 'Alarm Control Panel'
this.deviceData.name = `${this.device.location.name} Alarm`
this.data = {
attributes: {
lastArmedBy: 'Unknown',
lastArmedTime: '',
lastDisarmedBy: 'Unknown',
lastDisarmedTime: ''
}
}
this.entity = {
...this.entity,
alarm: {
component: 'alarm_control_panel',
attributes: true,
isLegacyEntity: true // Legacy compatibility
},
siren: {
@@ -33,15 +43,100 @@ export default class SecurityPanel extends RingSocketDevice {
}
} : {}
}
this.initAlarmAttributes()
// Listen to raw data updates for all devices and pick out
// arm/disarm events for this security panel
this.device.location.onDataUpdate.subscribe(async (message) => {
if (message.datatype === 'DeviceInfoDocType' &&
message.body?.[0]?.general?.v2?.zid === this.deviceId &&
message.body[0].impulse?.v1?.[0] &&
message.body[0].impulse.v1.filter(i => i.data?.commandType === 'security-panel.switch-mode').length > 0
) {
const impulse = message.body[0].impulse.v1
if (message.context) {
if (impulse.filter(i => i.data?.data?.mode.match(/some|all/)).length > 0) {
await this.updateAlarmAttributes(message.context, 'Armed')
} else if (impulse.filter(i => i.data?.data?.mode === 'none').length > 0) {
await this.updateAlarmAttributes(message.context, 'Disarmed')
}
}
this.pubishAlarmState()
}
})
}
async initAlarmAttributes() {
const alarmEvents = await this.device.location.getHistory({ affectedId: this.deviceId })
const armEvents = alarmEvents.filter(e =>
Array.isArray(e.body?.[0]?.impulse?.v1) &&
e.body[0].impulse.v1.filter(i =>
i.data?.commandType === 'security-panel.switch-mode' &&
i.data?.data?.mode.match(/some|all/)
).length > 0
)
if (armEvents.length > 0) {
this.updateAlarmAttributes(armEvents[0].context, 'Armed')
}
const disarmEvents = alarmEvents.filter(e =>
Array.isArray(e.body?.[0]?.impulse?.v1) &&
e.body[0].impulse.v1.filter(i =>
i.data?.commandType === 'security-panel.switch-mode' &&
i.data?.data?.mode === 'none'
).length > 0
)
if (disarmEvents.length > 0) {
this.updateAlarmAttributes(disarmEvents[0].context, 'Disarmed')
}
}
async updateAlarmAttributes(contextData, mode) {
let initiatingUser = contextData.initiatingEntityType
if (contextData.initiatingEntityType === 'user' && contextData.initiatingEntityId) {
try {
const userInfo = await this.getUserInfo(contextData.initiatingEntityId)
if (userInfo) {
initiatingUser = `${userInfo.firstName} ${userInfo.lastName}`
} else {
throw new Error('Invalid user information was returned by API')
}
} catch (err) {
this.debug(err.message)
this.debug('Could not get user information from Ring API')
}
}
this.data.attributes[`last${mode}By`] = initiatingUser
this.data.attributes[`last${mode}Time`] = new Date(contextData.eventOccurredTsMs).toISOString()
}
publishState(data) {
const isPublish = data === undefined ? true : false
if (isPublish) {
// Eventually remove this but for now this attempts to delete the old light component based volume control from Home Assistant
this.mqttPublish(`homeassistant/switch/${this.locationId}/${this.deviceId}_bypass/config`, '', false)
this.pubishAlarmState()
}
const sirenState = (this.device.data.siren?.state === 'on') ? 'ON' : 'OFF'
this.mqttPublish(this.entity.siren.state_topic, sirenState)
if (utils.config().enable_panic) {
const policeState = this.device.data.alarmInfo?.state?.match(/burglar|panic/) ? 'ON' : 'OFF'
if (policeState === 'ON') { this.debug('Burgler alarm is triggered for '+this.device.location.name) }
this.mqttPublish(this.entity.police.state_topic, policeState)
const fireState = this.device.data.alarmInfo?.state?.match(/co|fire/) ? 'ON' : 'OFF'
if (fireState === 'ON') { this.debug('Fire alarm is triggered for '+this.device.location.name) }
this.mqttPublish(this.entity.fire.state_topic, fireState)
}
}
async pubishAlarmState() {
let alarmMode
const alarmInfo = this.device.data.alarmInfo ? this.device.data.alarmInfo : []
@@ -69,32 +164,9 @@ export default class SecurityPanel extends RingSocketDevice {
alarmMode = 'unknown'
}
}
this.mqttPublish(this.entity.alarm.state_topic, alarmMode)
const sirenState = (this.device.data.siren && this.device.data.siren.state === 'on') ? 'ON' : 'OFF'
this.mqttPublish(this.entity.siren.state_topic, sirenState)
if (utils.config().enable_panic) {
let policeState = 'OFF'
let fireState = 'OFF'
const alarmState = this.device.data.alarmInfo ? this.device.data.alarmInfo.state : ''
switch (alarmState) {
case 'burglar-alarm':
case 'user-verified-burglar-alarm':
case 'burglar-accelerated-alarm':
policeState = 'ON'
this.debug('Burgler alarm is active for '+this.device.location.name)
case 'fire-alarm':
case 'co-alarm':
case 'user-verified-co-or-fire-alarm':
case 'fire-accelerated-alarm':
fireState = 'ON'
this.debug('Fire alarm is active for '+this.device.location.name)
}
this.mqttPublish(this.entity.police.state_topic, policeState)
this.mqttPublish(this.entity.fire.state_topic, fireState)
}
this.mqttPublish(this.entity.alarm.json_attributes_topic, JSON.stringify(this.data.attributes), 'attr')
this.publishAttributes()
}

View File

@@ -1,3 +1,21 @@
## v5.4.0
This release is mostly to get back to stable ring-client-api version with final fixes for camera notification issues, however, managed to sneak in one highly requested feature and a few minor improvements as well.
**New Features**
- Alarm control panel now includes lastArmedBy/lastDisarmedBy attributes making it possible to determine who/what triggered an arm/disarm event.
**Other Changes**
- Device Name/System ID is now displayed in the Web UI and CLI authentication tools providing easier identification of the corresponding device in the Authorized Devices section of the Ring Control Center.
- Camera event management has been completely reworked using a new event management API. Primary goal was to avoid API throttling issues that could occur with large numbers of cameras (>50% reduction in API calls, even more during startup).
**Bug Fixed**
- Fixed an issue where motion snapshots might return an cached snapshot instead
- Fixed an issue with panic switches where a burglar alarm could trigger both police and fire panic states.
**Dependency Updates**
- ring-client-api v11.8.0
- bashio v0.15.0
## v5.3.0
The primary goal of this update is to address issues with camera/doorbell/intercom notifications that have impacted many users due to changes in the Ring API for push notifications. This version uses a new upstream ring-client-api that persist the FCM token and hardware ID across restarts which will hopefully address these issues, however, it's important to note that addressing this will likely require users to re-authenticate following the instructions below:
@@ -17,7 +35,6 @@ If you have cameras/doorbells/intercoms and are not receiving notifications you
7. Use the addon web UI to authenticate with Ring and re-establish the connection with the Ring API
8. Notifications should now be working!
**New Features**
- Added support to enable/disable motion detection for cameras

View File

@@ -79,7 +79,8 @@ const main = async() => {
try {
await writeFileAtomic(stateFile, JSON.stringify(stateData))
console.log('State file ' +stateFile+ ' saved with updated refresh token.')
console.log(`State file ${stateFile} saved with updated refresh token.`)
console.log(`Device name: ring-mqtt-${systemId.slice(-5)}`)
} catch (err) {
console.log('Saving state file '+stateFile+' failed with error: ')
console.log(err)

View File

@@ -95,6 +95,7 @@ export default new class RingMqtt {
debug('A new refresh token was generated, attempting to re-establish connection to Ring API')
this.client.restClient.refreshToken = this.refreshToken
this.client.restClient._authPromise = undefined
await utils.sleep(2)
await this.client.getProfile()
debug(`Successfully re-established connection to Ring API using generated refresh token`)
} catch (error) {
@@ -114,6 +115,7 @@ export default new class RingMqtt {
try {
debug(`Attempting connection to Ring API using ${generatedToken ? 'generated' : 'saved'} refresh token...`)
this.client = new RingApi(ringAuth)
await utils.sleep(2)
await this.client.getProfile()
utils.event.emit('ring_api_state', 'connected')
debug(`Successfully established connection to Ring API using ${generatedToken ? 'generated' : 'saved'} token`)
@@ -137,48 +139,6 @@ export default new class RingMqtt {
return this.client
}
// Loop through each location and publish supported devices
async publishLocations() {
// 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 initRingData() {
// Small delay here makes debug output more readable
@@ -210,6 +170,7 @@ export default new class RingMqtt {
let cameras = new Array()
let chimes = new Array()
let intercoms = new Array()
let events = new Array()
const unsupportedDevices = new Array()
debug(chalk.green('-'.repeat(90)))
@@ -233,6 +194,23 @@ export default new class RingMqtt {
chimes = location.chimes
intercoms = location.intercoms
}
if (cameras.length > 0) {
const cameraIds = (cameras.map(camera => camera.id)).join('%2C')
try {
const response = await location.restClient.request({
method: 'GET',
url: `https://api.ring.com/evm/v2/history/devices?source_ids=${cameraIds}&capabilities=offline_event&limit=100`
})
if (Array.isArray(response?.items) && response.items?.length > 0) {
events = response.items
}
} catch (err) {
debug(err)
debug('Failed to retrieve camera event history from Ring API')
}
}
const allDevices = [...devices, ...cameras, ...chimes, ...intercoms]
// Add modes panel, if configured and the location supports it
@@ -257,7 +235,7 @@ export default new class RingMqtt {
if (ringDevice) {
foundMessage = ' Existing device: '
} else {
ringDevice = await this.getDevice(device, allDevices)
ringDevice = await this.getDevice(device, allDevices, events)
switch (ringDevice) {
case 'not-supported':
// Save unsupported device type
@@ -302,10 +280,9 @@ export default new class RingMqtt {
}
await utils.sleep(3)
}
// Return supported device
async getDevice(device, allDevices) {
async getDevice(device, allDevices, events) {
const deviceInfo = {
device: device,
...allDevices.filter(d => d.data.parentZid === device.id).length
@@ -314,7 +291,7 @@ export default new class RingMqtt {
? { parentDevice: allDevices.find(d => d.id === device.data.parentZid) } : {}
}
if (device instanceof RingCamera) {
return new Camera(deviceInfo)
return new Camera(deviceInfo, events.filter(event => event.source_id === device.id.toString()))
} else if (device instanceof RingChime) {
return new Chime(deviceInfo)
} else if (device instanceof RingIntercom) {
@@ -384,6 +361,48 @@ export default new class RingMqtt {
return "not-supported"
}
// Loop through each location and publish supported devices
async publishLocations() {
// 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}`)
}
})
}
// Publish devices/cameras for given location
async publishDevices(location) {
this.republishCount = (this.republishCount < 1) ? 1 : this.republishCount

View File

@@ -54,6 +54,7 @@ export default new class TokenApp {
this.app.use(bodyParser.urlencoded({ extended: false }))
this.app.get('/', (req, res) => {
res.cookie('systemId', `${process.env.RUNMODE === 'addon' ? 'ring-mqtt-addon' : 'ring-mqtt'}-${this.systemId.slice(-5)}`, { maxAge: 3600000, encode: String })
if (this.ringConnected) {
res.sendFile('connected.html', {root: webdir})
} else {
@@ -62,10 +63,12 @@ export default new class TokenApp {
})
this.app.get(/.*force-token-generation$/, (req, res) => {
res.cookie('systemId', `${process.env.RUNMODE === 'addon' ? 'ring-mqtt-addon' : 'ring-mqtt'}-${this.systemId.slice(-5)}`, { maxAge: 3600000, encode: String })
res.sendFile('account.html', {root: webdir})
})
this.app.post(/.*submit-account$/, async (req, res) => {
res.cookie('systemId', `${process.env.RUNMODE === 'addon' ? 'ring-mqtt-addon' : 'ring-mqtt'}-${this.systemId.slice(-5)}`, { maxAge: 3600000, encode: String })
const email = req.body.email
const password = req.body.password
restClient = await new RingRestClient({
@@ -89,6 +92,7 @@ export default new class TokenApp {
})
this.app.post(/.*submit-code$/, async (req, res) => {
res.cookie('systemId', `${process.env.RUNMODE === 'addon' ? 'ring-mqtt-addon' : 'ring-mqtt'}-${this.systemId.slice(-5)}`, { maxAge: 3600000, encode: String })
let generatedToken
const code = req.body.code
try {

145
package-lock.json generated
View File

@@ -1,17 +1,18 @@
{
"name": "ring-mqtt",
"version": "5.3.0",
"version": "5.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ring-mqtt",
"version": "5.3.0",
"version": "5.4.0",
"license": "MIT",
"dependencies": {
"aedes": "0.49.0",
"body-parser": "^1.20.2",
"chalk": "^5.2.0",
"date-fns": "^2.30.0",
"debug": "^4.3.4",
"express": "^4.18.2",
"ip": "^1.1.8",
@@ -19,7 +20,7 @@
"js-yaml": "^4.1.0",
"minimist": "^1.2.8",
"mqtt": "^4.3.7",
"ring-client-api": "11.8.0-beta.0",
"ring-client-api": "11.8.0",
"rxjs": "^7.8.1",
"werift": "^0.18.3",
"write-file-atomic": "^5.0.1"
@@ -38,21 +39,21 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.19.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz",
"integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz",
"integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==",
"dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.18.6",
"@babel/helper-validator-identifier": "^7.22.5",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
},
@@ -111,9 +112,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.22.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.3.tgz",
"integrity": "sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==",
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz",
"integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==",
"dependencies": {
"regenerator-runtime": "^0.13.11"
},
@@ -514,9 +515,9 @@
}
},
"node_modules/@sindresorhus/is": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.4.0.tgz",
"integrity": "sha512-Ggh6E9AnMpiNXlbXfFUcWE9qm408rL8jDi7+PMBBx7TMbwEmiqAiSmZ+zydYwxcJLqPGNDoLc9mXDuMDBZg0sA==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.4.1.tgz",
"integrity": "sha512-axlrvsHlHlFmKKMEg4VyvMzFr93JWJj4eIfXY1STVuO2fsImCa7ncaiG5gC8HKOX590AW5RtRsC41/B+OfrSqw==",
"engines": {
"node": ">=14.16"
},
@@ -583,9 +584,9 @@
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
},
"node_modules/@types/node": {
"version": "20.2.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz",
"integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ=="
"version": "20.3.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
"integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg=="
},
"node_modules/@types/responselike": {
"version": "1.0.0",
@@ -1122,12 +1123,12 @@
}
},
"node_modules/cacheable-request": {
"version": "10.2.10",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.10.tgz",
"integrity": "sha512-v6WB+Epm/qO4Hdlio/sfUn69r5Shgh39SsE9DSd4bIezP0mblOlObI+I0kUEM7J0JFc+I7pSeMeYaOYtX1N/VQ==",
"version": "10.2.11",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.11.tgz",
"integrity": "sha512-kn0t0oJnlFo1Nzl/AYQzS/oByMtmaqLasFUa7MUMsiTrIHy8TxSkx2KzWCybE3Nuz1F4sJRGnLAfUGsPe47viQ==",
"dependencies": {
"@types/http-cache-semantics": "^4.0.1",
"get-stream": "^6.0.1",
"get-stream": "^7.0.0",
"http-cache-semantics": "^4.1.1",
"keyv": "^4.5.2",
"mimic-response": "^4.0.0",
@@ -1138,6 +1139,17 @@
"node": ">=14.16"
}
},
"node_modules/cacheable-request/node_modules/get-stream": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-7.0.0.tgz",
"integrity": "sha512-ql6FW5b8tgMYvI4UaoxG3EQN3VyZ6VeQpxNBGg5BZ4xD4u+HJeprzhMMA4OCBEGQgSR+m87pstWMpiVW64W8Fw==",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/call-bind": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@@ -1576,9 +1588,9 @@
}
},
"node_modules/dotenv": {
"version": "16.1.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.1.3.tgz",
"integrity": "sha512-FYssxsmCTtKL72fGBSvb1K9dRz0/VZeWqFme/vSb7r7323x4CRaHu4LvQ5JG3+s6yt2YPbBrkpiEODktfyjI9A==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
"engines": {
"node": ">=12"
},
@@ -2523,9 +2535,9 @@
}
},
"node_modules/graphql": {
"version": "16.6.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz",
"integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==",
"version": "16.7.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.7.0.tgz",
"integrity": "sha512-kdNG+ZGNf0E4dezSA2N9cRq8UdOMCcz9Wzh1dDSrCzGCz0nj6p8qlE+utY6iqr9y1sh3MZxUb7K794neZ2oT1w==",
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
@@ -3522,9 +3534,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/msw": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/msw/-/msw-1.2.1.tgz",
"integrity": "sha512-bF7qWJQSmKn6bwGYVPXOxhexTCGD5oJSZg8yt8IBClxvo3Dx/1W0zqE1nX9BSWmzRsCKWfeGWcB/vpqV6aclpw==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/msw/-/msw-1.2.2.tgz",
"integrity": "sha512-GsW3PE/Es/a1tYThXcM8YHOZ1S1MtivcS3He/LQbbTCx3rbWJYCtWD5XXyJ53KlNPT7O1VI9sCW3xMtgFe8XpQ==",
"hasInstallScript": true,
"dependencies": {
"@mswjs/cookies": "^0.2.2",
@@ -3558,7 +3570,7 @@
"url": "https://opencollective.com/mswjs"
},
"peerDependencies": {
"typescript": ">= 4.4.x <= 5.0.x"
"typescript": ">= 4.4.x <= 5.1.x"
},
"peerDependenciesMeta": {
"typescript": {
@@ -4299,9 +4311,9 @@
}
},
"node_modules/ring-client-api": {
"version": "11.8.0-beta.0",
"resolved": "https://registry.npmjs.org/ring-client-api/-/ring-client-api-11.8.0-beta.0.tgz",
"integrity": "sha512-UlKa94s97b2hJhjBsXOclmmDJlrfDr42e/ryuwBtCRUGne+SwvqFXWR58i2tqMiV0qCNYEXYX3H5812JHNEYfA==",
"version": "11.8.0",
"resolved": "https://registry.npmjs.org/ring-client-api/-/ring-client-api-11.8.0.tgz",
"integrity": "sha512-C/hD6MvKlHBr0z9+i5Ejq8dzdn902G1b0Jjdd5T0Cz991Bd4upM9SCYcM72nUaDF10jPAAyhkxHWBzu4q+ZDiQ==",
"funding": [
{
"type": "paypal",
@@ -4323,12 +4335,12 @@
"got": "^11.8.5",
"json-bigint": "^1.0.0",
"msw": "^1.2.1",
"rxjs": "^7.8.0",
"rxjs": "^7.8.1",
"socket.io-client": "^2.5.0",
"systeminformation": "^5.17.12",
"systeminformation": "^5.18.2",
"uuid": "^9.0.0",
"webcrypto-core": "^1.7.7",
"werift": "0.18.2",
"werift": "0.18.3",
"ws": "^8.13.0"
},
"bin": {
@@ -4370,9 +4382,9 @@
}
},
"node_modules/ring-client-api/node_modules/cacheable-request": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
"integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
"dependencies": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
@@ -4474,37 +4486,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ring-client-api/node_modules/werift": {
"version": "0.18.2",
"resolved": "https://registry.npmjs.org/werift/-/werift-0.18.2.tgz",
"integrity": "sha512-0RSrSnRo5SC8Ks7XgwvUtVGyKiRdlg6RxWWSaWv3ey2iTGrcPvUUp06ZGLeYRSablT7yoFpXJEz+JQuhgA+tJQ==",
"dependencies": {
"@fidm/x509": "^1.2.1",
"@minhducsun2002/leb128": "^1.0.0",
"@peculiar/webcrypto": "^1.4.1",
"@peculiar/x509": "^1.9.2",
"@shinyoshiaki/ebml-builder": "^0.0.1",
"aes-js": "^3.1.2",
"binary-data": "^0.6.0",
"buffer-crc32": "^0.2.13",
"date-fns": "^2.29.3",
"debug": "^4.3.4",
"elliptic": "^6.5.4",
"int64-buffer": "^1.0.1",
"ip": "^1.1.8",
"jspack": "^0.0.4",
"lodash": "^4.17.21",
"nano-time": "^1.0.0",
"p-cancelable": "^2.1.1",
"rx.mini": "^1.2.2",
"turbo-crc32": "^1.0.1",
"tweetnacl": "^1.0.3",
"uuid": "^9.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/ring-client-api/node_modules/ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
@@ -4571,9 +4552,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/semver": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
"integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
"integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@@ -4898,9 +4879,9 @@
}
},
"node_modules/systeminformation": {
"version": "5.17.16",
"resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.17.16.tgz",
"integrity": "sha512-dl2QLa7yp9QbBl9um+51CAr3p/40tbz+f34X1lUXkk1SnDcNeJR2iWu/8HD7GM2yRukmy3RCRXFYcPZs0lCs0Q==",
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.18.3.tgz",
"integrity": "sha512-k+gk7zSi0hI/m3Mgu1OzR8j9BfXMDYa2HUMBdEQZUVCVAO326kDrzrvtVMljiSoDs6T6ojI0AHneDn8AMa0Y6A==",
"os": [
"darwin",
"linux",
@@ -5033,9 +5014,9 @@
"integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w=="
},
"node_modules/tsyringe": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.7.0.tgz",
"integrity": "sha512-ncFDM1jTLsok4ejMvSW5jN1VGPQD48y2tfAR0pdptWRKYX4bkbqPt92k7KJ5RFJ1KV36JEs/+TMh7I6OUgj74g==",
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.8.0.tgz",
"integrity": "sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==",
"dependencies": {
"tslib": "^1.9.3"
},

View File

@@ -1,6 +1,6 @@
{
"name": "ring-mqtt",
"version": "5.3.0",
"version": "5.4.0",
"type": "module",
"description": "Ring Devices via MQTT",
"main": "ring-mqtt.js",
@@ -8,6 +8,7 @@
"aedes": "0.49.0",
"body-parser": "^1.20.2",
"chalk": "^5.2.0",
"date-fns": "^2.30.0",
"debug": "^4.3.4",
"express": "^4.18.2",
"ip": "^1.1.8",
@@ -15,10 +16,10 @@
"js-yaml": "^4.1.0",
"minimist": "^1.2.8",
"mqtt": "^4.3.7",
"ring-client-api": "11.8.0-beta.0",
"write-file-atomic": "^5.0.1",
"ring-client-api": "11.8.0",
"rxjs": "^7.8.1",
"werift": "^0.18.3",
"rxjs": "^7.8.1"
"write-file-atomic": "^5.0.1"
},
"devDependencies": {
"eslint": "^7.19.0"

View File

@@ -37,7 +37,7 @@ cleanup() {
exit 0
}
# go2rtc does not pass stdout through from child processes so send debug loggins
# go2rtc does not pass stdout through from child processes so send debug logs
# via main process using MQTT messages
logger() {
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${debug_topic}" -m "${1}"

View File

@@ -52,7 +52,8 @@ input[type=submit]:hover {
</head>
<body>
<h3>Acquire Refresh Token</h3>
<h2>Acquire Refresh Token</h2>
<h3 id="systemId"></h3>
Please enter your Ring account information below and the 2FA code on the following page to generate the refresh token required for this application to access the Ring API.<br>
<p style="background-color:red" id="errormsg"></p>
<h3>Login</h3>
@@ -68,21 +69,24 @@ Please enter your Ring account information below and the 2FA code on the followi
</form>
</div>
<script>
function showPassword() {
function showPassword() {
var x = document.getElementById("password");
if (x.type === "password") {
x.type = "text";
} else {
x.type = "password";
}
}
function getCookie(key) {
}
function getCookie(key) {
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null;
}
if (getCookie('error')) {
}
if (getCookie('error')) {
document.getElementById("errormsg").innerHTML = getCookie('error')
}
}
if (getCookie('systemId')) {
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>`
}
</script>
</body>
</html>

View File

@@ -41,7 +41,8 @@ input[type=submit]:hover {
</head>
<body>
<h3>Enter 2FA Code</h3>
<h2>Enter 2FA Code</h2>
<h3 id="systemId"></h3>
<p style="background-color:Red;" id="errormsg"></p></br>
<div class="container">
<form action="./submit-code" method="post">
@@ -52,12 +53,15 @@ input[type=submit]:hover {
</div>
<script>
function getCookie(key) {
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null;
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null;
}
if (getCookie('error')) {
document.getElementById("errormsg").innerHTML = getCookie('error')
document.getElementById("errormsg").innerHTML = getCookie('error')
}
</script>
if (getCookie('systemId')) {
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>`
}
</script>
</body>
</html>

View File

@@ -24,13 +24,23 @@ input[type=submit]:hover {
</style>
</head>
<body>
<h3>Ring Device Addon Connected</h3>
<h2>Ring Device Addon Connected</h2>
<h3 id="systemId"></h3>
It appears that this addon is already authenticated and connected to the Ring API, no additional action is required.
If you wish to force reauthentication, for example, to change the account used by this addon, clicking the button below will restart the authentication process.
<div class="container">
<form action="./force-token-generation" method="get">
<input type="submit" value="Force Reauthentication">
</form>
</div>
<form action="./force-token-generation" method="get">
<input type="submit" value="Force Reauthentication">
</form>
</div>
<script>
function getCookie(key) {
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null;
}
if (getCookie('systemId')) {
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>`
}
</script>
</body>
</html>

View File

@@ -7,7 +7,17 @@ body {font-family: Arial, Helvetica, sans-serif; max-width: 500px; color: white;
</style>
</head>
<body>
<h3>Refresh Token Generation Complete</h3>
<h2>Refresh Token Generation Complete</h2>
<h3 id="systemId"></h3>
The <b>Home Assistant ring-mqtt Add-on</b> will now automatically connect using the generated token. No additional configuration steps are required, please check the addon logs to monitor progress.
<script>
function getCookie(key) {
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null;
}
if (getCookie('systemId')) {
document.getElementById("systemId").innerHTML = `Device Name: <span style='color:chartreuse'> ${getCookie('systemId')}</span>`
}
</script>
</body>
</html>