mirror of
https://github.com/tsightler/ring-mqtt.git
synced 2025-09-26 21:01:12 +08:00
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1,2 +1,3 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
*.sh text
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.18
|
||||
FROM alpine:3.20
|
||||
|
||||
ENV LANG="C.UTF-8" \
|
||||
PS1="$(whoami)@$(hostname):$(pwd)$ " \
|
||||
@@ -11,7 +11,7 @@ ENV LANG="C.UTF-8" \
|
||||
COPY . /app/ring-mqtt
|
||||
RUN S6_VERSION="v3.2.0.0" && \
|
||||
BASHIO_VERSION="v0.16.2" && \
|
||||
GO2RTC_VERSION="v1.9.2" && \
|
||||
GO2RTC_VERSION="v1.9.4" && \
|
||||
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 && \
|
||||
curl -L -s "https://github.com/just-containers/s6-overlay/releases/download/${S6_VERSION}/s6-overlay-noarch.tar.xz" | tar -Jxpf - -C / && \
|
||||
@@ -42,7 +42,9 @@ RUN S6_VERSION="v3.2.0.0" && \
|
||||
exit 1;; \
|
||||
esac && \
|
||||
curl -L -s -o /usr/local/bin/go2rtc "https://github.com/AlexxIT/go2rtc/releases/download/${GO2RTC_VERSION}/go2rtc_linux_${GO2RTC_ARCH}" && \
|
||||
cp "/app/ring-mqtt/bin/go2rtc_linux_${GO2RTC_ARCH}" /usr/local/bin/go2rtc && \
|
||||
chmod +x /usr/local/bin/go2rtc && \
|
||||
rm -rf /app/ring-mqtt/bin && \
|
||||
curl -J -L -o /tmp/bashio.tar.gz "https://github.com/hassio-addons/bashio/archive/${BASHIO_VERSION}.tar.gz" && \
|
||||
mkdir /tmp/bashio && \
|
||||
tar zxvf /tmp/bashio.tar.gz --strip 1 -C /tmp/bashio && \
|
||||
|
BIN
bin/go2rtc_linux_amd64
Normal file
BIN
bin/go2rtc_linux_amd64
Normal file
Binary file not shown.
BIN
bin/go2rtc_linux_arm
Normal file
BIN
bin/go2rtc_linux_arm
Normal file
Binary file not shown.
BIN
bin/go2rtc_linux_arm64
Normal file
BIN
bin/go2rtc_linux_arm64
Normal file
Binary file not shown.
@@ -66,7 +66,7 @@ export default class BeamOutdoorPlug extends RingSocketDevice {
|
||||
case 'on':
|
||||
case 'off': {
|
||||
const duration = 32767
|
||||
const data = Boolean(command === 'on') ? { lightMode: 'on', duration } : { lightMode: 'default' }
|
||||
const data = command === 'on' ? { lightMode: 'on', duration } : { lightMode: 'default' }
|
||||
this[outletId].sendCommand('light-mode.set', data)
|
||||
break;
|
||||
}
|
||||
|
@@ -126,7 +126,7 @@ export default class Beam extends RingSocketDevice {
|
||||
if (this.isLightGroup && this.groupId) {
|
||||
this.device.location.setLightGroup(this.groupId, Boolean(command === 'on'), duration)
|
||||
} else {
|
||||
const data = Boolean(command === 'on') ? { lightMode: 'on', duration } : { lightMode: 'default' }
|
||||
const data = command === 'on' ? { lightMode: 'on', duration } : { lightMode: 'default' }
|
||||
this.device.sendCommand('light-mode.set', data)
|
||||
}
|
||||
break;
|
||||
|
@@ -5,13 +5,20 @@ import { StreamingSession } from '../lib/streaming/streaming-session.js'
|
||||
const deviceName = workerData.deviceName
|
||||
const doorbotId = workerData.doorbotId
|
||||
let liveStream = false
|
||||
let streamStopping = false
|
||||
|
||||
parentPort.on("message", async(data) => {
|
||||
const streamData = data.streamData
|
||||
switch (data.command) {
|
||||
case 'start':
|
||||
if (!liveStream) {
|
||||
if (streamStopping) {
|
||||
parentPort.postMessage({type: 'log_error', data: "Live stream could not be started because it is in stopping state"})
|
||||
parentPort.postMessage({type: 'state', data: 'failed'})
|
||||
} else if (!liveStream) {
|
||||
startLiveStream(streamData)
|
||||
} else {
|
||||
parentPort.postMessage({type: 'log_error', data: "Live stream could not be started because there is already an active stream"})
|
||||
parentPort.postMessage({type: 'state', data: 'active'})
|
||||
}
|
||||
break;
|
||||
case 'stop':
|
||||
@@ -87,11 +94,19 @@ async function startLiveStream(streamData) {
|
||||
}
|
||||
|
||||
async function stopLiveStream() {
|
||||
liveStream.stop()
|
||||
await new Promise(res => setTimeout(res, 2000))
|
||||
if (liveStream) {
|
||||
parentPort.postMessage({type: 'log_info', data: 'Live stream failed to stop on request, deleting anyway...'})
|
||||
parentPort.postMessage({type: 'state', data: 'inactive'})
|
||||
liveStream = false
|
||||
if (!streamStopping) {
|
||||
streamStopping = true
|
||||
let stopTimeout = 10
|
||||
liveStream.stop()
|
||||
do {
|
||||
await new Promise(res => setTimeout(res, 200))
|
||||
if (liveStream) {
|
||||
parentPort.postMessage({type: 'log_info', data: 'Live stream failed to stop on request, deleting anyway...'})
|
||||
parentPort.postMessage({type: 'state', data: 'inactive'})
|
||||
liveStream = false
|
||||
}
|
||||
stopTimeout--
|
||||
} while (liveStream && stopTimeout)
|
||||
streamStopping = false
|
||||
}
|
||||
}
|
@@ -485,19 +485,17 @@ export default class Camera extends RingPolledDevice {
|
||||
async processNotification(pushData) {
|
||||
let dingKind
|
||||
// Is it a motion or doorbell ding? (for others we do nothing)
|
||||
switch (pushData.action) {
|
||||
case 'com.ring.push.HANDLE_NEW_DING':
|
||||
switch (pushData.android_config?.category) {
|
||||
case 'com.ring.pn.live-event.ding':
|
||||
dingKind = 'ding'
|
||||
break
|
||||
case 'com.ring.push.HANDLE_NEW_motion':
|
||||
case 'com.ring.pn.live-event.motion':
|
||||
dingKind = 'motion'
|
||||
break
|
||||
default:
|
||||
this.debug(`Received push notification of unknown type ${pushData.action}`)
|
||||
return
|
||||
}
|
||||
const ding = pushData.ding
|
||||
ding.created_at = Math.floor(Date.now()/1000)
|
||||
this.debug(`Received ${dingKind} push notification, expires in ${this.data[dingKind].duration} seconds`)
|
||||
|
||||
// Is this a new Ding or refresh of active ding?
|
||||
@@ -505,19 +503,19 @@ export default class Camera extends RingPolledDevice {
|
||||
this.data[dingKind].active_ding = true
|
||||
|
||||
// Update last_ding and expire time
|
||||
this.data[dingKind].last_ding = ding.created_at
|
||||
this.data[dingKind].last_ding_time = utils.getISOTime(ding.created_at*1000)
|
||||
this.data[dingKind].last_ding = Math.floor(pushData.data?.event?.eventito?.timestamp/1000)
|
||||
this.data[dingKind].last_ding_time = pushData.data?.event?.ding?.created_at
|
||||
this.data[dingKind].last_ding_expires = this.data[dingKind].last_ding+this.data[dingKind].duration
|
||||
|
||||
// If motion ding and snapshots on motion are enabled, publish a new snapshot
|
||||
if (dingKind === 'motion') {
|
||||
this.data[dingKind].is_person = Boolean(ding.detection_type === 'human')
|
||||
this.data[dingKind].is_person = Boolean(pushData.data?.event?.ding?.detection_type === 'human')
|
||||
if (this.data.snapshot.motion) {
|
||||
this.refreshSnapshot('motion', ding.image_uuid)
|
||||
this.refreshSnapshot('motion', pushData?.img?.snapshot_uuid)
|
||||
}
|
||||
} else if (this.data.snapshot.ding) {
|
||||
// If doorbell press and snapshots on ding are enabled, publish a new snapshot
|
||||
this.refreshSnapshot('ding', ding.image_uuid)
|
||||
this.refreshSnapshot('ding', pushData?.img?.snapshot_uuid)
|
||||
}
|
||||
|
||||
// Publish MQTT active sensor state
|
||||
|
@@ -101,7 +101,7 @@ export default class Chime extends RingPolledDevice {
|
||||
this.data.volume = volumeState
|
||||
}
|
||||
|
||||
const snoozeState = Boolean(this.device.data.do_not_disturb.seconds_left) ? 'ON' : 'OFF'
|
||||
const snoozeState = this.device.data.do_not_disturb.seconds_left ? 'ON' : 'OFF'
|
||||
if (snoozeState !== this.data.snooze || isPublish) {
|
||||
this.mqttPublish(this.entity.snooze.state_topic, snoozeState)
|
||||
this.data.snooze = snoozeState
|
||||
|
@@ -112,7 +112,7 @@ export default class Lock extends RingPolledDevice {
|
||||
}
|
||||
this.mqttPublish(this.entity.info.state_topic, JSON.stringify(attributes), 'attr')
|
||||
this.publishAttributeEntities(attributes)
|
||||
} catch(error) {
|
||||
} catch {
|
||||
this.debug('Could not publish attributes due to no health data')
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import RingSocketDevice from './base-socket-device.js'
|
||||
import { allAlarmStates, RingDeviceType } from 'ring-client-api'
|
||||
import { allAlarmStates } from 'ring-client-api'
|
||||
import utils from '../lib/utils.js'
|
||||
import state from '../lib/state.js'
|
||||
|
||||
|
@@ -164,6 +164,7 @@ export default class Thermostat extends RingSocketDevice {
|
||||
switch(mode) {
|
||||
case 'off':
|
||||
this.mqttPublish(this.entity.thermostat.action_topic, mode)
|
||||
// Fall through
|
||||
case 'cool':
|
||||
case 'heat':
|
||||
case 'auto':
|
||||
@@ -262,11 +263,12 @@ export default class Thermostat extends RingSocketDevice {
|
||||
const presetMode = value.toLowerCase()
|
||||
switch(presetMode) {
|
||||
case 'auxillary':
|
||||
case 'none':
|
||||
case 'none': {
|
||||
const mode = presetMode === 'auxillary' ? 'aux' : 'heat'
|
||||
this.device.setInfo({ device: { v1: { mode } } })
|
||||
this.mqttPublish(this.entity.thermostat.preset_mode_state_topic, presetMode.replace(/^./, str => str.toUpperCase()))
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this.debug('Received invalid preset mode command')
|
||||
}
|
||||
|
@@ -1,3 +1,22 @@
|
||||
## v5.7.0
|
||||
This release migrates to the new FCM HTTP v1 API for push notifications as the legacy FCM/GCM APIs have been deprecated for some time and shutdown of those legacy APIs started in late July 2024. While the transition to this new API should be transparent for most users, the under the hood changes are signfiicant including an entirely new push notification format. While the goal is to make this transition as seemless as possible, it is impossible to guarantee 100% success. If you experience issues with motion/ding notification from cameras, doorbells or intercoms after upgrading to this version, please follow the standard push notification troubleshooting steps as follows:
|
||||
|
||||
1) Open the ring-mqtt web UI and note the device name
|
||||
2) Stop the ring-mqtt addon/container
|
||||
3) Navigate to the Ring Control Center using the Ring App or Ring Web console
|
||||
4) Locate the device with the matching device name from step 1 in Authorized Client Devices and delete it
|
||||
5) Restart the addon and use the addon web UI to re-authenticate to the Ring API
|
||||
|
||||
**Minor Enhancements**
|
||||
- A significant amount of work has gone into improving the reliability of streaming, especially the live stream. In prior versions there were various failure scenarios that could lead to states where future streaming requests would not succeed and the only option was to restart the entire addon/container. Hours of testing have gone into this version and many such issues have been addressed.
|
||||
|
||||
**Dependency Updates**
|
||||
- ring-client-api v13.0.1
|
||||
- go2rtc v1.9.4 (custom build to fix a hang on exit issue)
|
||||
- Alpine Linux 3.20.2
|
||||
- NodeJS v20.15.1
|
||||
- s6-overlay v3.2.0.0
|
||||
|
||||
## v5.6.7
|
||||
This release is intended to address an ongoing instability with websocket connections by using a newer API endpoint for requesting tickets.
|
||||
|
||||
|
17
eslint.config.js
Normal file
17
eslint.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
|
||||
|
||||
export default [
|
||||
{
|
||||
languageOptions: {
|
||||
globals: globals.node
|
||||
}
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
"no-prototype-builtins": "off"
|
||||
}
|
||||
}
|
||||
];
|
@@ -12,11 +12,11 @@ async function getRefreshToken(systemId) {
|
||||
let generatedToken
|
||||
const email = await requestInput('Email: ')
|
||||
const password = await requestInput('Password: ')
|
||||
const restClient = new RingRestClient({
|
||||
email,
|
||||
password,
|
||||
const restClient = new RingRestClient({
|
||||
email,
|
||||
password,
|
||||
controlCenterDisplayName: `ring-mqtt-${systemId.slice(-5)}`,
|
||||
systemId: systemId
|
||||
systemId: systemId
|
||||
})
|
||||
try {
|
||||
await restClient.getCurrentAuth()
|
||||
@@ -28,12 +28,12 @@ async function getRefreshToken(systemId) {
|
||||
}
|
||||
}
|
||||
|
||||
while(!generatedToken) {
|
||||
while(!generatedToken) {
|
||||
const code = await requestInput('2FA Code: ')
|
||||
try {
|
||||
generatedToken = await restClient.getAuth(code)
|
||||
return generatedToken.refresh_token
|
||||
} catch(err) {
|
||||
} catch {
|
||||
throw('Failed to validate the entered 2FA code. (error: invalid_code)')
|
||||
}
|
||||
}
|
||||
@@ -43,11 +43,11 @@ const main = async() => {
|
||||
let refresh_token
|
||||
let stateData = {}
|
||||
// If running in Docker set state file path as appropriate
|
||||
const stateFile = (fs.existsSync('/etc/cont-init.d/ring-mqtt.sh'))
|
||||
const stateFile = (fs.existsSync('/etc/cont-init.d/ring-mqtt.sh'))
|
||||
? '/data/ring-state.json'
|
||||
: dirname(fileURLToPath(new URL(import.meta.url)))+'/ring-state.json'
|
||||
|
||||
const configFile = (fs.existsSync('/etc/cont-init.d/ring-mqtt.sh'))
|
||||
|
||||
const configFile = (fs.existsSync('/etc/cont-init.d/ring-mqtt.sh'))
|
||||
? '/data/config.json'
|
||||
: dirname(fileURLToPath(new URL(import.meta.url)))+'/config.json'
|
||||
|
||||
@@ -109,7 +109,7 @@ const main = async() => {
|
||||
console.log('New config file written to '+configFile)
|
||||
} catch (err) {
|
||||
console.log('Failed to create new config file at '+stateFile)
|
||||
conslog.log(err)
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -31,10 +31,11 @@ export default new class Config {
|
||||
await this.loadConfigFile()
|
||||
this.doMqttDiscovery()
|
||||
break;
|
||||
default:
|
||||
default: {
|
||||
const configPath = dirname(fileURLToPath(new URL('.', import.meta.url)))+'/'
|
||||
this.file = (process.env.RINGMQTT_CONFIG) ? configPath+process.env.RINGMQTT_CONFIG : configPath+'config.json'
|
||||
await this.loadConfigFile()
|
||||
}
|
||||
}
|
||||
|
||||
// If there's still no configured settings, force some defaults.
|
||||
|
@@ -53,9 +53,9 @@ export default new class Go2RTC {
|
||||
config.streams = {}
|
||||
for (const camera of cameras) {
|
||||
config.streams[`${camera.deviceId}_live`] =
|
||||
`exec:${dirname(fileURLToPath(new URL('.', import.meta.url)))}/scripts/start-stream.sh ${camera.deviceId} live ${camera.deviceTopic} {output}`
|
||||
`exec:${dirname(fileURLToPath(new URL('.', import.meta.url)))}/scripts/start-stream.sh ${camera.deviceId} live ${camera.deviceTopic} {output}#killsignal=15`
|
||||
config.streams[`${camera.deviceId}_event`] =
|
||||
`exec:${dirname(fileURLToPath(new URL('.', import.meta.url)))}/scripts/start-stream.sh ${camera.deviceId} event ${camera.deviceTopic} {output}`
|
||||
`exec:${dirname(fileURLToPath(new URL('.', import.meta.url)))}/scripts/start-stream.sh ${camera.deviceId} event ${camera.deviceTopic} {output}#killsignal=15`
|
||||
}
|
||||
try {
|
||||
await writeFileAtomic(configFile, yaml.dump(config, { lineWidth: -1 }))
|
||||
@@ -91,13 +91,13 @@ export default new class Go2RTC {
|
||||
const stdoutLine = readline.createInterface({ input: this.go2rtcProcess.stdout })
|
||||
stdoutLine.on('line', (line) => {
|
||||
// Replace time in go2rtc log messages with tag
|
||||
debug(line.replace(/^.*\d{2}:\d{2}:\d{2}\.\d{3}([^\s]+) /, chalk.green('[go2rtc] ')))
|
||||
debug(line.replace(/^.*\d{2}:\d{2}:\d{2}\.\d{3} /, chalk.green('[go2rtc] ')))
|
||||
})
|
||||
|
||||
const stderrLine = readline.createInterface({ input: this.go2rtcProcess.stderr })
|
||||
stderrLine.on('line', (line) => {
|
||||
// Replace time in go2rtc log messages with tag
|
||||
debug(line.replace(/^.*\d{2}:\d{2}:\d{2}\.\d{3}([^\s]+) /, '[go2rtc] '))
|
||||
debug(line.replace(/^.*\d{2}:\d{2}:\d{2}\.\d{3} /, chalk.green('[go2rtc] ')))
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import exithandler from './exithandler.js'
|
||||
import mqtt from './mqtt.js'
|
||||
export * from './exithandler.js'
|
||||
export * from './mqtt.js'
|
||||
import state from './state.js'
|
||||
import ring from './ring.js'
|
||||
import utils from './utils.js'
|
||||
|
@@ -241,6 +241,7 @@ export default new class RingMqtt {
|
||||
case 'not-supported':
|
||||
// Save unsupported device type for log output later
|
||||
unsupportedDevices.push(device.deviceType)
|
||||
// fall through
|
||||
case 'ignore':
|
||||
ringDevice=false
|
||||
break
|
||||
|
@@ -87,9 +87,7 @@ export class WeriftPeerConnection extends Subscribed {
|
||||
this.addSubscriptions(merge(this.onRequestKeyFrame, interval(4000)).subscribe(() => {
|
||||
videoTransceiver.receiver
|
||||
.sendRtcpPLI(track.ssrc)
|
||||
.catch((e) => {
|
||||
// debug(e)
|
||||
})
|
||||
.catch()
|
||||
}))
|
||||
this.requestKeyFrame()
|
||||
})
|
||||
@@ -131,9 +129,7 @@ export class WeriftPeerConnection extends Subscribed {
|
||||
}
|
||||
|
||||
close() {
|
||||
this.pc.close().catch((e) => {
|
||||
//debug
|
||||
})
|
||||
this.pc.close().catch()
|
||||
this.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
@@ -150,7 +150,7 @@ export class WebrtcConnection extends Subscribed {
|
||||
return
|
||||
case 'pong':
|
||||
return
|
||||
case 'notification':
|
||||
case 'notification': {
|
||||
const { text } = message.body
|
||||
if (text === 'camera_connected') {
|
||||
this.onCameraConnected.next()
|
||||
@@ -162,6 +162,7 @@ export class WebrtcConnection extends Subscribed {
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'close':
|
||||
this.callEnded()
|
||||
return
|
||||
@@ -219,7 +220,7 @@ export class WebrtcConnection extends Subscribed {
|
||||
})
|
||||
this.ws.close()
|
||||
}
|
||||
catch (_) {
|
||||
catch {
|
||||
// ignore any errors since we are stopping the call
|
||||
}
|
||||
this.hasEnded = true
|
||||
|
@@ -100,7 +100,7 @@ export default new class TokenApp {
|
||||
const code = req.body.code
|
||||
try {
|
||||
generatedToken = await restClient.getAuth(code)
|
||||
} catch(err) {
|
||||
} catch {
|
||||
generatedToken = false
|
||||
const errormsg = 'The 2FA code was not accepted, please verify the code and try again.'
|
||||
debug(errormsg)
|
||||
|
3311
package-lock.json
generated
3311
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ring-mqtt",
|
||||
"version": "5.6.7",
|
||||
"version": "5.7.0",
|
||||
"type": "module",
|
||||
"description": "Ring Devices via MQTT",
|
||||
"main": "ring-mqtt.js",
|
||||
@@ -9,19 +9,24 @@
|
||||
"body-parser": "^1.20.2",
|
||||
"chalk": "^5.3.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"debug": "^4.3.5",
|
||||
"debug": "^4.3.6",
|
||||
"express": "^4.19.2",
|
||||
"is-online": "^10.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"minimist": "^1.2.8",
|
||||
"mqtt": "^5.7.0",
|
||||
"ring-client-api": "12.1.1",
|
||||
"mqtt": "^5.9.1",
|
||||
"ring-client-api": "^13.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"werift": "^0.19.3",
|
||||
"werift": "^0.19.4",
|
||||
"write-file-atomic": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.32.0"
|
||||
"@eslint/js": "^9.8.0",
|
||||
"eslint": "^9.8.0",
|
||||
"globals": "^15.9.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@eneris/push-receiver": "4.1.5"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
|
@@ -1,2 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
import Main from './lib/main.js'
|
||||
export * from './lib/main.js'
|
||||
|
100
scripts/monitor-stream.sh
Normal file
100
scripts/monitor-stream.sh
Normal file
@@ -0,0 +1,100 @@
|
||||
#!/bin/bash
|
||||
# Activate video stream on Ring cameras via ring-mqtt
|
||||
# Intended only for use as on-demand script for rtsp-simple-server
|
||||
# Requires mosquitto MQTT clients package to be installed
|
||||
# Uses ring-mqtt internal IPC broker for communications with main process
|
||||
# Provides status updates and termintates stream on script exit
|
||||
|
||||
# Required command line arguments
|
||||
device_id=${1} # Camera device Id
|
||||
type=${2} # Stream type ("live" or "event")
|
||||
base_topic=${3} # Command topic for Camera entity
|
||||
rtsp_pub_url=${4} # URL for publishing RTSP stream
|
||||
client_id="${device_id}_${type}" # Id used to connect to the MQTT broker, camera Id + event type
|
||||
activated="false"
|
||||
reason="none"
|
||||
|
||||
[[ ${type} = "live" ]] && base_topic="${base_topic}/stream" || base_topic="${base_topic}/event_stream"
|
||||
json_attribute_topic="${base_topic}/attributes"
|
||||
command_topic="${base_topic}/command"
|
||||
debug_topic="${base_topic}/debug"
|
||||
|
||||
# Set some colors for debug output
|
||||
red='\e[0;31m'
|
||||
yellow='\e[0;33m'
|
||||
green='\e[0;32m'
|
||||
blue='\e[0;34m'
|
||||
reset='\e[0m'
|
||||
|
||||
cleanup() {
|
||||
local ffpids=$(pgrep -f "ffmpeg.*${rtsp_pub_url}" | grep -v ^$$\$)
|
||||
[ -n "$ffpids" ] && kill -9 $ffpids
|
||||
local pids=$(pgrep -f "mosquitto_sub.*${client_id}_sub" | grep -v ^$$\$)
|
||||
[ -n "$pids" ] && kill $pids
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 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}"
|
||||
}
|
||||
|
||||
# Trap signals so that the MQTT command to stop the stream can be published on exit
|
||||
trap cleanup INT TERM
|
||||
|
||||
# This loop starts mosquitto_sub with a subscription on the camera stream topic that sends all received
|
||||
# messages via file descriptor to the read process. On initial startup the script publishes the message
|
||||
# 'ON-DEMAND' to the stream command topic which lets ring-mqtt know that an RTSP client has requested
|
||||
# the stream. Stream state is determined via the the detailed stream state messages received via the
|
||||
# json_attributes_topic:
|
||||
#
|
||||
# "inactive" = There is no active video stream and none currently requested
|
||||
# "activating" = A video stream has been requested and is initializing but has not yet started
|
||||
# "active" = The stream was requested successfully and an active stream is currently in progress
|
||||
# "failed" = A live stream was requested but failed to start
|
||||
mosquitto_sub -q 1 -i "${client_id}_sub" -L "mqtt://127.0.0.1:51883/${json_attribute_topic}" |
|
||||
while read message; do
|
||||
# Otherwise it should be a JSON message from the stream state attribute topic so extract the detailed stream state
|
||||
stream_state=`echo ${message} | jq -r '.status'`
|
||||
case ${stream_state,,} in
|
||||
activating)
|
||||
if [ ${activated} = "false" ]; then
|
||||
logger "State indicates ${type} stream is activating"
|
||||
fi
|
||||
;;
|
||||
active)
|
||||
if [ ${activated} = "false" ]; then
|
||||
logger "State indicates ${type} stream is active"
|
||||
activated="true"
|
||||
fi
|
||||
;;
|
||||
deactivate)
|
||||
if [ ${activated} = "true" ]; then
|
||||
reason='deactivate'
|
||||
fi
|
||||
;;
|
||||
inactive)
|
||||
if [ ${reason} = "deactivate" ] ; then
|
||||
logmsg="State indicates ${type} stream is inactive"
|
||||
else
|
||||
logmsg=$(echo -en "${yellow}State indicates ${type} stream has gone unexpectedly inactive${reset}")
|
||||
fi
|
||||
logger "${logmsg}"
|
||||
reason='inactive'
|
||||
cleanup
|
||||
;;
|
||||
failed)
|
||||
logmsg=$(echo -en "${red}ERROR - State indicates ${type} stream failed to activate${reset}")
|
||||
logger "${logmsg}"
|
||||
reason='failed'
|
||||
cleanup
|
||||
;;
|
||||
*)
|
||||
logmsg=$(echo -en "${red}ERROR - Received unknown ${type} stream state on topic ${blue}${json_attribute_topic}${reset}")
|
||||
logger "${logmsg}"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
cleanup
|
@@ -1,9 +1,17 @@
|
||||
#!/bin/bash
|
||||
# Activate video stream on Ring cameras via ring-mqtt
|
||||
# Intended only for use as on-demand script for rtsp-simple-server
|
||||
# Activate Ring camera video stream via ring-mqtt
|
||||
#
|
||||
# This script is intended for use only with ring-mqtt
|
||||
# and go2rtc.
|
||||
#
|
||||
# Requires mosquitto MQTT clients package to be installed
|
||||
# Uses ring-mqtt internal IPC broker for communications with main process
|
||||
# Provides status updates and termintates stream on script exit
|
||||
# Uses ring-mqtt internal IPC broker for communication with
|
||||
# ring-mqtt process
|
||||
#
|
||||
# Spawns stream control in background due to issues with
|
||||
# process exit hanging go2rtc. Script then just monitors
|
||||
# for control script to exit or, if script is killed,
|
||||
# sends commands to control script prior to exiting
|
||||
|
||||
# Required command line arguments
|
||||
device_id=${1} # Camera device Id
|
||||
@@ -11,10 +19,20 @@ type=${2} # Stream type ("live" or "event")
|
||||
base_topic=${3} # Command topic for Camera entity
|
||||
rtsp_pub_url=${4} # URL for publishing RTSP stream
|
||||
client_id="${device_id}_${type}" # Id used to connect to the MQTT broker, camera Id + event type
|
||||
activated="false"
|
||||
|
||||
# If previous run hasn't exited yet, just perform a short wait and exit with error
|
||||
if test -f /tmp/ring-mqtt-${device_id}.lock; then
|
||||
sleep .1
|
||||
exit 1
|
||||
else
|
||||
touch /tmp/ring-mqtt-${device_id}.lock
|
||||
fi
|
||||
|
||||
script_dir=$(dirname "$0")
|
||||
${script_dir}/monitor-stream.sh ${1} ${2} ${3} ${4} &
|
||||
|
||||
# Build the MQTT topics
|
||||
[[ ${type} = "live" ]] && base_topic="${base_topic}/stream" || base_topic="${base_topic}/event_stream"
|
||||
|
||||
json_attribute_topic="${base_topic}/attributes"
|
||||
command_topic="${base_topic}/command"
|
||||
debug_topic="${base_topic}/debug"
|
||||
@@ -26,76 +44,39 @@ green='\e[0;32m'
|
||||
blue='\e[0;34m'
|
||||
reset='\e[0m'
|
||||
|
||||
stop() {
|
||||
# Interrupted by signal so send command to stop stream
|
||||
# Send message to monitor script that stream was requested to stop so that it doesn't log a warning
|
||||
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${json_attribute_topic}" -m {\"status\":\"deactivate\"}
|
||||
|
||||
# Send ring-mqtt the command to stop the stream
|
||||
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${debug_topic}" -m "Deactivating ${type} stream due to signal from RTSP server (no more active clients or publisher ended stream)"
|
||||
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${command_topic}" -m "OFF"
|
||||
|
||||
# Send kill signal to monitor script and wait for it to exit
|
||||
local pids=$(jobs -pr)
|
||||
[ -n "$pids" ] && kill $pids
|
||||
wait
|
||||
cleanup
|
||||
}
|
||||
|
||||
# If control script is still runnning send kill signal and exit
|
||||
cleanup() {
|
||||
if [ -z ${reason} ]; then
|
||||
# If no reason defined, that means we were interrupted by a signal, send the command to stop the live stream
|
||||
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${debug_topic}" -m "Deactivating ${type} stream due to signal from RTSP server (no more active clients or publisher ended stream)"
|
||||
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${command_topic}" -m "OFF"
|
||||
fi
|
||||
# Kill the spawed mosquitto_sub process or it will stay listening forever
|
||||
kill $(pgrep -f "mosquitto_sub.*${client_id}_sub" | grep -v ^$$\$)
|
||||
rm -f /tmp/ring-mqtt-${device_id}.lock
|
||||
# For some reason sleeping for 100ms seems to keep go2rtc from hanging
|
||||
exit 0
|
||||
}
|
||||
|
||||
# go2rtc does not pass stdout through from child processes so send debug logs
|
||||
# via main process using MQTT messages
|
||||
# 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}"
|
||||
}
|
||||
|
||||
# Trap signals so that the MQTT command to stop the stream can be published on exit
|
||||
trap cleanup INT TERM QUIT
|
||||
trap stop INT TERM EXIT
|
||||
|
||||
# This loop starts mosquitto_sub with a subscription on the camera stream topic that sends all received
|
||||
# messages via file descriptor to the read process. On initial startup the script publishes the message
|
||||
# 'ON-DEMAND' to the stream command topic which lets ring-mqtt know that an RTSP client has requested
|
||||
# the stream. Stream state is determined via the the detailed stream state messages received via the
|
||||
# json_attributes_topic:
|
||||
#
|
||||
# "inactive" = There is no active video stream and none currently requested
|
||||
# "activating" = A video stream has been requested and is initializing but has not yet started
|
||||
# "active" = The stream was requested successfully and an active stream is currently in progress
|
||||
# "failed" = A live stream was requested but failed to start
|
||||
while read -u 10 message
|
||||
do
|
||||
# If start message received, publish the command to start stream
|
||||
if [ ${message} = "START" ]; then
|
||||
logger "Sending command to activate ${type} stream ON-DEMAND"
|
||||
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${command_topic}" -m "ON-DEMAND ${rtsp_pub_url}"
|
||||
else
|
||||
# Otherwise it should be a JSON message from the stream state attribute topic so extract the detailed stream state
|
||||
stream_state=`echo ${message} | jq -r '.status'`
|
||||
case ${stream_state,,} in
|
||||
activating)
|
||||
if [ ${activated} = "false" ]; then
|
||||
logger "State indicates ${type} stream is activating"
|
||||
fi
|
||||
;;
|
||||
active)
|
||||
if [ ${activated} = "false" ]; then
|
||||
logger "State indicates ${type} stream is active"
|
||||
activated="true"
|
||||
fi
|
||||
;;
|
||||
inactive)
|
||||
logmsg=$(echo -en "${yellow}State indicates ${type} stream has gone inactive${reset}")
|
||||
logger "${logmsg}"
|
||||
reason='inactive'
|
||||
cleanup
|
||||
;;
|
||||
failed)
|
||||
logmsg=$(echo -en "${red}ERROR - State indicates ${type} stream failed to activate${reset}")
|
||||
logger "${logmsg}"
|
||||
reason='failed'
|
||||
cleanup
|
||||
;;
|
||||
*)
|
||||
logmsg=$(echo -en "${red}ERROR - Received unknown ${type} stream state on topic ${blue}${json_attribute_topic}${reset}")
|
||||
logger "${logmsg}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done 10< <(mosquitto_sub -q 1 -i "${client_id}_sub" -L "mqtt://127.0.0.1:51883/${json_attribute_topic}" & echo "START")
|
||||
logger "Sending command to activate ${type} stream ON-DEMAND"
|
||||
mosquitto_pub -i "${client_id}_pub" -L "mqtt://127.0.0.1:51883/${command_topic}" -m "ON-DEMAND ${rtsp_pub_url}" &
|
||||
|
||||
cleanup
|
||||
exit 0
|
||||
wait
|
||||
cleanup
|
@@ -22,22 +22,40 @@ else
|
||||
echo "The ring-mqtt-${BRANCH} branch has been updated."
|
||||
|
||||
APK_ARCH="$(apk --print-arch)"
|
||||
GO2RTC_VERSION="v1.9.2"
|
||||
GO2RTC_VERSION="v1.9.4"
|
||||
case "${APK_ARCH}" in
|
||||
x86_64)
|
||||
GO2RTC_ARCH="amd64";;
|
||||
GO2RTC_ARCH="amd64"
|
||||
;;
|
||||
aarch64)
|
||||
GO2RTC_ARCH="arm64";;
|
||||
GO2RTC_ARCH="arm64"
|
||||
;;
|
||||
armv7|armhf)
|
||||
GO2RTC_ARCH="arm";;
|
||||
GO2RTC_ARCH="arm"
|
||||
;;
|
||||
*)
|
||||
echo >&2 "ERROR: Unsupported architecture '$APK_ARCH'"
|
||||
exit 1;;
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
rm -f /usr/local/bin/go2rtc
|
||||
curl -L -s -o /usr/local/bin/go2rtc "https://github.com/AlexxIT/go2rtc/releases/download/${GO2RTC_VERSION}/go2rtc_linux_${GO2RTC_ARCH}"
|
||||
#curl -L -s -o /usr/local/bin/go2rtc "https://github.com/AlexxIT/go2rtc/releases/download/${GO2RTC_VERSION}/go2rtc_linux_${GO2RTC_ARCH}"
|
||||
cp "/app/ring-mqtt-${BRANCH}/bin/go2rtc_linux_${GO2RTC_ARCH}" /usr/local/bin/go2rtc
|
||||
chmod +x /usr/local/bin/go2rtc
|
||||
|
||||
case "${APK_ARCH}" in
|
||||
x86_64)
|
||||
apk del npm nodejs
|
||||
apk add libstdc++
|
||||
cd /opt
|
||||
wget https://unofficial-builds.nodejs.org/download/release/v20.16.0/node-v20.16.0-linux-x64-musl.tar.gz
|
||||
mkdir nodejs
|
||||
tar -zxvf *.tar.gz --directory /opt/nodejs --strip-components=1
|
||||
ln -s /opt/nodejs/bin/node /usr/local/bin/node
|
||||
ln -s /opt/nodejs/bin/npm /usr/local/bin/npm
|
||||
;;
|
||||
esac
|
||||
|
||||
cp -f "/app/ring-mqtt-${BRANCH}/init/s6/services.d/ring-mqtt/run" /etc/services.d/ring-mqtt/run
|
||||
chmod +x /etc/services.d/ring-mqtt/run
|
||||
fi
|
Reference in New Issue
Block a user