@koush/ring-client-api
Version:
Unofficial API for Ring doorbells, cameras, security alarm system and smart lighting
177 lines (176 loc) • 8.88 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RingApi = void 0;
const rest_client_1 = require("./rest-client");
const location_1 = require("./location");
const ring_camera_1 = require("./ring-camera");
const ring_chime_1 = require("./ring-chime");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const util_1 = require("./util");
const ffmpeg_1 = require("./ffmpeg");
const subscribed_1 = require("./subscribed");
class RingApi extends subscribed_1.Subscribed {
constructor(options) {
super();
this.options = options;
this.restClient = new rest_client_1.RingRestClient(this.options);
this.onRefreshTokenUpdated =
this.restClient.onRefreshTokenUpdated.asObservable();
if (options.debug) {
(0, util_1.enableDebug)();
}
const { locationIds, ffmpegPath } = options;
if (locationIds && !locationIds.length) {
(0, util_1.logError)('Your Ring config has `"locationIds": []`, which means no locations will be used and no devices will be found.');
}
if (ffmpegPath) {
(0, ffmpeg_1.setFfmpegPath)(ffmpegPath);
}
}
fetchRingDevices() {
return __awaiter(this, void 0, void 0, function* () {
const { doorbots, chimes, authorized_doorbots: authorizedDoorbots, stickup_cams: stickupCams, base_stations: baseStations, beams_bridges: beamBridges, } = yield this.restClient.request({ url: (0, rest_client_1.clientApi)('ring_devices') });
return {
doorbots,
chimes,
authorizedDoorbots,
stickupCams,
allCameras: doorbots.concat(stickupCams, authorizedDoorbots),
baseStations,
beamBridges,
};
});
}
fetchActiveDings() {
return this.restClient.request({
url: (0, rest_client_1.clientApi)('dings/active'),
});
}
listenForDeviceUpdates(cameras, chimes) {
const { cameraStatusPollingSeconds, cameraDingsPollingSeconds } = this.options, onCamerasRequestUpdate = (0, rxjs_1.merge)(...cameras.map((camera) => camera.onRequestUpdate)), onChimesRequestUpdate = (0, rxjs_1.merge)(...chimes.map((chime) => chime.onRequestUpdate)), onCamerasRequestActiveDings = (0, rxjs_1.merge)(...cameras.map((camera) => camera.onRequestActiveDings)), onUpdateReceived = new rxjs_1.Subject(), onActiveDingsReceived = new rxjs_1.Subject(), onPollForStatusUpdate = cameraStatusPollingSeconds
? onUpdateReceived.pipe((0, operators_1.debounceTime)(cameraStatusPollingSeconds * 1000))
: rxjs_1.EMPTY, onPollForActiveDings = cameraDingsPollingSeconds
? onActiveDingsReceived.pipe((0, operators_1.debounceTime)(cameraDingsPollingSeconds * 1000))
: rxjs_1.EMPTY, camerasById = cameras.reduce((byId, camera) => {
byId[camera.id] = camera;
return byId;
}, {}), chimesById = chimes.reduce((byId, chime) => {
byId[chime.id] = chime;
return byId;
}, {});
if (!cameras.length && !chimes.length) {
return;
}
this.addSubscriptions((0, rxjs_1.merge)(onCamerasRequestUpdate, onChimesRequestUpdate, onPollForStatusUpdate)
.pipe((0, operators_1.throttleTime)(500), (0, operators_1.switchMap)(() => this.fetchRingDevices().catch(() => null)))
.subscribe((response) => {
onUpdateReceived.next(null);
if (!response) {
return;
}
response.allCameras.forEach((data) => {
const camera = camerasById[data.id];
if (camera) {
camera.updateData(data);
}
});
response.chimes.forEach((data) => {
const chime = chimesById[data.id];
if (chime) {
chime.updateData(data);
}
});
}));
if (cameraStatusPollingSeconds) {
onUpdateReceived.next(null); // kick off polling
}
this.addSubscriptions((0, rxjs_1.merge)(onCamerasRequestActiveDings, onPollForActiveDings).subscribe(() => __awaiter(this, void 0, void 0, function* () {
const activeDings = yield this.fetchActiveDings().catch(() => null);
onActiveDingsReceived.next(null);
if (!activeDings || !activeDings.length) {
return;
}
activeDings.forEach((activeDing) => {
const camera = camerasById[activeDing.doorbot_id];
if (camera) {
camera.processActiveDing(activeDing);
}
});
})));
if (cameras.length && cameraDingsPollingSeconds) {
onActiveDingsReceived.next(null); // kick off polling
}
}
fetchRawLocations() {
return __awaiter(this, void 0, void 0, function* () {
const { user_locations: rawLocations } = yield this.restClient.request({ url: (0, rest_client_1.deviceApi)('locations') });
if (!rawLocations) {
throw new Error('The Ring account which you used to generate a refresh token does not have any associated locations. Please use an account that has access to at least one location.');
}
return rawLocations;
});
}
fetchAmazonKeyLocks() {
return this.restClient.request({
url: 'https://api.ring.com/integrations/amazonkey/v2/devices/lock_associations',
});
}
fetchAndBuildLocations() {
return __awaiter(this, void 0, void 0, function* () {
const rawLocations = yield this.fetchRawLocations(), { authorizedDoorbots, chimes, doorbots, allCameras, baseStations, beamBridges, } = yield this.fetchRingDevices(), locationIdsWithHubs = [...baseStations, ...beamBridges].map((x) => x.location_id), cameras = allCameras.map((data) => new ring_camera_1.RingCamera(data, doorbots.includes(data) ||
authorizedDoorbots.includes(data) ||
data.kind.startsWith('doorbell'), this.restClient, this.options.avoidSnapshotBatteryDrain || false, this.options.treatKnockAsDing || false)), ringChimes = chimes.map((data) => new ring_chime_1.RingChime(data, this.restClient)), locations = rawLocations
.filter((location) => {
return (!Array.isArray(this.options.locationIds) ||
this.options.locationIds.includes(location.location_id));
})
.map((location) => new location_1.Location(location, cameras.filter((x) => x.data.location_id === location.location_id), ringChimes.filter((x) => x.data.location_id === location.location_id), {
hasHubs: locationIdsWithHubs.includes(location.location_id),
hasAlarmBaseStation: baseStations.some((station) => station.location_id === location.location_id),
locationModePollingSeconds: this.options.locationModePollingSeconds,
}, this.restClient));
this.listenForDeviceUpdates(cameras, ringChimes);
return locations;
});
}
getLocations() {
if (!this.locationsPromise) {
this.locationsPromise = this.fetchAndBuildLocations();
}
return this.locationsPromise;
}
getCameras() {
return __awaiter(this, void 0, void 0, function* () {
const locations = yield this.getLocations();
return locations.reduce((cameras, location) => [...cameras, ...location.cameras], []);
});
}
getProfile() {
return this.restClient.request({
url: (0, rest_client_1.clientApi)('profile'),
});
}
disconnect() {
this.unsubscribe();
if (!this.locationsPromise) {
return;
}
this.getLocations()
.then((locations) => locations.forEach((location) => location.disconnect()))
.catch((e) => {
(0, util_1.logError)(e);
});
this.restClient.clearTimeouts();
}
}
exports.RingApi = RingApi;