This repository has been archived by the owner on Mar 28, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathapp.js
256 lines (223 loc) · 9.7 KB
/
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
/*
* app.js
*
* This node application acts as a bridge between the ring-client-api and the PiPup Android
* application to show Ring camera snapshots as an overlay/popup on Android TV devices.
*
* Remember to change the tvIpAddress variable and save your API token to token.txt.
*/
// Dependencies
const Ring = require('ring-client-api')
const fs = require('fs')
const request = require('request')
const { promisify } = require('util')
const { exit } = require('process')
require('dotenv').config()
// Configuration
const tvIpAddress = process.env.R2ATV_IP_ADDRESS // IP address of the Android TV you are running PiPup on
const displayTime = process.env.R2ATV_DISPLAY_TIME || 12 // Display time for notifications, in seconds
/**
* Sends a notification to PiPup app on Android TV.
* @param {*} title Title of notification message.
* @param {*} message Text of notification message.
* @param {*} imageFile Path to image file, can be blank string to display no image.
* @param {*} exitAfter If true, calls process.exit() after completing request.
*/
async function sendNotification(title, message, imageFile, exitAfter = false) {
const options = {
method: "POST",
url: "http://" + tvIpAddress + ":7979/notify",
port: 7979,
headers: {
"Content-Type": "multipart/form-data"
},
formData: {
"duration": displayTime,
"position": 0,
"title": title,
"titleColor": "#0066cc",
"titleSize": 20,
"message": message,
"messageColor": "#000000",
"messageSize": 14,
"backgroundColor": "#ffffff",
"image" : (imageFile == '') ? "" : fs.createReadStream(__dirname + '/' + imageFile),
"imageWidth": 640
}
}
// Fire off POST message to PiPup with 'request'
request(options, function (err, res, body) {
if(err) {
console.log(`[ERROR] Error sending notification: ${title} - ${message}`)
console.log(err)
process.exitCode = 1
} else {
console.log(`Sent notification successfully: ${title} - ${message}`)
}
if(exitAfter) process.exit()
})
}
async function listLocationsAndCameras() {
locations = await ringApi.getLocations().catch(error => {
console.log('[ERROR] Unable to retrieve camera locations because: ' + error.message)
process.exit(1) // exit with error code
})
intLocation = 0
intCamera = 0
locations.forEach(function(location) {
intCamera = 0
console.log(`Found location[${intLocation}]: ${location.name}`)
// Subscribe to each camera at this location.
location.cameras.forEach(function(camera) {
console.log(`\t - Found ${camera.model} named ${camera.name}. Test with --test ${intLocation},${intCamera}`)
intCamera++
})
intLocation++
})
process.exit()
}
/**
* For testing: onnects to the first camera at first detected location, saves and sends a snapshot notification.
* @param {*} intLocation Number of location to use in Location array.
* @param {*} intCamera Number of camera to use in Location->Camera array.
*/
async function getTestSnapshot(intLocation = 0, intCamera = 0) {
const locations = await ringApi.getLocations().catch(error => {
console.log('[ERROR] Unable to retrieve camera locations because: ' + error.message)
process.exit(1) // exit with error code
})
const location = locations[intLocation]
const camera = location.cameras[intCamera]
console.log(`Attempting to get snapshot for location #${intLocation}, camera #${intCamera}`)
try {
const snapshotBuffer = await camera.getSnapshot()
console.log('Snapshot size: ' + Math.floor(snapshotBuffer.byteLength/1024) + ' kb')
fs.writeFile(__dirname + '/snapshot.png', snapshotBuffer, (err) => {
// throws an error, you could also catch it here
if (err) throw err;
// success case, the file was saved
console.log('Snapshot saved!')
sendNotification('Test Snapshot', 'This is a test snapshot message!', 'snapshot.png', true)
})
} catch (e) {
// failed to get a snapshot. handle the error however you please
console.log('Unable to get snapshot...')
console.log(e)
sendNotification('Test Snapshot Failed', 'An error occured trying to get a snapshot!', 'error.png', true)
}
}
/**
* Starts polling a Ring camera for events and grabs snapshots on motion/dings.
* @param {*} notifyOnStart Whether to send a notification when beginning camera polling.
*/
async function startCameraPolling(notifyOnStart) {
const locations = await ringApi.getLocations().catch(error => {
console.log('Unable to retrieve camera locations because: ' + error.message)
process.exit(1) // exit with error code
})
locations.forEach(function(location) {
console.log(`Found location: ${location.name}`)
// Subscribe to each camera at this location.
location.cameras.forEach(function(camera) {
console.log(`\t - Found ${camera.model} named ${camera.name}.`)
// Start the camera subscription to listen for motion/rings/etc...
camera.onNewDing.subscribe(async ding => {
var event = "Unknown Event"
var notifyTitle;
var notifyMessage;
// Get friendly name for event happening and set notification params.
switch(ding.kind) {
case "motion":
event = "Motion detected"
notifyTitle = 'Motion Detected'
notifyMessage = `Motion detected at ${camera.name}!`
break
case "ding":
event = "Doorbell pressed"
notifyTitle = 'Doorbell Ring'
notifyMessage = `Doorbell rung at ${camera.name}!`
break
default:
event = `Video started (${ding.kind})`
notifyTitle = 'Video Started'
notifyMessage = `Video started at ${camera.name}`
}
console.log(`[${new Date()}] ${event} on ${camera.name} camera.`)
// Grab new snapshot
try {
const snapshotBuffer = await camera.getSnapshot().catch(error => {
console.log('[ERROR] Unable to retrieve snapshot because:' + error.message)
})
fs.writeFile(__dirname + '/snapshot.png', snapshotBuffer, (err) => {
// throws an error, you could also catch it here
if (err) throw err;
// success case, the file was saved
console.log('Snapshot saved!');
sendNotification(notifyTitle, notifyMessage, 'snapshot.png')
})
} catch (e) {
// Failed to retrieve snapshot. We send text of notification along with error image.
// Most common errors are due to expired API token, or battery-powered camera taking too long to wake.
console.log('Unable to get snapshot.')
sendNotification(notifyTitle, notifyMessage, 'error.png')
}
console.log('')
}, err => {
console.log(`Error subscribing to ${location.name} ${camera.name}:`)
console.log(err)
},
() => {
console.log('Subscription complete.') // We shouldn't get here!
})
})
})
// Send notification on app start, if enabled.
if(notifyOnStart) sendNotification('ring-to-android-tv', 'Ring notifications started!', '')
}
// Set up Ring API object
ringApi = new Ring.RingApi({
refreshToken: process.env.R2ATV_API_TOKEN,
controlCenterDisplayName: 'ring-to-android-tv',
cameraDingsPollingSeconds: 5 // Default is 5, less seems to cause API token to expire.
})
// Automatically replace refresh tokens, as they now expire after each use.
// See: https://github.com/dgreif/ring/wiki/Refresh-Tokens#refresh-token-expiration
ringApi.onRefreshTokenUpdated.subscribe(
async ({ newRefreshToken, oldRefreshToken }) => {
console.log('Refresh Token Updated') // Changed from example, don't write new token to log.
if (!oldRefreshToken) {
return
}
const currentConfig = await promisify(fs.readFile)('.env'),
updatedConfig = currentConfig
.toString()
.replace(oldRefreshToken, newRefreshToken)
await promisify(fs.writeFile)('.env', updatedConfig)
}
)
if(process.argv.includes('--test')) {
// Just grab a snapshot for testing, then exit.
console.log('Attempting to get demo snapshot...')
try {
intArg = process.argv.indexOf('--test')
var intLocation = intCamera = 0
if(process.argv.length > intArg + 1) {
// Attempt to get location,camera from next arg.
strLocCam = process.argv[intArg + 1]
intLocation = strLocCam.split(',')[0]
intCamera = strLocCam.split(',')[1]
}
getTestSnapshot(intLocation, intCamera)
} catch (e) {
console.log('Error attempting to call getTestSnapshot().')
console.log(e)
process.exit()
} finally {
//process.exit()
}
} else if(process.argv.includes('--list')) {
listLocationsAndCameras()
} else {
// Begin polling camera for events
startCameraPolling(true)
}