Repository: davglass/doorbot Branch: master Commit: 9d7dd5bef304 Files: 13 Total size: 47.2 KB Directory structure: gitextract_e8u_mklf/ ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── doorbot.js ├── examples/ │ ├── download-all.js │ ├── download.js │ └── links.js ├── package.json └── tests/ ├── .eslintrc.json └── index.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ artifacts build coverage ================================================ FILE: .eslintrc.json ================================================ { "root": true, "extends": "eslint:recommended", "parserOptions": { "ecmaVersion": 6, "sourceType": "script" }, "rules": { "no-unused-vars": ["error", { "args": "after-used" }], "semi": 2, "eqeqeq": [2, "always"], "no-console": 0, "no-irregular-whitespace": 2, "indent": ["error", 4], "space-before-function-paren": ["error", "never"], "brace-style": [2, "1tbs", { "allowSingleLine": true }], "arrow-body-style": [2, "always"], "array-bracket-spacing": [2, "never"], "object-curly-spacing": [2, "always"], "key-spacing": ["error", { "beforeColon": false }] }, "env": { "node": true } } ================================================ FILE: .gitignore ================================================ examples/downloads/ **/node_modules/ **/npm-debug.log node_modules/ coverage/ artifacts/ tmp/ CVS/ .DS_Store .*.swp .svn *~ .com.apple.timemachine.supported tests/out/ .nyc_output ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - '8' - '10' ================================================ FILE: LICENSE ================================================ Copyright 2016 Dav Glass Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Dav Glass nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ Ring.com Doorbell API [![Build Status](https://travis-ci.org/davglass/doorbot.svg?branch=master)](https://travis-ci.org/davglass/doorbot) ===================== I built this because of this [tweet](https://twitter.com/ring/status/816752533137977344). I have nothing to do with Ring.com, they just annoyed me with that tweet, so I figured out their api.. **doorbot 2.x has an API change** usage ----- `npm i doorbot --save` ```js const RingAPI = require('doorbot'); const ring = RingAPI({ email: 'your@email.com', password: '12345', retries: 10, //authentication retries, optional, defaults to 0 userAgent: 'My User Agent', //optional, defaults to @android:com.ringapp:2.0.67(423) api_version: 11, //optional in case you need to change it from the default of 9 timeout: (10 * 60 * 1000) //Defaults to 5 minutes }); ring.devices((e, devices) => { console.log(e, devices); ring.history((e, history) => { console.log(e, history); ring.recording(history[0].id, (e, recording) => { console.log(e, recording); const check = () => { console.log('Checking for ring activity..'); ring.dings((e, json) => { console.log(e, json); }); }; setInterval(check, 30 * 1000); check(); }); }); //floodlights are under the stickups_cams prop if (devices.hasOwnProperty('stickup_cams') && Array.isArray(devices.stickup_cams) && devices.stickup_cams.length > 0) { ring.lightToggle(devices.stickup_cams[0], (e) => { //Light state has been toggled }); } }); ``` api --- Get a list of your devices: `ring.devices(callback) => (error, array)` Device Health: `ring.health(device, callback) => (error, json)` Get your ring history: `ring.history(callback) => (error, array)` `ring.history(limit, callback) => (error, array)` - `limit` - The `Number` of items to return from the history. `ring.history(limit, older_than, callback) => (error, array)` - `limit` - The `Number` of items to return from the history. `older_than` - The ID of the latest history item to start with when going backward. Get a URL to a recording: `ring.recording(id, callback) => (error, url)` Get information for video on demand: `ring.vod(device, callback) => (error, json)` Turn on floodlights `ring.lightOn(device, callback) => (error)` Turn off floodlights `ring.lightOff(device, callback) => (error)` Toggle floodlights `ring.lightToggle(device, callback) => (error)` Set Chime Do Not Disturb `ring.set_chime_dnd(device, minutes, callback) => (error, json)` * on: `ring.set_chime_dnd(device, 15, callback) => (error, json)` * off: `ring.set_chime_dnd(device, 0, callback) => (error, json)` Get Chime Do Not Disturb `ring.get_chime_dnd(device, callback) => (error, json)` Set Doorbot Do Not Disturb (motion snooze) `ring.set_doorbot_dnd(device, minutes, callback) => (error, json)` * on: `ring.set_doorbot_dnd(device, 60, callback) => (error, json)` * off: `ring.set_doorbot_dnd(device, 0, callback) => (error, json)` *The Get API call for the doorbot DND returned a 404, not sure how to get the current time* debugging --------- I've added the `debug` module, so you can run this with `export DEBUG=doorbot` and it will print some helpful logs. ================================================ FILE: doorbot.js ================================================ const https = require('https'); const parse = require('url').parse; const format = require('url').format; const stringify = require('querystring').stringify; const crypto = require("crypto"); const logger = require('debug')('doorbot'); const API_VERSION = 9; const hardware_id = crypto.randomBytes(16).toString("hex"); const formatDates = (key, value) => { if (value && value.indexOf && value.indexOf('.000Z') > -1) { return new Date(value); } return value; }; /* * This converts the ID to a string, they are using large numbers for their ID's * and that breaks in JS since it can't math too well.. */ const scrub = (data) => { data = data.replace(/"id":(\d+),"created_at"/g, '"id":"$1","created_at"'); return data; }; const validate_number = (num) => { if (typeof num !== 'number') { throw new Error('Number argument required'); } }; const validate_device = (device) => { if (typeof device !== 'object' || !device) { throw new Error('Device needs to be an object'); } if (device && !device.id) { throw new Error('Device.id not found'); } }; const validate_callback = (callback) => { if (typeof callback !== 'function') { throw new Error('Callback not defined'); } }; class Doorbot { constructor(options) { options = options || {}; this.username = options.username || options.email; this.password = options.password; this.retries = options.retries || 0; this.timeout = options.timeout || (5 * 60 * 1000); this.counter = 0; this.userAgent = options.userAgent || 'android:com.ringapp:2.0.67(423)'; this.token = options.token || null; this.api_version = options.api_version || API_VERSION; if (!this.username) { throw(new Error('username is required')); } if (!this.password) { throw(new Error('password is required')); } this.authenticating = false; this.authQueue = []; } fetch(method, url, query, body, callback) { logger('fetch:', this.counter, method, url); var d = parse('https://api.ring.com/clients_api' + url, true); logger('query', query); delete d.path; delete d.href; delete d.search; /*istanbul ignore next*/ if (query) { Object.keys(query).forEach((key) => { d.query[key] = query[key]; }); } d = parse(format(d), true); logger('fetch-data', d); d.method = method; d.headers = d.headers || {}; if (body) { body = stringify(body); d.headers['content-type'] = 'application/x-www-form-urlencoded'; d.headers['content-length'] = body.length; } d.headers['user-agent'] = this.userAgent; logger('fetch-headers', d.headers); let timeoutP; const TIMEOUT = this.timeout; const req = https.request(d, (res) => { if (timeoutP) { return; } var data = ''; res.on('data', (d) => { data += d; }); /*istanbul ignore next*/ res.on('error', (e) => { callback(e); }); res.on('end', () => { req.setTimeout(0); logger('fetch-raw-data', data); var json, e = null; try { data = scrub(data); json = JSON.parse(data, formatDates); } catch (e) { json = data; } logger('fetch-json', json); if (json.error) { e = json; e.status = Number(e.status); json = {}; } if (res.statusCode >= 400) { e = new Error(`API returned Status Code ${res.statusCode}`); e.code = res.statusCode; } callback(e, json, res); }); }); req.on('error', callback); req.setTimeout(TIMEOUT, () => { timeoutP = true; callback(new Error('An API Timeout Occurred')); }); if (method === 'POST') { logger('fetch-post', body); req.write(body); } req.end(); } simpleRequest(url, method, data, callback) { if (typeof data === 'function') { callback = data; data = null; } /*istanbul ignore next*/ if (data && !data.api_version) { data.api_version = this.api_version; } this.authenticate((e) => { if (e && !this.retries) { return callback(e); } this.fetch(method, url, { api_version: this.api_version, auth_token: this.token }, data, (e, res, json) => { /*istanbul ignore else - It's only for logging..*/ if (json) { logger('code', json.statusCode); logger('headers', json.headers); } logger(e); if (e && e.code === 401 && this.counter < this.retries) { logger('auth failed, retrying', e); this.counter += 1; var self = this; setTimeout(() => { logger('auth failed, retry', { counter: self.counter }); self.token = null; self.authenticate(true, (e) => { /*istanbul ignore next*/ if (e) { return callback(e); } self.simpleRequest(url, method, callback); }); }, 500); return; } this.counter = 0; callback(e, res, json); }); }); } authenticate(retryP, callback) { if (typeof retryP === 'function') { callback = retryP; retryP = false; } if (!retryP) { if (this.token) { logger('auth skipped, we have a token'); return callback(); } if (this.authenticating) { logger('authenticate in progress, queuing callback'); this.authQueue.push(callback); return; } this.authenticating = true; } logger('authenticating with oAuth...'); const body = JSON.stringify({ client_id: "ring_official_android", grant_type: "password", username: this.username, password: this.password, scope: "client" }); const url = parse('https://oauth.ring.com/oauth/token'); url.method = 'POST'; url.headers = { 'content-type': 'application/json', 'content-length': body.length }; logger('fetching access_token from oAuth token endpoint'); const req = https.request(url, (res) => { logger('access_token statusCode', res.statusCode); logger('access_token headers', res.headers); let data = ''; res.on('data', d => {return data += d;}); res.on('end', () => { let e = null; let json = null; try { json = JSON.parse(data); } catch (je) { logger('JSON parse error', data); logger(je); e = new Error('JSON parse error from ring, check logging..'); } let token = null; if (json && json.access_token) { token = json.access_token; logger('authentication_token', token); } if (!token || e) { logger('access_token request failed, bailing..'); e = e || new Error('Api failed to return an authentication_token'); return callback(e); } const body = JSON.stringify({ device: { hardware_id: hardware_id, metadata: { api_version: this.api_version, }, os: "android" } }); logger('session json', body); const sessionURL = `https://api.ring.com/clients_api/session?api_version=${this.api_version}`; logger('sessionURL', sessionURL); const u = parse(sessionURL, true); u.method = 'POST'; u.headers = { Authorization: 'Bearer ' + token, 'content-type': 'application/json', 'content-length': body.length }; logger('fetching token with oAuth access_token'); const a = https.request(u, (res) => { logger('token fetch statusCode', res.statusCode); logger('token fetch headers', res.headers); let data = ''; let e = null; res.on('data', d => {return data += d;}); res.on('end', () => { let json = null; try { json = JSON.parse(data); } catch (je) { logger('JSON parse error', data); logger(je); e = 'JSON parse error from ring, check logging..'; } logger('token fetch response', json); const token = json && json.profile && json.profile.authentication_token; if (!token || e) { /*istanbul ignore next*/ const msg = e || json && json.error || 'Authentication failed'; return callback(new Error(msg)); } //Timeout after authentication to let the token take effect //performance issue.. var self = this; setTimeout(() => { self.token = token; self.authenticating = false; if (self.authQueue.length) { logger(`Clearing ${self.authQueue.length} callbacks from the queue`); self.authQueue.forEach(_cb => {return _cb(e, token);}); self.quthQueue = []; } callback(e, token); }, 1500); }); }); a.on('error', callback); a.write(body); a.end(); }); }); req.on('error', callback); req.write(body); req.end(); } devices(callback) { validate_callback(callback); this.simpleRequest('/ring_devices', 'GET', callback); } history(limit, older_than, callback) { if (typeof older_than === 'function') { callback = older_than; older_than = null; } if (typeof limit === 'function') { callback = limit; limit = 20; } validate_number(limit); validate_callback(callback); const url = `/doorbots/history?limit=${limit}` + ((older_than) ? `&older_than=${older_than}` : ''); this.simpleRequest(url, 'GET', callback); } dings(callback) { validate_callback(callback); this.simpleRequest('/dings/active', 'GET', callback); } lightOn(device, callback) { validate_device(device); validate_callback(callback); var url = `/doorbots/${device.id}/floodlight_light_on`; this.simpleRequest(url, 'PUT', callback); } lightOff(device, callback) { validate_device(device); validate_callback(callback); var url = `/doorbots/${device.id}/floodlight_light_off`; this.simpleRequest(url, 'PUT', callback); } lightToggle(device, callback) { validate_device(device); validate_callback(callback); var url = `/doorbots/${device.id}/floodlight_light_off`; if (device.hasOwnProperty('led_status') && device.led_status === 'off') { url = `/doorbots/${device.id}/floodlight_light_on`; } this.simpleRequest(url, 'PUT', callback); } vod(device, callback) { validate_device(device); validate_callback(callback); this.simpleRequest(`/doorbots/${device.id}/vod`, 'POST' , '', (e, json, res) => { if (e) return callback(e, res); this.dings((e, dings) => { if (e) return callback(e); for (let i = 0; i < dings.length; i++) { const ding = dings[i]; if ((ding.doorbot_id === device.id) && (ding.kind === 'on_demand')) return callback(null, ding); } return callback(new Error('VOD not available')); }); }); } recording(id, callback) { validate_callback(callback); this.simpleRequest(`/dings/${id}/recording`, 'GET', (e, json, res) => { callback(e, res && res.headers && res.headers.location, res); }); } set_chime_dnd(device, time, callback) { validate_device(device); validate_callback(callback); validate_number(time); var url = `/chimes/${device.id}/do_not_disturb`; this.simpleRequest(url, 'POST', { time: time }, callback); } get_chime_dnd(device, callback) { validate_device(device); validate_callback(callback); var url = `/chimes/${device.id}/do_not_disturb`; this.simpleRequest(url, 'GET', callback); } set_doorbot_dnd(device, time, callback) { validate_device(device); validate_callback(callback); validate_number(time); var url = `/doorbots/${device.id}/motion_snooze`; if (!time) { url = `/doorbots/${device.id}/motion_snooze/clear`; } this.simpleRequest(url, 'POST', { time: time }, callback); } health(device, callback) { validate_device(device); validate_callback(callback); this.simpleRequest(`/doorbots/${device.id}/health`, 'GET' , callback); } } module.exports = function(options) { return new Doorbot(options); }; ================================================ FILE: examples/download-all.js ================================================ #!/usr/bin/env node /* * * To use this: npm install async mkdirp request dateformat doorbot * * To run this: node download.js * To run this: node download.js * */ //Includes const dateFormat = require('dateformat'); const RingAPI = require('doorbot'); const async = require('async'); const mkdirp = require('mkdirp'); const fs = require('fs'); const path = require('path'); const url = require('url'); const request = require('request'); const ring = RingAPI({ email: 'EMAILADDRESS', password: 'PASSWORD' }); /* * Script Settings * * loopForOlder - If true, once the 100 max items are returned from the API, we get the next 100, repeating until the API returns no more items * * skipExistingFiles - If true, we don't download files that we already have a local copy of (based on Device ID, Video ID and CreatedAt date) - if false, we re-download and overwrite any existing files on disk. * */ var loopForOlder = true; var skipExistingFiles = true; //Parse 1st command line argument to take in the ID of what we want this to be older than, otherwise start with most recent var olderthan = process.argv[2]; //Variables for tracking what the oldest file in a run is, as well as the previous oldest-file we started at, to determine when we are no longer receiving additional older files anymore var oldestFile = parseInt('9999999999999999999'); //Expected max file ID var lastOldest = olderthan; const base = path.join(__dirname, 'downloads'); fs.mkdir(base, () => { //ignoring if it exists.. const doAgain = (goBack) => { //Implements the get-next-100-oldest feature if (goBack !== null) { olderthan = goBack; console.log('Getting more, older than: ' + olderthan); } //First value is HistoryLimit, max return is 100 so I hardcoded 1000 to make sure this number is bigger than what the API returns ring.history(1000, olderthan, (e, history) => { const fetch = (info, callback) => { ring.recording(info.id, (e, recording) => { //Calculate the filename we want this to be saved as const datea = dateFormat(info['created_at'],"yyyymmdd_HHMMssZ"); const partFilePath = url.parse(recording).pathname.substring(0,url.parse(recording).pathname.length - 4); const parts = partFilePath.split('/'); const filePath = '/' + parts[1] + '/' + datea + '_' + parts[2] + '.mp4'; const file = path.join(base, '.', filePath); //Is the file we just processed an older File ID than the previous file ID? if (parts[2] < oldestFile) { oldestFile = parts[2]; } //Make sure the directory exists const dirname = path.dirname(file); mkdirp(dirname, () => { //Tracking variable var writeFile = true; //Test if the file we are about to write already exists try { fs.accessSync(file); console.log('File Exists, Skipping: ', file); writeFile = false; } catch (err) { writeFile = true; } //If we aren't skipping existing files, we write them regardless of the write-file value if (skipExistingFiles && !writeFile) { return callback(); } console.log('Fetching file', file); const writer = fs.createWriteStream(file); writer.on('close', () => { console.log('Done writing', file); callback(); }); request(recording).pipe(writer); }); }); }; async.eachLimit(history, 10, fetch, () => { console.log('Done, Oldest File: ' + oldestFile); //If we started at the most recent video and don't have an existing oldest, or if we found a new, older Video ID, we start the look again from there - assuming loopForOlder is true if ((lastOldest === null || lastOldest !== oldestFile) && loopForOlder) { lastOldest = oldestFile; doAgain(lastOldest); //If we could a new oldest file, start again from there } }); }); }; doAgain(null); //Initially start it }); ================================================ FILE: examples/download.js ================================================ #!/usr/bin/env node /* * * To use this: npm install async mkdirp request doorbot * */ const RingAPI = require('doorbot'); const async = require('async'); const mkdirp = require('mkdirp'); const fs = require('fs'); const path = require('path'); const url = require('url'); const request = require('request'); /* * Configure your settings here: * email, password, historyLimit */ const ring = RingAPI({ email: 'your@email.com', password: '12345' }); const historyLimit = 1000; const base = path.join(__dirname, 'downloads'); fs.mkdir(base, () => { //ignoring if it exists.. ring.history(historyLimit, (e, history) => { const fetch = (info, callback) => { ring.recording(info.id, (e, recording) => { const file = path.join(base, '.', url.parse(recording).pathname); const dirname = path.dirname(file); mkdirp(dirname, () => { console.log('Fetching file', file); const writer = fs.createWriteStream(file); writer.on('close', () => { console.log('Done writing', file); callback(); }); request(recording).pipe(writer); }); }); }; async.eachLimit(history, 10, fetch, () => { console.log('done'); }); }); }); ================================================ FILE: examples/links.js ================================================ #!/usr/bin/env node /* * * To use this: npm install async doorbot * */ const RingAPI = require('doorbot'); const async = require('async'); const ring = RingAPI({ email: 'your@email.com', password: '12345' }); ring.history((e, history) => { const fetch = (info, callback) => { ring.recording(info.id, (e, recording) => { callback(null, recording); }); }; async.map(history, fetch, (e, data) => { console.log(data.join('\n')); }); }); ================================================ FILE: package.json ================================================ { "name": "doorbot", "version": "5.4.0", "description": "Ring.com Doorbell API", "main": "doorbot.js", "scripts": { "pretest": "eslint --fix .", "test": "jenkins-mocha ./tests/*.js", "posttest": "nyc report" }, "nyc": { "lines": 100, "statements": 100, "functions": 100, "branches": 90, "check-coverage": true, "reporter": [ "text", "text-summary" ] }, "repository": { "type": "git", "url": "git@github.com:davglass/doorbot.git" }, "keywords": [ "ring", "doorbell" ], "author": "Dav Glass ", "license": "BSD-3-Clause", "bugs": { "url": "https://github.com/davglass/doorbot/issues" }, "homepage": "https://github.com/davglass/doorbot", "devDependencies": { "eslint": "^3.12.2", "jenkins-mocha": "^6.0.0", "nock": "^9.6.1", "nyc": "^13.0.1" }, "dependencies": { "debug": "^2.6.8" } } ================================================ FILE: tests/.eslintrc.json ================================================ { "env": { "mocha": true } } ================================================ FILE: tests/index.js ================================================ const RingAPI = require('../doorbot.js'); const assert = require('assert'); const nock = require('nock'); nock.disableNetConnect(); describe('doorbot tests', () => { beforeEach(() => { nock.cleanAll(); }); it('should export stuff', () => { assert.ok(RingAPI); }); it('authenticate', (done) => { nock('https://oauth.ring.com').post('/oauth/token') .reply(200, { access_token: 'ACCESS_TOKEN' }); nock('https://api.ring.com').post('/clients_api/session?api_version=11') .reply(200, { profile: { authentication_token: 'TOKEN' } }); const ring = RingAPI({ email: 'test', password: 'test', api_version: 11 }); ring.authenticate((e, token) => { assert.equal(token, 'TOKEN'); done(); }); }); it('should use auth queue for parallel calls', function(done) { nock('https://oauth.ring.com').post('/oauth/token') .reply(200, { access_token: 'ACCESS_TOKEN' }); nock('https://api.ring.com').persist().post('/clients_api/session?api_version=9') .reply(200, { profile: { authentication_token: 'TOKEN' } }); const ring = RingAPI({ email: 'test', password: 'test' }); ring.authenticate(() => {}); assert.ok(ring.authenticating); ring.authenticate(() => {}); assert.equal(ring.authQueue.length, 1); ring.authenticate(() => {}); assert.equal(ring.authQueue.length, 2); done(); }); it('authenticate throw no username', () => { assert.throws(() => { RingAPI(); }, /username is required/); }); it('authenticate throw no password', () => { assert.throws(() => { RingAPI({ username: 'foo' }); }, /password is required/); }); it('authenticate failed - no access_token', (done) => { nock('https://oauth.ring.com').post('/oauth/token') .reply(401, { error: 'You must log in' }); const ring = RingAPI({ username: 'asdf', password: 'asdf' }); ring.devices((e, token) => { assert.equal(token, undefined); assert.equal(e.message, 'Api failed to return an authentication_token'); done(); }); }); it('authenticate failed - access_token bad json', (done) => { nock('https://oauth.ring.com').post('/oauth/token') .reply(500, ''); const ring = RingAPI({ username: 'asdf', password: 'asdf' }); ring.devices((e, token) => { assert.equal(token, undefined); assert.equal(e.message, 'JSON parse error from ring, check logging..'); done(); }); }); it('authenticate failed - token bad json', (done) => { nock('https://oauth.ring.com').post('/oauth/token') .reply(200, { access_token: 'ACCESS_TOKEN' }); nock('https://api.ring.com').post('/clients_api/session?api_version=9') .reply(500, ''); const ring = RingAPI({ username: 'asdf', password: 'asdf' }); ring.devices((e, token) => { assert.equal(token, undefined); assert.equal(e.message, 'JSON parse error from ring, check logging..'); done(); }); }); it('authenticate failed', (done) => { nock('https://oauth.ring.com').post('/oauth/token') .reply(200, { access_token: 'ACCESS_TOKEN' }); nock('https://api.ring.com').post('/clients_api/session?api_version=9') .reply(500, { error: 'Ring.com defined error message' }); const ring = RingAPI({ username: 'asdf', password: 'asdf' }); ring.devices((e, token) => { assert.equal(token, undefined); assert.equal(e.message, 'Ring.com defined error message'); done(); }); }); it('get devices', (done) => { nock('https://api.ring.com').get('/clients_api/ring_devices') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200, [ { device: 1, d: '2017-01-05T19:05:40.000Z' } ]); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; ring.devices((e, json) => { assert.ok(json); assert.ok(Array.isArray(json)); assert.equal(json[0].device, 1); assert.ok(json[0].d instanceof Date && isFinite(json[0].d)); done(); }); }); it('get devices error', (done) => { nock('https://api.ring.com').get('/clients_api/ring_devices') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200, { error: 'something happened' }); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; ring.devices((e) => { assert.equal(e.error, 'something happened'); done(); }); }); it('get history', (done) => { nock('https://api.ring.com').get('/clients_api/doorbots/history') .query({ auth_token: 'TOKEN', api_version: 9, limit: 20 }) .reply(200, [ { device: 1, d: '2017-01-05T19:05:40.000Z' } ]); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; ring.history((e, json) => { assert.ok(json); assert.ok(Array.isArray(json)); assert.equal(json[0].device, 1); assert.ok(json[0].d instanceof Date && isFinite(json[0].d)); done(); }); }); it('get history with limit', (done) => { nock('https://api.ring.com').get('/clients_api/doorbots/history') .query({ auth_token: 'TOKEN', api_version: 9, limit: 40 }) .reply(200, [ { device: 1, d: '2017-01-05T19:05:40.000Z' } ]); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; ring.history(40, (e, json) => { assert.ok(json); assert.ok(Array.isArray(json)); assert.equal(json[0].device, 1); assert.ok(json[0].d instanceof Date && isFinite(json[0].d)); done(); }); }); it('get history with limit and older_than', (done) => { nock('https://api.ring.com').get('/clients_api/doorbots/history') .query({ auth_token: 'TOKEN', api_version: 9, limit: 40, older_than: '12345678' }) .reply(200, [ { device: 1, d: '2017-01-05T19:05:40.000Z' } ]); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; ring.history(40, '12345678', (e, json) => { assert.ok(json); assert.ok(Array.isArray(json)); assert.equal(json[0].device, 1); assert.ok(json[0].d instanceof Date && isFinite(json[0].d)); done(); }); }); it('get dings', (done) => { nock('https://api.ring.com').get('/clients_api/dings/active') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200, [ { device: 1, d: '2017-01-05T19:05:40.000Z' } ]); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; ring.dings((e, json) => { assert.ok(json); assert.ok(Array.isArray(json)); assert.equal(json[0].device, 1); assert.ok(json[0].d instanceof Date && isFinite(json[0].d)); done(); }); }); it('start vod', (done) => { nock('https://api.ring.com').post('/clients_api/doorbots/1/vod') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200, { }); nock('https://api.ring.com').get('/clients_api/dings/active') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200, [ { doorbot_id: 2, kind: 'on_demand' }, { doorbot_id: 1, kind: 'on_demand' } ]); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; ring.vod({ id: 1 }, (e, json) => { assert.ok(json); assert.equal(json.doorbot_id, 1); assert.equal(json.kind, 'on_demand'); done(); }); }); it('start vod with error', (done) => { nock('https://api.ring.com').post('/clients_api/doorbots/1/vod') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200, { }); nock('https://api.ring.com').get('/clients_api/dings/active') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200, []); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; ring.vod({ id: 1 }, (e) => { assert.ok(e); done(); }); }); it('start vod with vod error', (done) => { nock('https://api.ring.com').post('/clients_api/doorbots/1/vod') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(500, { error: new Error('Borked') }); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; ring.vod({ id: 1 }, (e) => { assert.ok(e); done(); }); }); it('start vod with ding error', (done) => { nock('https://api.ring.com').post('/clients_api/doorbots/1/vod') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200, { }); nock('https://api.ring.com').get('/clients_api/dings/active') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(500, { error: new Error('Borked') }); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; ring.vod({ id: 1 }, (e) => { assert.ok(e); done(); }); }); it('get recordings', (done) => { const URL = 'http://some.amazon.com/url/to/movie.mp4'; nock('https://api.ring.com').get('/clients_api/dings/1/recording') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200, '', { location: URL }); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; ring.recording(1, (e, url) => { assert.equal(url, URL); done(); }); }); it('turn on floodlight', (done) => { nock('https://api.ring.com').put('/clients_api/doorbots/12345/floodlight_light_on') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; const device = { id: '12345' }; ring.lightOn(device, (e) => { assert.equal(e, null); done(); }); }); it('turn off floodlight', (done) => { nock('https://api.ring.com').put('/clients_api/doorbots/12345/floodlight_light_off') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; const device = { id: '12345' }; ring.lightOff(device, (e) => { assert.equal(e, null); done(); }); }); it('toggle floodlight off -> on', (done) => { nock('https://api.ring.com').put('/clients_api/doorbots/12345/floodlight_light_on') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; const device = { id: '12345', led_status: 'off' }; ring.lightToggle(device, (e) => { assert.equal(e, null); done(); }); }); it('toggle floodlight on -> off', (done) => { nock('https://api.ring.com').put('/clients_api/doorbots/12345/floodlight_light_off') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; const device = { id: '12345', led_status: 'on' }; ring.lightToggle(device, (e) => { assert.equal(e, null); done(); }); }); it('Retry on error..', function(done) { this.timeout(100000); nock('https://oauth.ring.com').persist().post('/oauth/token') .reply(200, { access_token: 'ACCESS_TOKEN' }); nock('https://api.ring.com').persist().post('/clients_api/session?api_version=9') .reply(200, { profile: { authentication_token: 'TOKEN' } }); nock('https://api.ring.com').persist().get('/clients_api/ring_devices') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(401, { error: 'Denied!!' }); const ring = RingAPI({ username: 'test', password: 'test', retries: 2 }); ring.token = 'TOKEN'; ring.devices((e) => { assert.equal(e.message, 'API returned Status Code 401'); done(); }); }); it('set chime do not disturb', (done) => { nock('https://api.ring.com').post('/clients_api/chimes/12345/do_not_disturb') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; const device = { id: '12345' }; ring.set_chime_dnd(device, 180, (e) => { assert.equal(e, null); done(); }); }); it('set chime do not disturb to off', (done) => { nock('https://api.ring.com').post('/clients_api/chimes/12345/do_not_disturb') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; const device = { id: '12345' }; ring.set_chime_dnd(device, 0, (e) => { assert.equal(e, null); done(); }); }); it('get chime do not disturb', (done) => { nock('https://api.ring.com').get('/clients_api/chimes/12345/do_not_disturb') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; const device = { id: '12345' }; ring.get_chime_dnd(device, (e) => { assert.equal(e, null); done(); }); }); it('set doorbot do not disturb', (done) => { nock('https://api.ring.com').post('/clients_api/doorbots/12345/motion_snooze') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; const device = { id: '12345' }; ring.set_doorbot_dnd(device, 180, (e) => { assert.equal(e, null); done(); }); }); it('set doorbot do not disturb to off', (done) => { nock('https://api.ring.com').post('/clients_api/doorbots/12345/motion_snooze/clear') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; const device = { id: '12345' }; ring.set_doorbot_dnd(device, 0, (e) => { assert.equal(e, null); done(); }); }); it('call the health check api', (done) => { nock('https://api.ring.com').get('/clients_api/doorbots/12345/health') .query({ auth_token: 'TOKEN', api_version: 9 }) .reply(200, { device_health: {} }); const ring = RingAPI({ username: 'test', password: 'test' }); ring.token = 'TOKEN'; const device = { id: '12345' }; ring.health(device, (e, json) => { assert.equal(e, null); assert.ok(json.device_health); done(); }); }); it('should error on a timeout', (done) => { nock('https://api.ring.com').get('/clients_api/doorbots/12345/health') .query({ auth_token: 'TOKEN', api_version: 9 }) .delay(2000) .reply(200, { device_health: {} }); const ring = RingAPI({ username: 'test', password: 'test', timeout: 100 }); ring.token = 'TOKEN'; const device = { id: '12345' }; ring.health(device, (e) => { assert.ok(e); assert.equal(e.message, 'An API Timeout Occurred'); done(); }); }); it('should error on no device object', () => { assert.throws(() => { const ring = RingAPI({ username: 'test', password: 'test' }); ring.set_doorbot_dnd(null); }, /Device needs to be an object/); }); it('should error on bad device', () => { assert.throws(() => { const ring = RingAPI({ username: 'test', password: 'test' }); ring.set_doorbot_dnd({}); }, /Device.id not found/); }); it('should error on no callback', () => { assert.throws(() => { const ring = RingAPI({ username: 'test', password: 'test' }); ring.set_doorbot_dnd({ id: 1234 }); }, /Callback not defined/); }); it('should error on no number argument', () => { assert.throws(() => { const ring = RingAPI({ username: 'test', password: 'test' }); /*istanbul ignore next*/ ring.set_doorbot_dnd({ id: 1234 }, null, () => {}); }, /Number argument required/); }); });