Skip to content

Commit

Permalink
Merge pull request #888 from tsightler/dev
Browse files Browse the repository at this point in the history
Release v5.7.0
  • Loading branch information
tsightler authored Aug 12, 2024
2 parents 26775fc + 3daa9ea commit 848cffe
Show file tree
Hide file tree
Showing 29 changed files with 1,426 additions and 2,316 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Auto detect text files and perform LF normalization
* text=auto
*.sh text
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM alpine:3.18
FROM alpine:3.20

ENV LANG="C.UTF-8" \
PS1="$(whoami)@$(hostname):$(pwd)$ " \
Expand All @@ -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 / && \
Expand Down Expand Up @@ -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 && \
Expand Down
Binary file added bin/go2rtc_linux_amd64
Binary file not shown.
Binary file added bin/go2rtc_linux_arm
Binary file not shown.
Binary file added bin/go2rtc_linux_arm64
Binary file not shown.
2 changes: 1 addition & 1 deletion devices/beam-outdoor-plug.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion devices/beam.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
29 changes: 22 additions & 7 deletions devices/camera-livestream.js
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -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
}
}
18 changes: 8 additions & 10 deletions devices/camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -485,39 +485,37 @@ 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?
const newDing = Boolean(!this.data[dingKind].active_ding)
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
Expand Down
2 changes: 1 addition & 1 deletion devices/chime.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion devices/intercom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
}
Expand Down
2 changes: 1 addition & 1 deletion devices/security-panel.js
Original file line number Diff line number Diff line change
@@ -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'

Expand Down
4 changes: 3 additions & 1 deletion devices/thermostat.js
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -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')
}
Expand Down
19 changes: 19 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
17 changes: 17 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -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"
}
}
];
20 changes: 10 additions & 10 deletions init-ring-mqtt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)')
}
}
Expand All @@ -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'

Expand Down Expand Up @@ -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)
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions lib/go2rtc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }))
Expand Down Expand Up @@ -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] ')))
})
}

Expand Down
4 changes: 2 additions & 2 deletions lib/main.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
1 change: 1 addition & 0 deletions lib/ring.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 2 additions & 6 deletions lib/streaming/peer-connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
Expand Down Expand Up @@ -131,9 +129,7 @@ export class WeriftPeerConnection extends Subscribed {
}

close() {
this.pc.close().catch((e) => {
//debug
})
this.pc.close().catch()
this.unsubscribe()
}
}
5 changes: 3 additions & 2 deletions lib/streaming/webrtc-connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -162,6 +162,7 @@ export class WebrtcConnection extends Subscribed {
return
}
break
}
case 'close':
this.callEnded()
return
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 848cffe

Please sign in to comment.