mirror of
https://github.com/tsightler/ring-mqtt.git
synced 2025-09-26 21:01:12 +08:00
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:
@@ -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 && \
|
||||
|
@@ -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}` : '';
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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()
|
||||
|
@@ -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() }
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -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()
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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)
|
||||
|
113
lib/ring.js
113
lib/ring.js
@@ -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
|
||||
|
@@ -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
145
package-lock.json
generated
@@ -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"
|
||||
},
|
||||
|
@@ -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"
|
||||
|
@@ -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}"
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user