node-arlo
Version:
JS library for Arlo Cameras
441 lines (377 loc) • 12.4 kB
JavaScript
"use strict";
const EventEmitter = require("events").EventEmitter;
const Extend = require("util")._extend;
const Request = require("request");
const debug = require("debug")("Node-Arlo");
const debugComm = require("debug")("Arlo:REST");
const Constants = require("./ArloConstants");
const ArloBaseStation = require("./ArloBaseStation");
const ArloCamera = require("./ArloCamera");
const ArloQ = require("./ArloQ");
const HTTP_GET = "GET",
HTTP_POST = "POST";
class Arlo extends EventEmitter {
constructor() {
super();
this.devices = {};
this.headers = {
"User-Agent":
"Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_2 like Mac OS X) AppleWebKit/604.3.5 (KHTML, like Gecko) Mobile/15B202 NETGEAR/v1 (iOS Vuezone)",
};
this.pendingSnapshots = {};
}
getDevices(callback) {
this._get(
Constants.WEB.DEVICES,
{},
function (error, response, body) {
if (!body || body.success !== true) {
return;
}
for (let i = 0; i < body.data.length; i++) {
let device = body.data[i];
if (device.deviceType === Constants.TYPE_BASESTATION) {
this.devices[device.deviceId] = new ArloBaseStation(device, this);
this.devices[device.deviceId].subscribe();
this.emit(Constants.EVENT_FOUND, this.devices[device.deviceId]);
}
}
for (let i = 0; i < body.data.length; i++) {
let device = body.data[i];
if (device.deviceType === Constants.TYPE_CAMERA) {
this.devices[device.deviceId] = new ArloCamera(device, this);
this.emit(Constants.EVENT_FOUND, this.devices[device.deviceId]);
} else if (
device.deviceType === Constants.TYPE_ARLOQS ||
device.deviceType === Constants.TYPE_ARLOQ
) {
this.devices[device.deviceId] = new ArloQ(device, this);
this.devices[device.deviceId].subscribe();
this.emit(Constants.EVENT_FOUND, this.devices[device.deviceId]);
}
}
this.emit(Constants.EVENT_GOT_DEVICES, this.devices);
if (typeof callback == "function") {
callback();
callback = undefined;
}
}.bind(this)
);
}
login(username, password, callback) {
this._post(
Constants.WEB.LOGIN,
{ email: username, password: password },
{},
function (error, response, body) {
this.token = body.data.token;
this.headers = Extend({ Authorization: this.token }, this.headers);
this.userId = body.data.userId;
this.emit(Constants.EVENT_LOGGED_IN, body.data.serialNumber);
this.subscribe(
function () {
this.getDevices();
}.bind(this)
);
if (typeof callback == "function") {
callback();
callback = undefined;
}
}.bind(this)
);
}
keepAlive = true;
async loginAndKeepAlive(user, password, callback, heartbeat = 30000) {
let msLastResponse = 0;
this.addListener(
Constants.EVENT_MESSAGE,
() => (msLastResponse = new Date())
);
while (this.keepAlive) {
if (new Date() - msLastResponse > heartbeat)
this.login(user, password, callback);
else if (this.token) this.getDevices();
await new Promise((resolve) => setTimeout(resolve, heartbeat));
}
}
subscribe(callback) {
let reCamera = /cameras\/(.+)$/;
let reSubscription = /subscriptions\/(.+)$/;
Request.get({
url: Constants.WEB.SUBSCRIBE + "?token=" + this.token,
method: HTTP_GET,
json: false,
jar: true,
headers: Extend({ Accept: "text/event-stream" }, this.headers),
})
.on(
"data",
function (data) {
let str, msg;
if (typeof callback == "function") {
callback();
callback = undefined;
}
try {
str =
"{" +
data
.toString()
.replace(
/^event: message\s*data/,
'"event": "message", "data"'
) +
"}";
msg = JSON.parse(str);
//debug(msg);
} catch (e) {
debug(str);
return;
}
data = msg.data;
this.emit(Constants.EVENT_MESSAGE, data);
switch (data.resource) {
case Constants.RESOURCE_CAMERAS:
for (let i = 0; i < msg.data.properties.length; i++) {
let info = msg.data.properties[i];
let camera = this.devices[info.serialNumber];
if (camera === undefined) {
continue;
}
camera.emit(Constants.EVENT_UPDATE, info);
}
break;
case Constants.RESOURCE_MODES:
let baseStation = this.devices[msg.data.from];
if (baseStation) {
baseStation.emit(msg.data.properties.activeMode);
}
break;
default:
if (reSubscription.test(msg.data.resource)) {
let device = this.devices[msg.data.from];
if (device !== undefined) {
device.isSubscribed = true;
}
} else if (reCamera.test(msg.data.resource)) {
let deviceId;
[, deviceId] = msg.data.resource.match(reCamera);
let camera = this.devices[deviceId];
if (!camera || msg.data.properties === undefined) {
return;
}
switch (data.action) {
case Constants.FF_SNAPSHOT_AVAILABLE:
camera.emit(
Constants.FF_SNAPSHOT_AVAILABLE,
data.properties.presignedFullFrameSnapshotUrl
);
break;
case "is":
if (
data.properties.activityState === Constants.FF_SNAPSHOT
) {
let callback = this.pendingSnapshots[data.transId];
delete this.pendingSnapshots[data.transId];
if (typeof callback == "function") {
callback(data.error, data);
}
return;
}
if (data.properties[Constants.EVENT_MOTION] !== undefined) {
camera.emit(
Constants.EVENT_MOTION,
data.properties[Constants.EVENT_MOTION]
);
}
if (data.properties[Constants.EVENT_AUDIO] !== undefined) {
camera.emit(
Constants.EVENT_AUDIO,
data.properties[Constants.EVENT_AUDIO]
);
}
if (
data.properties[Constants.EVENT_BATTERY] !== undefined
) {
camera.emit(
Constants.EVENT_BATTERY,
data.properties[Constants.EVENT_BATTERY]
);
}
if (
data.properties[Constants.EVENT_CHARGING] !== undefined
) {
camera.emit(
Constants.EVENT_CHARGING,
data.properties[Constants.EVENT_CHARGING]
);
}
camera.emit(Constants.EVENT_UPDATE, data.properties);
}
}
}
}.bind(this)
)
.on("error", (err) => {
debug(err);
});
}
downloadSnapshot(url, callback) {
var bufs = [];
Request.get(url)
.on("data", function (data) {
bufs.push(data);
})
.on("end", function () {
if (typeof callback == "function") {
callback(Buffer.concat(bufs));
}
});
}
getSnapshot(device, callback, label) {
if (!label) {
label = "node-arlo";
}
let parent = this.devices[device.parentId];
let transId = label + "-" + device.deviceId + "!snapshot-" + Date.now();
let body = {
[Constants.FROM]: this.userId + "_web",
[Constants.TO]: parent.id,
[Constants.ACTION]: Constants.ACTION_SET,
[Constants.RESOURCE]: Constants.RESOURCE_CAMERAS + "/" + device.deviceId,
[Constants.PUBLISH]: true,
[Constants.TRANS_ID]: transId,
[Constants.PROPERTIES]: {
[Constants.ACTIVITY_STATE]: Constants.FF_SNAPSHOT,
},
};
this._post(
Constants.WEB.SNAPSHOT,
body,
{ [Constants.XCLOUD_ID]: parent.cloudId },
function (error, response, data) {
if (data && data.success === true) {
this.pendingSnapshots[transId] = callback;
} else if (typeof callback == "function") {
callback(null);
}
}.bind(this)
);
}
getStream(device, callback, label) {
if (!label) {
label = "node-arlo";
}
debug("Device: %O", device);
let deviceId = device.deviceId;
let parentId = device.parentId;
let cloudId = device.xCloudId;
let transId = label + "-" + deviceId + "!stream-" + Date.now();
let body = {
[Constants.FROM]: this.userId + "_web",
[Constants.TO]: parentId,
[Constants.ACTION]: Constants.ACTION_SET,
[Constants.RESOURCE]: Constants.RESOURCE_CAMERAS + "/" + deviceId,
[Constants.PUBLISH]: true,
[Constants.TRANS_ID]: transId,
[Constants.PROPERTIES]: {
[Constants.ACTIVITY_STATE]: "startUserStream",
[Constants.CAMERA_ID]: deviceId,
},
};
debug("Getting stream");
this._post(
Constants.WEB.STREAM,
body,
{ [Constants.XCLOUD_ID]: cloudId },
function (error, response, body) {
if (error || body.data.url === null || body.data.url === undefined) {
debug("Error getting stream: %O", error);
return;
}
if (typeof callback == "function") {
let url = body.data.url.replace("rtsp://", "rtsps://");
debug("Got stream URL: %s", url);
callback(url);
callback = undefined;
}
}
);
}
notify(device, body, callback) {
try {
if (typeof device === "string") {
device = this.devices[device];
}
body[Constants.FROM] = this.userId + "_web";
body[Constants.TO] = device.id;
this._post(
Constants.WEB.NOTIFY + device.id,
body,
{ [Constants.XCLOUD_ID]: device.cloudId },
callback
);
} catch (e) {
console.log(e);
console.log(device);
}
}
_get(url, headers, callback) {
Request(
{
url: url,
method: HTTP_GET,
json: true,
jar: true,
headers: Extend(headers || {}, this.headers),
},
function (error, response, body) {
debugComm(body);
if (typeof callback == "function") {
callback(error, response, body);
}
}
);
}
_post(url, body, headers, callback) {
debugComm({
url: url,
method: HTTP_POST,
body: body,
json: true,
jar: true,
headers: Extend(headers || {}, this.headers),
});
Request(
{
url: url,
method: HTTP_POST,
body: body,
json: true,
jar: true,
headers: Extend(headers || {}, this.headers),
},
function (error, response, body) {
debugComm(body);
if (typeof callback == "function") {
callback(error, response, body);
}
}
);
}
}
Arlo.ARMED = Constants.MODE_ARMED;
Arlo.DISARMED = Constants.MODE_DISARMED;
Arlo.BASESTATION = Constants.TYPE_BASESTATION;
Arlo.CAMERA = Constants.TYPE_CAMERA;
Arlo.Q = Constants.TYPE_ARLOQS;
Arlo.SIREN = Constants.TYPE_SIREN;
Arlo.AUDIO = Constants.EVENT_AUDIO;
Arlo.BATTERY = Constants.EVENT_BATTERY;
Arlo.CHARGING = Constants.EVENT_CHARGING;
Arlo.FOUND = Constants.EVENT_FOUND;
Arlo.MOTION = Constants.EVENT_MOTION;
Arlo.UPDATE = Constants.EVENT_UPDATE;
Arlo.MESSAGE = Constants.EVENT_MESSAGE;
Arlo.FF_SNAPSHOT = Constants.FF_SNAPSHOT_AVAILABLE;
module.exports = Arlo;