Skip to content

Commit

Permalink
address new timestamp for thumbnails; reduce concurrency logging; add…
Browse files Browse the repository at this point in the history
… noop for lowbattery or offline batteries
  • Loading branch information
colinbendell committed Mar 29, 2022
1 parent 1c21ffa commit ddec3e1
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 82 deletions.
6 changes: 3 additions & 3 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"type": "boolean"
},
"disable-thumbnail-refresh": {
"title": "Disable thumbnail refresh",
"title": "Disable thumbnail auto-refresh",
"default": false,
"description": "By default, the thumbnails of the cameras will refresh when the camera is enabled, disabling the auto refresh keeps the default thumbnail untouched.",
"type": "boolean"
Expand All @@ -67,8 +67,8 @@
"title": "Camera Thumbnail Refresh (seconds)",
"type": "integer",
"minimum": 0,
"placeholder": "60",
"description": "Force Thumbnail refresh every N seconds. Disabling thumbnail refresh setting takes priority over the timeout."
"placeholder": "3600",
"description": "Force Thumbnail refresh every N seconds. If auto refresh is disabled, this value is ignored."
},
"camera-status-polling-seconds": {
"title": "Status Polling (seconds)",
Expand Down
8 changes: 8 additions & 0 deletions src/blink-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,14 @@ class BlinkAPI {
await sleep(500);
return this._request(method, path, body, maxTTL, false, httpErrorAsError);
}
else if (res.status === 409) {
if (httpErrorAsError) {
if (!/busy/.test(res?._body?.message)) {
const status = res.headers.get('status') || res.status + ' ' + res.statusText;
throw new Error(`${method} ${targetPath} (${status})`);
}
}
}
else if (res.status >= 400) {
const status = res.headers.get('status') || res.status + ' ' + res.statusText;
log.error(`${method} ${targetPath} (${status})`);
Expand Down
42 changes: 32 additions & 10 deletions src/blink.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const fs = require('fs');
const {stringify} = require('./stringify');
// const stringify = JSON.stringify;

const THUMBNAIL_TTL = 1 * 60; // 1min
const THUMBNAIL_TTL = 60 * 60; // 1min
const BATTERY_TTL = 60 * 60; // 60min
const MOTION_POLL = 15;
const STATUS_POLL = 30;
Expand Down Expand Up @@ -96,7 +96,11 @@ class BlinkNetwork extends BlinkDevice {
}

get status() {
return this.syncModule?.status;
return this.data.status ?? this.syncModule?.status;
}

get online() {
return ['online'].includes(this.status);
}

get armed() {
Expand Down Expand Up @@ -153,6 +157,10 @@ class BlinkCamera extends BlinkDevice {
return this.data.status && this.data.status !== 'done' ? this.data.status : this.network.status;
}

get online() {
return ['online', 'done'].includes(this.data.status) && (this.isCameraMini || this.network.online);
}

get armed() {
return this.network.armed;
}
Expand All @@ -178,11 +186,17 @@ class BlinkCamera extends BlinkDevice {
}

get thumbnailCreatedAt() {
// we store it on the .data object to it will be auto scrubbed on the next data poll
if (this.data.thumbnail_created_at) return this.data.thumbnail_created_at;

const dateRegex = /(\d{4})_(\d\d)_(\d\d)__(\d\d)_(\d\d)(am|pm)?$/i;
const [, year, month, day, hour, minute] = dateRegex.exec(this.thumbnail) || [];
this.thumbnailCreatedAt = Date.parse(`${year}-${month}-${day} ${hour}:${minute} +000`) || Date.now();
const dateRegex = /(\d{4})_(\d\d)_(\d\d)__(\d\d)_(\d\d)(?:am|pm)?$|[?&]ts=(\d+)(?:&|$)/i;
const [, year, month, day, hour, minute, epoch] = dateRegex.exec(this.thumbnail) || [];
if (epoch) {
this.thumbnailCreatedAt = Date.parse(new Date(Number(epoch.padEnd(13, '0'))).toISOString());
}
else {
this.thumbnailCreatedAt = Date.parse(`${year}-${month}-${day} ${hour}:${minute} +000`) || Date.now();
}
return this.data.thumbnail_created_at;
}

Expand Down Expand Up @@ -288,7 +302,8 @@ class BlinkCamera extends BlinkDevice {

if (this.cacheThumbnail.has(thumbnailUrl)) return this.cacheThumbnail.get(thumbnailUrl);

const data = await this.blink.getUrl(thumbnailUrl + '.jpg');
// legacy thumbnails need a suffix of .jpg appended to the url
const data = await this.blink.getUrl(thumbnailUrl.replace(/\.jpg|$/, '.jpg'));
this.cacheThumbnail.clear(); // avoid memory from getting large
this.cacheThumbnail.set(thumbnailUrl, data);
return data;
Expand Down Expand Up @@ -371,14 +386,14 @@ class Blink {
const start = Date.now();

// if there is an error, we are going to retry for 15s and fail
let cmd = await Promise.resolve(fn()).catch(e => log.error(e)) || {message: 'busy'};
let cmd = await Promise.resolve(fn()).catch(() => undefined) || {message: 'busy'};
while (cmd.message && /busy/i.test(cmd.message)) {
// TODO: should this be an error?

log.info(`Sleeping ${busyWait}s: ${cmd.message}`);
await sleep(busyWait * 1000);
if (Date.now() - start > timeout * 1000) return;
cmd = await Promise.resolve(fn()).catch(e => log.error(e)) || {message: 'busy'};
cmd = await Promise.resolve(fn()).catch(() => undefined) || {message: 'busy'};
}
return await this._commandWaitAll(networkID, cmd, timeout);
}
Expand Down Expand Up @@ -660,9 +675,16 @@ class Blink {
const eligible = force || (camera.armed && camera.enabled);

if (eligible && Date.now() >= lastSnapshot) {
if (camera.lowBattery || !camera.online) {
log(`${camera.name} - ${!camera.online ? 'Offline' : 'Low Battery'}; Skipping snapshot`);
return false;
}

// set the thumbnail to the future to avoid pile-ons
camera.thumbnailCreatedAt = Date.now();
const networkID = camera.networkID;
const cameraID = camera.cameraID;
log(`Refreshing snapshot for ${camera.name}`);
log(`${camera.name} - Refreshing snapshot`);
let updateCamera = this.blinkAPI.updateCameraThumbnail;
if (camera.isCameraMini) updateCamera = this.blinkAPI.updateOwlThumbnail;

Expand Down Expand Up @@ -696,7 +718,7 @@ class Blink {
const lastMedia = await this.getCameraLastMotion(camera.networkID, camera.cameraID);
const lastSnapshot = Date.parse(lastMedia.created_at) + (this.snapshotRate * 1000);
if (force || (camera.armed && camera.enabled && Date.now() >= lastSnapshot)) {
log(`Refreshing clip for ${camera.name}`);
log(`${camera.name} - Refreshing clip`);
const cmd = async () => await this.blinkAPI.updateCameraClip(camera.networkID, camera.cameraID);
await this._command(camera.networkID, cmd);

Expand Down
128 changes: 70 additions & 58 deletions src/blink.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,47 +63,6 @@ describe('Blink', () => {
await blink.logout();
});

test.concurrent('refreshData()', async () => {
const blink = new Blink(DEFAULT_BLINK_CLIENT_UUID);
blink.blinkAPI.getAccountHomescreen.mockResolvedValue(SAMPLE.HOMESCREEN);
await blink.refreshData();
expect(blink.blinkAPI.getAccountHomescreen).toHaveBeenCalled();
expect(blink.networks.size).toBe(3);
expect(blink.cameras.size).toBe(3);
});
test.concurrent('_commandWait()', async () => {
const blink = new Blink(DEFAULT_BLINK_CLIENT_UUID);
blink.blinkAPI.getAccountHomescreen.mockResolvedValue(JSON.parse(JSON.stringify(SAMPLE.HOMESCREEN)));
await blink.refreshData();

blink.blinkAPI.getCommand.mockResolvedValue(SAMPLE.COMMAND_RUNNING);
blink.blinkAPI.deleteCommand.mockResolvedValue({});
const {id: commandID, network_id: networkID} = SAMPLE.COMMAND_RUNNING.commands[0];
await blink._commandWait(networkID, commandID, 0.0001);

expect(blink.blinkAPI.getCommand).toBeCalledTimes(2);
expect(blink.blinkAPI.deleteCommand).toBeCalledTimes(1);

expect(await blink._commandWait(networkID)).toBeUndefined();
expect(await blink._commandWait(null, commandID)).toBeUndefined();
});
test.concurrent('_lock()', async () => {
const blink = new Blink(DEFAULT_BLINK_CLIENT_UUID);

let calls = 0;
const cmd = async () => {
await sleep(1);
calls++;
};
blink._lock('test', cmd);
await blink._lock('test', cmd);
await sleep(11);

expect(calls).toBe(1);

await blink._lock('test', cmd);
expect(calls).toBe(2);
});
describe('BlinkCamera', () => {
test.concurrent('.data', async () => {
const blink = new Blink(DEFAULT_BLINK_CLIENT_UUID);
Expand Down Expand Up @@ -228,13 +187,15 @@ describe('Blink', () => {
});

test.concurrent.each([
[false, false, false, false, 0, 0],
[false, false, true, false, 0, 0],
[false, false, false, true, 0, 0],
[false, false, true, true, Date.now(), 0],
[false, true, true, true, 0, 1],
[true, true, true, true, 0, 1],
])('BlinkCamera.refreshThumbnail()', async (mini, force, armed, enabled, thumbnailDate, expected) => {
[false, false, false, false, 'ok', 'online', 1, 0],
[false, false, true, false, 'ok', 'online', 1, 0],
[false, false, false, true, 'ok', 'online', 1, 0],
[false, false, true, true, 'ok', 'online', Date.now(), 0],
[false, true, true, true, 'ok', 'online', 1, 1],
[true, true, true, true, null, 'online', 1, 1],
[false, true, true, true, 'ok', 'offline', 1, 0],
[false, true, true, true, 'low', 'online', 1, 0],
])('BlinkCamera.refreshThumbnail()', async (mini, force, armed, enabled, battery, status, thumbnailDate, calls) => {
const blink = new Blink(DEFAULT_BLINK_CLIENT_UUID);
blink.blinkAPI.getAccountHomescreen.mockResolvedValue(SAMPLE.HOMESCREEN);
blink.blinkAPI.updateCameraThumbnail.mockResolvedValue(SAMPLE.UPDATE_THUMBNAIL);
Expand All @@ -246,14 +207,16 @@ describe('Blink', () => {
const cameraDevice = blink.cameras.get(cameraData.id);
cameraDevice.network.data.armed = armed;
cameraDevice.data.enabled = enabled;
if (battery) cameraDevice.data.battery = battery;
if (status) cameraDevice.data.status = status;
if (thumbnailDate) cameraDevice.thumbnailCreatedAt = thumbnailDate;

await cameraDevice.refreshThumbnail(force);

expect(blink.blinkAPI.updateCameraThumbnail).toBeCalledTimes(!mini ? expected : 0);
expect(blink.blinkAPI.updateOwlThumbnail).toBeCalledTimes(mini ? expected : 0);
expect(blink.blinkAPI.getCommand).toBeCalledTimes(expected);
expect(blink.blinkAPI.getAccountHomescreen).toBeCalledTimes(expected + 1);
expect(blink.blinkAPI.updateCameraThumbnail).toBeCalledTimes(!mini ? calls : 0);
expect(blink.blinkAPI.updateOwlThumbnail).toBeCalledTimes(mini ? calls : 0);
expect(blink.blinkAPI.getCommand).toBeCalledTimes(calls);
expect(blink.blinkAPI.getAccountHomescreen).toBeCalledTimes(calls + 1);
});

test.concurrent.each([
Expand Down Expand Up @@ -322,6 +285,11 @@ describe('Blink', () => {
expect(blink.blinkAPI.getAccountHomescreen).toBeCalledTimes(expected + 1);
});

test.concurrent.each([
[],
])('.thumbnailCreatedAt', async (url, date) => {

});
test.concurrent.each([
[false, false, true, false, 0, BlinkCamera.PRIVACY_BYTES],
[false, true, true, false, 0, BlinkCamera.PRIVACY_BYTES],
Expand Down Expand Up @@ -435,7 +403,7 @@ describe('Blink', () => {
});
});
describe('Blink', () => {
test.concurrent('Blink.diagnosticDebug()', async () => {
test.concurrent('.diagnosticDebug()', async () => {
const blink = new Blink(DEFAULT_BLINK_CLIENT_UUID);
blink.blinkAPI.login.mockResolvedValue({});
blink.blinkAPI.getAccountHomescreen.mockResolvedValue(SAMPLE.HOMESCREEN);
Expand Down Expand Up @@ -466,6 +434,47 @@ describe('Blink', () => {
await blink.refreshData();
await blink.diagnosticDebug();
});
test.concurrent('refreshData()', async () => {
const blink = new Blink(DEFAULT_BLINK_CLIENT_UUID);
blink.blinkAPI.getAccountHomescreen.mockResolvedValue(SAMPLE.HOMESCREEN);
await blink.refreshData();
expect(blink.blinkAPI.getAccountHomescreen).toHaveBeenCalled();
expect(blink.networks.size).toBe(3);
expect(blink.cameras.size).toBe(3);
});
test.concurrent('_commandWait()', async () => {
const blink = new Blink(DEFAULT_BLINK_CLIENT_UUID);
blink.blinkAPI.getAccountHomescreen.mockResolvedValue(JSON.parse(JSON.stringify(SAMPLE.HOMESCREEN)));
await blink.refreshData();

blink.blinkAPI.getCommand.mockResolvedValue(SAMPLE.COMMAND_RUNNING);
blink.blinkAPI.deleteCommand.mockResolvedValue({});
const {id: commandID, network_id: networkID} = SAMPLE.COMMAND_RUNNING.commands[0];
await blink._commandWait(networkID, commandID, 0.0001);

expect(blink.blinkAPI.getCommand).toBeCalledTimes(2);
expect(blink.blinkAPI.deleteCommand).toBeCalledTimes(1);

expect(await blink._commandWait(networkID)).toBeUndefined();
expect(await blink._commandWait(null, commandID)).toBeUndefined();
});
test.concurrent('_lock()', async () => {
const blink = new Blink(DEFAULT_BLINK_CLIENT_UUID);

let calls = 0;
const cmd = async () => {
await sleep(1);
calls++;
};
blink._lock('test', cmd);
await blink._lock('test', cmd);
await sleep(11);

expect(calls).toBe(1);

await blink._lock('test', cmd);
expect(calls).toBe(2);
});
test.concurrent.each([
[true, true, 1],
[true, false, 2],
Expand Down Expand Up @@ -494,13 +503,16 @@ describe('Blink', () => {
}
expect(blink.blinkAPI.armNetwork).toBeCalledTimes(expectedAPI);
expect(blink.blinkAPI.getCommand).toBeCalledTimes(timeout ? 0 : 1);
});
test.concurrent('.refreshCameraThumbnail()', async () => {

});
test.concurrent.each([
[0, Date.now(), 0, true, 0],
[Date.now(), 0, -1, true, 1],
[Date.now(), 0, 0, true, 1],
[Date.now()-1, 0, Date.now(), false, 1],
])('Blink.getCameraLastThumbnail()', async (cameraAt, thumbnailAt, mediaAt, expectThumbnail, expectAPI) => {
])('.getCameraLastThumbnail()', async (cameraAt, thumbnailAt, mediaAt, expectThumbnail, expectAPI) => {
const blink = new Blink(DEFAULT_BLINK_CLIENT_UUID);
blink.blinkAPI.getAccountHomescreen.mockResolvedValue(SAMPLE.HOMESCREEN);
await blink.refreshData();
Expand Down Expand Up @@ -528,7 +540,7 @@ describe('Blink', () => {
test.concurrent.each([
true,
false,
])('Blink.getCameraLastVideo()', async hasClip => {
])('.getCameraLastVideo()', async hasClip => {
const blink = new Blink(DEFAULT_BLINK_CLIENT_UUID);
blink.blinkAPI.getAccountHomescreen.mockResolvedValue(SAMPLE.HOMESCREEN);
await blink.refreshData();
Expand All @@ -555,7 +567,7 @@ describe('Blink', () => {
[false, null, 0, false],
[true, 7000001, 1, true],
[false, 7000001, 1, true],
])('Blink.deleteCameraMotion()', async (hasClip, motionId, expectDelete, success) => {
])('.deleteCameraMotion()', async (hasClip, motionId, expectDelete, success) => {
const blink = new Blink(DEFAULT_BLINK_CLIENT_UUID);
blink.blinkAPI.getAccountHomescreen.mockResolvedValue(SAMPLE.HOMESCREEN);
await blink.refreshData();
Expand All @@ -579,7 +591,7 @@ describe('Blink', () => {
[true, false, true, 2],
[true, true, false, 2],
[true, false, false, 4],
])('Blink.getSavedMedia()', async (hasClip, useNetworkId, useCameraId, expectedCount) => {
])('.getSavedMedia()', async (hasClip, useNetworkId, useCameraId, expectedCount) => {
const blink = new Blink(DEFAULT_BLINK_CLIENT_UUID);
blink.blinkAPI.getAccountHomescreen.mockResolvedValue(SAMPLE.HOMESCREEN);
await blink.refreshData();
Expand All @@ -597,7 +609,7 @@ describe('Blink', () => {
expect(res).toHaveLength(expectedCount);
expect(blink.blinkAPI.getMediaChange).toBeCalledTimes(1);
});
test.concurrent('Blink.stopCameraLiveView()', async () => {
test.concurrent('.stopCameraLiveView()', async () => {
const blink = new Blink(DEFAULT_BLINK_CLIENT_UUID);
blink.blinkAPI.getAccountHomescreen.mockResolvedValue(SAMPLE.HOMESCREEN);
await blink.refreshData();
Expand Down
Loading

0 comments on commit ddec3e1

Please sign in to comment.