@apocaliss92/scrypted-reolink-native 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/camera.ts +3002 -90
- package/src/intercom.ts +2 -4
- package/src/main.ts +12 -38
- package/src/multiFocal.ts +70 -142
- package/src/nvr.ts +17 -193
- package/src/presets.ts +2 -2
- package/src/utils.ts +3 -4
- package/src/camera-battery.ts +0 -283
- package/src/common.ts +0 -2782
package/src/nvr.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ReolinkBaichuanApi, ReolinkBaichuanDeviceSummary, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
2
|
import sdk, { AdoptDevice, Device, DeviceDiscovery, DeviceProvider, DiscoveredDevice, Reboot, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from "@scrypted/sdk";
|
|
3
3
|
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
|
4
4
|
import { BaseBaichuanClass, type BaichuanConnectionCallbacks, type BaichuanConnectionConfig } from "./baichuan-base";
|
|
5
|
-
import {
|
|
6
|
-
import { ReolinkNativeBatteryCamera } from "./camera-battery";
|
|
7
|
-
import { CommonCameraMixin } from "./common";
|
|
5
|
+
import { ReolinkCamera } from "./camera";
|
|
8
6
|
import { convertDebugLogsToApiOptions, getApiRelevantDebugLogs, getDebugLogChoices } from "./debug-options";
|
|
9
7
|
import ReolinkNativePlugin from "./main";
|
|
10
8
|
import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
|
|
11
9
|
import { batteryCameraSuffix, batteryMultifocalSuffix, cameraSuffix, getDeviceInterfaces, multifocalSuffix, updateDeviceInfo } from "./utils";
|
|
12
|
-
import { createBaichuanApi } from "./connect";
|
|
13
|
-
import { parseStreamProfileFromId } from "./stream-utils";
|
|
14
10
|
|
|
15
11
|
export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Settings, DeviceDiscovery, DeviceProvider, Reboot {
|
|
12
|
+
private readonly onSimpleEventBound = (ev: ReolinkSimpleEvent) => this.onSimpleEvent(ev);
|
|
13
|
+
|
|
16
14
|
storageSettings = new StorageSettings(this, {
|
|
17
15
|
debugLogs: {
|
|
18
16
|
title: 'Debug Events',
|
|
@@ -82,14 +80,11 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
82
80
|
this.debugLogsResetTimeout = undefined;
|
|
83
81
|
}
|
|
84
82
|
|
|
85
|
-
// Defer reset by 2 seconds to allow settings to settle
|
|
86
83
|
this.debugLogsResetTimeout = setTimeout(async () => {
|
|
87
84
|
this.debugLogsResetTimeout = undefined;
|
|
88
85
|
try {
|
|
89
|
-
// Force reconnection with new debug options
|
|
90
86
|
this.baichuanApi = undefined;
|
|
91
87
|
this.ensureClientPromise = undefined;
|
|
92
|
-
// Trigger reconnection
|
|
93
88
|
await this.ensureBaichuanClient();
|
|
94
89
|
} catch (e) {
|
|
95
90
|
logger.warn('Failed to reset client after debug logs change', e?.message || String(e));
|
|
@@ -106,7 +101,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
106
101
|
rtspChannel: number;
|
|
107
102
|
deviceData: ReolinkBaichuanDeviceSummary;
|
|
108
103
|
}>();
|
|
109
|
-
cameraNativeMap = new Map<string,
|
|
104
|
+
cameraNativeMap = new Map<string, ReolinkCamera>();
|
|
110
105
|
private channelToNativeIdMap = new Map<number, string>();
|
|
111
106
|
private discoverDevicesPromise: Promise<DiscoveredDevice[]> | undefined;
|
|
112
107
|
processing = false;
|
|
@@ -169,12 +164,8 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
169
164
|
onClose: async () => {
|
|
170
165
|
await this.reinit();
|
|
171
166
|
},
|
|
172
|
-
onSimpleEvent:
|
|
167
|
+
onSimpleEvent: this.onSimpleEventBound,
|
|
173
168
|
getEventSubscriptionEnabled: () => true,
|
|
174
|
-
// getEventSubscriptionEnabled: () => {
|
|
175
|
-
// const eventSource = this.storageSettings.values.eventSource || 'Native';
|
|
176
|
-
// return eventSource === 'Native';
|
|
177
|
-
// },
|
|
178
169
|
};
|
|
179
170
|
}
|
|
180
171
|
|
|
@@ -187,7 +178,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
187
178
|
}
|
|
188
179
|
|
|
189
180
|
protected async onBeforeCleanup(): Promise<void> {
|
|
190
|
-
await this.
|
|
181
|
+
await this.unsubscribeFromEvents();
|
|
191
182
|
}
|
|
192
183
|
|
|
193
184
|
async reinit() {
|
|
@@ -196,7 +187,6 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
196
187
|
this.initReinitTimeout = undefined;
|
|
197
188
|
}
|
|
198
189
|
|
|
199
|
-
// Schedule reinit with debounce
|
|
200
190
|
this.scheduleInit(true);
|
|
201
191
|
}
|
|
202
192
|
|
|
@@ -215,24 +205,12 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
215
205
|
}, isReinit ? 500 : 2000);
|
|
216
206
|
}
|
|
217
207
|
|
|
218
|
-
|
|
219
|
-
* Forward events received from Baichuan to the appropriate child device (Camera or MultiFocal).
|
|
220
|
-
* This ensures that only NVR (root device) subscribes to events, and events are forwarded down the hierarchy:
|
|
221
|
-
* - NVR → MultiFocal → Camera
|
|
222
|
-
* - NVR → Camera (directly)
|
|
223
|
-
*/
|
|
224
|
-
private forwardNativeEvent(ev: ReolinkSimpleEvent): void {
|
|
208
|
+
onSimpleEvent(ev: ReolinkSimpleEvent) {
|
|
225
209
|
const logger = this.getBaichuanLogger();
|
|
226
210
|
|
|
227
|
-
// const eventSource = this.storageSettings.values.eventSource || 'Native';
|
|
228
|
-
// if (eventSource !== 'Native') {
|
|
229
|
-
// return;
|
|
230
|
-
// }
|
|
231
|
-
|
|
232
211
|
try {
|
|
233
|
-
logger.debug(`Baichuan event: ${JSON.stringify(ev)}`);
|
|
212
|
+
logger.debug(`Baichuan event on nvr: ${JSON.stringify(ev)}`);
|
|
234
213
|
|
|
235
|
-
// Find device (camera or multifocal) for this channel
|
|
236
214
|
const channel = ev?.channel;
|
|
237
215
|
if (channel === undefined) {
|
|
238
216
|
logger.error('Event has no channel, ignoring');
|
|
@@ -247,69 +225,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
247
225
|
return;
|
|
248
226
|
}
|
|
249
227
|
|
|
250
|
-
|
|
251
|
-
if (targetDevice instanceof ReolinkNativeMultiFocalDevice) {
|
|
252
|
-
targetDevice.forwardNativeEvent(ev);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Convert event to camera's processEvents format
|
|
257
|
-
const objects: string[] = [];
|
|
258
|
-
let motion = false;
|
|
259
|
-
let isSleepingEvent = false;
|
|
260
|
-
let isOnlineEvent = false;
|
|
261
|
-
|
|
262
|
-
switch (ev?.type) {
|
|
263
|
-
case 'motion':
|
|
264
|
-
motion = true;
|
|
265
|
-
break;
|
|
266
|
-
case 'doorbell':
|
|
267
|
-
// Handle doorbell if camera supports it
|
|
268
|
-
try {
|
|
269
|
-
targetDevice.handleDoorbellEvent();
|
|
270
|
-
}
|
|
271
|
-
catch (e) {
|
|
272
|
-
logger.warn(`Error handling doorbell event for camera channel ${channel}`, e?.message || String(e));
|
|
273
|
-
}
|
|
274
|
-
motion = true;
|
|
275
|
-
break;
|
|
276
|
-
case 'people':
|
|
277
|
-
case 'vehicle':
|
|
278
|
-
case 'animal':
|
|
279
|
-
case 'face':
|
|
280
|
-
case 'package':
|
|
281
|
-
case 'other':
|
|
282
|
-
objects.push(ev.type);
|
|
283
|
-
motion = true;
|
|
284
|
-
break;
|
|
285
|
-
case 'awake':
|
|
286
|
-
case 'sleeping':
|
|
287
|
-
isSleepingEvent = true;
|
|
288
|
-
break;
|
|
289
|
-
case 'offline':
|
|
290
|
-
case 'online':
|
|
291
|
-
isOnlineEvent = true;
|
|
292
|
-
break;
|
|
293
|
-
default:
|
|
294
|
-
logger.error(`Unknown event type: ${ev?.type}`);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (isSleepingEvent) {
|
|
299
|
-
targetDevice.updateSleepingState({
|
|
300
|
-
reason: 'NVR',
|
|
301
|
-
state: ev.type === 'sleeping' ? 'sleeping' : 'awake',
|
|
302
|
-
}).catch(() => { });
|
|
303
|
-
} else if (isOnlineEvent) {
|
|
304
|
-
(targetDevice as ReolinkNativeBatteryCamera).updateOnlineState(
|
|
305
|
-
ev.type === 'online' ? true : false
|
|
306
|
-
).catch(() => { });
|
|
307
|
-
} else {
|
|
308
|
-
// Process events on the target camera
|
|
309
|
-
targetDevice.processEvents({ motion, objects }).catch((e) => {
|
|
310
|
-
logger.warn(`Error processing events for camera channel ${channel}`, e?.message || String(e));
|
|
311
|
-
});
|
|
312
|
-
}
|
|
228
|
+
targetDevice.onSimpleEvent(ev);
|
|
313
229
|
}
|
|
314
230
|
catch (e) {
|
|
315
231
|
logger.warn('Error in NVR Native event forwarder', e?.message || String(e));
|
|
@@ -324,16 +240,6 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
324
240
|
return await this.ensureBaichuanClient();
|
|
325
241
|
}
|
|
326
242
|
|
|
327
|
-
// async subscribeToAllEvents(): Promise<void> {
|
|
328
|
-
// const eventSource = this.storageSettings.values.eventSource || 'Native';
|
|
329
|
-
|
|
330
|
-
// if (eventSource !== 'Native') {
|
|
331
|
-
// await this.unsubscribeFromAllEvents();
|
|
332
|
-
// } else {
|
|
333
|
-
// await super.subscribeToEvents();
|
|
334
|
-
// }
|
|
335
|
-
// }
|
|
336
|
-
|
|
337
243
|
private async runNvrDiagnostics(): Promise<void> {
|
|
338
244
|
const logger = this.getBaichuanLogger();
|
|
339
245
|
logger.log(`Starting NVR diagnostics...`);
|
|
@@ -350,96 +256,12 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
350
256
|
}
|
|
351
257
|
}
|
|
352
258
|
|
|
353
|
-
async unsubscribeFromAllEvents(): Promise<void> {
|
|
354
|
-
// Use base class implementation
|
|
355
|
-
await super.unsubscribeFromEvents();
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Reinitialize event subscriptions based on selected event source
|
|
360
|
-
*/
|
|
361
|
-
// private async reinitEventSubscriptions(): Promise<void> {
|
|
362
|
-
// const logger = this.getBaichuanLogger();
|
|
363
|
-
// const { eventSource } = this.storageSettings.values;
|
|
364
|
-
|
|
365
|
-
// // Unsubscribe from Native events if switching away
|
|
366
|
-
// if (eventSource !== 'Native') {
|
|
367
|
-
// await this.unsubscribeFromAllEvents();
|
|
368
|
-
// } else {
|
|
369
|
-
// this.subscribeToAllEvents().catch((e) => {
|
|
370
|
-
// logger.warn('Failed to subscribe to Native events', e?.message || String(e));
|
|
371
|
-
// });
|
|
372
|
-
// }
|
|
373
|
-
|
|
374
|
-
// logger.log(`Event source set to: ${eventSource}`);
|
|
375
|
-
// }
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Forward events from CGI source to cameras
|
|
379
|
-
*/
|
|
380
|
-
// private forwardCgiEvents(eventsRes: Record<number, EventsResponse>): void {
|
|
381
|
-
// const logger = this.getBaichuanLogger();
|
|
382
|
-
|
|
383
|
-
// logger.debug(`CGI Events call result: ${JSON.stringify(eventsRes)}`);
|
|
384
|
-
|
|
385
|
-
// // Use channel map for efficient lookup
|
|
386
|
-
// for (const [channel, nativeId] of this.channelToNativeIdMap.entries()) {
|
|
387
|
-
// const targetCamera = nativeId ? this.cameraNativeMap.get(nativeId) : undefined;
|
|
388
|
-
// const cameraEventsData = eventsRes[channel];
|
|
389
|
-
// if (cameraEventsData && targetCamera) {
|
|
390
|
-
// targetCamera.processEvents(cameraEventsData);
|
|
391
|
-
// }
|
|
392
|
-
// }
|
|
393
|
-
// }
|
|
394
|
-
|
|
395
259
|
async init() {
|
|
396
|
-
// const logger = this.getBaichuanLogger();
|
|
397
260
|
await this.ensureBaichuanClient();
|
|
398
|
-
|
|
399
|
-
await this.updateDeviceInfo();
|
|
400
261
|
await this.subscribeToEvents();
|
|
262
|
+
await this.discoverDevices(true);
|
|
401
263
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
// setInterval(async () => {
|
|
405
|
-
// if (this.processing) {
|
|
406
|
-
// return;
|
|
407
|
-
// }
|
|
408
|
-
// this.processing = true;
|
|
409
|
-
// try {
|
|
410
|
-
// const api = await this.ensureBaichuanClient();
|
|
411
|
-
|
|
412
|
-
// const { eventSource } = this.storageSettings.values;
|
|
413
|
-
|
|
414
|
-
// if (eventSource === 'CGI') {
|
|
415
|
-
// const eventsRes = await api.getAllChannelsEvents();
|
|
416
|
-
// this.forwardCgiEvents(eventsRes.parsed);
|
|
417
|
-
|
|
418
|
-
// const { batteryInfoData, response } = await api.getAllChannelsBatteryInfo();
|
|
419
|
-
|
|
420
|
-
// logger.debug(`Battery info call result: ${JSON.stringify({ batteryInfoData, response })}`);
|
|
421
|
-
|
|
422
|
-
// this.cameraNativeMap.forEach((camera) => {
|
|
423
|
-
// if (camera) {
|
|
424
|
-
// const channel = camera.storageSettings.values.rtspChannel;
|
|
425
|
-
// const cameraBatteryData = batteryInfoData[channel];
|
|
426
|
-
// if (cameraBatteryData) {
|
|
427
|
-
// camera.updateSleepingState({
|
|
428
|
-
// reason: 'NVR',
|
|
429
|
-
// state: cameraBatteryData.sleeping ? 'sleeping' : 'awake',
|
|
430
|
-
// idleMs: 0,
|
|
431
|
-
// lastRxAtMs: 0,
|
|
432
|
-
// }).catch(() => { });
|
|
433
|
-
// }
|
|
434
|
-
// }
|
|
435
|
-
// });
|
|
436
|
-
// }
|
|
437
|
-
// } catch (e) {
|
|
438
|
-
// logger.error('Error on events flow', e?.message || String(e));
|
|
439
|
-
// } finally {
|
|
440
|
-
// this.processing = false;
|
|
441
|
-
// }
|
|
442
|
-
// }, 1000);
|
|
264
|
+
await this.updateDeviceInfo();
|
|
443
265
|
}
|
|
444
266
|
|
|
445
267
|
async updateDeviceInfo(): Promise<void> {
|
|
@@ -474,18 +296,18 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
474
296
|
this.cameraNativeMap.delete(nativeId);
|
|
475
297
|
}
|
|
476
298
|
|
|
477
|
-
async getDevice(nativeId: string): Promise<
|
|
299
|
+
async getDevice(nativeId: string): Promise<ReolinkCamera> {
|
|
478
300
|
let device = this.cameraNativeMap.get(nativeId);
|
|
479
301
|
|
|
480
302
|
if (!device) {
|
|
481
303
|
if (nativeId.endsWith(batteryCameraSuffix)) {
|
|
482
|
-
device = new
|
|
304
|
+
device = new ReolinkCamera(nativeId, this.plugin, { type: 'battery', nvrDevice: this });
|
|
483
305
|
} else if (nativeId.endsWith(batteryMultifocalSuffix)) {
|
|
484
306
|
device = new ReolinkNativeMultiFocalDevice(nativeId, this.plugin, "multi-focal-battery", this);
|
|
485
307
|
} else if (nativeId.endsWith(multifocalSuffix)) {
|
|
486
308
|
device = new ReolinkNativeMultiFocalDevice(nativeId, this.plugin, "multi-focal", this);
|
|
487
309
|
} else {
|
|
488
|
-
device = new
|
|
310
|
+
device = new ReolinkCamera(nativeId, this.plugin, { type: 'regular', nvrDevice: this });
|
|
489
311
|
}
|
|
490
312
|
|
|
491
313
|
if (device) {
|
|
@@ -529,6 +351,8 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
|
|
|
529
351
|
|
|
530
352
|
if (!channels.length) {
|
|
531
353
|
logger.debug(`No channels found, ${JSON.stringify({ channels, devices })}`);
|
|
354
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
355
|
+
await this.syncEntitiesFromRemote();
|
|
532
356
|
return;
|
|
533
357
|
}
|
|
534
358
|
|
package/src/presets.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PtzPreset } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
|
-
import type {
|
|
2
|
+
import type { ReolinkCamera } from "./camera";
|
|
3
3
|
|
|
4
4
|
export type PtzCapabilitiesShape = {
|
|
5
5
|
presets?: Record<string, string>;
|
|
@@ -7,7 +7,7 @@ export type PtzCapabilitiesShape = {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
export class ReolinkPtzPresets {
|
|
10
|
-
constructor(private camera:
|
|
10
|
+
constructor(private camera: ReolinkCamera & { ptzCapabilities?: any }) { }
|
|
11
11
|
|
|
12
12
|
private get storageSettings() {
|
|
13
13
|
return this.camera.storageSettings;
|
package/src/utils.ts
CHANGED
|
@@ -7,7 +7,7 @@ import https from "https";
|
|
|
7
7
|
import fs from "fs";
|
|
8
8
|
import path from "path";
|
|
9
9
|
import crypto from "crypto";
|
|
10
|
-
import {
|
|
10
|
+
import { ReolinkCamera } from "./camera";
|
|
11
11
|
/**
|
|
12
12
|
* Sanitize FFmpeg output or URLs to avoid leaking credentials
|
|
13
13
|
*/
|
|
@@ -135,7 +135,6 @@ export const updateDeviceInfo = async (props: {
|
|
|
135
135
|
|
|
136
136
|
throw e;
|
|
137
137
|
} finally {
|
|
138
|
-
|
|
139
138
|
logger.log(`Device info updated`);
|
|
140
139
|
logger.debug(`${JSON.stringify({ newInfo: device.info, deviceData })}`);
|
|
141
140
|
}
|
|
@@ -443,7 +442,7 @@ export async function extractThumbnailFromVideo(props: {
|
|
|
443
442
|
fileId: string;
|
|
444
443
|
deviceId: string;
|
|
445
444
|
logger?: Console;
|
|
446
|
-
device?:
|
|
445
|
+
device?: ReolinkCamera;
|
|
447
446
|
}): Promise<MediaObject> {
|
|
448
447
|
const { rtmpUrl, filePath, fileId, deviceId, device } = props;
|
|
449
448
|
// Use device logger if available, otherwise fallback to provided logger
|
|
@@ -489,7 +488,7 @@ function getVideoClipCachePath(deviceId: string, fileId: string): string {
|
|
|
489
488
|
* Checks cache first, then proxies RTMP stream if not cached
|
|
490
489
|
*/
|
|
491
490
|
export async function handleVideoClipRequest(props: {
|
|
492
|
-
device:
|
|
491
|
+
device: ReolinkCamera;
|
|
493
492
|
deviceId: string;
|
|
494
493
|
fileId: string;
|
|
495
494
|
request: HttpRequest;
|
package/src/camera-battery.ts
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CommonCameraMixin,
|
|
3
|
-
} from "./common";
|
|
4
|
-
import type ReolinkNativePlugin from "./main";
|
|
5
|
-
import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
|
|
6
|
-
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
7
|
-
|
|
8
|
-
export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
|
|
9
|
-
doorbellBinaryTimeout?: NodeJS.Timeout;
|
|
10
|
-
motionDetected: boolean = false;
|
|
11
|
-
motionTimeout: NodeJS.Timeout | undefined;
|
|
12
|
-
private periodicStarted = false;
|
|
13
|
-
private sleepCheckTimer: NodeJS.Timeout | undefined;
|
|
14
|
-
private batteryUpdateTimer: NodeJS.Timeout | undefined;
|
|
15
|
-
private lastBatteryLevel: number | undefined;
|
|
16
|
-
private batteryUpdateInProgress: boolean = false;
|
|
17
|
-
|
|
18
|
-
constructor(
|
|
19
|
-
nativeId: string,
|
|
20
|
-
public plugin: ReolinkNativePlugin,
|
|
21
|
-
nvrDevice?: ReolinkNativeNvrDevice,
|
|
22
|
-
multiFocalDevice?: ReolinkNativeMultiFocalDevice
|
|
23
|
-
) {
|
|
24
|
-
super(nativeId, plugin, {
|
|
25
|
-
type: 'battery',
|
|
26
|
-
nvrDevice,
|
|
27
|
-
multiFocalDevice,
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async init(): Promise<void> {
|
|
32
|
-
this.startPeriodicTasks();
|
|
33
|
-
await this.alignAuxDevicesState();
|
|
34
|
-
await this.updateBatteryInfo();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async release(): Promise<void> {
|
|
38
|
-
this.stopPeriodicTasks();
|
|
39
|
-
return this.resetBaichuanClient();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async reportDevices(): Promise<void> {
|
|
43
|
-
// Do nothing
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
private stopPeriodicTasks(): void {
|
|
47
|
-
if (this.sleepCheckTimer) {
|
|
48
|
-
clearInterval(this.sleepCheckTimer);
|
|
49
|
-
this.sleepCheckTimer = undefined;
|
|
50
|
-
}
|
|
51
|
-
if (this.batteryUpdateTimer) {
|
|
52
|
-
clearInterval(this.batteryUpdateTimer);
|
|
53
|
-
this.batteryUpdateTimer = undefined;
|
|
54
|
-
}
|
|
55
|
-
this.periodicStarted = false;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
private startPeriodicTasks(): void {
|
|
59
|
-
if (this.periodicStarted) return;
|
|
60
|
-
const logger = this.getBaichuanLogger();
|
|
61
|
-
this.periodicStarted = true;
|
|
62
|
-
|
|
63
|
-
logger.log('Starting periodic tasks for battery camera');
|
|
64
|
-
|
|
65
|
-
if (!this.nvrDevice && !this.multiFocalDevice) {
|
|
66
|
-
this.sleepCheckTimer = setInterval(async () => {
|
|
67
|
-
try {
|
|
68
|
-
const api = this.baichuanApi;
|
|
69
|
-
const channel = this.storageSettings.values.rtspChannel;
|
|
70
|
-
|
|
71
|
-
if (!api) {
|
|
72
|
-
if (!this.sleeping) {
|
|
73
|
-
logger.log('Camera is sleeping: no active Baichuan client');
|
|
74
|
-
this.sleeping = true;
|
|
75
|
-
}
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const sleepStatus = api.getSleepStatus({ channel });
|
|
80
|
-
await this.updateSleepingState(sleepStatus);
|
|
81
|
-
} catch (e) {
|
|
82
|
-
logger.warn('Error checking sleeping state:', e);
|
|
83
|
-
}
|
|
84
|
-
}, 5_000);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Update battery and snapshot every N minutes
|
|
88
|
-
const { batteryUpdateIntervalMinutes = 10 } = this.storageSettings.values;
|
|
89
|
-
const updateIntervalMs = batteryUpdateIntervalMinutes * 60_000;
|
|
90
|
-
this.batteryUpdateTimer = setInterval(() => {
|
|
91
|
-
this.updateBatteryAndSnapshot().catch(() => { });
|
|
92
|
-
}, updateIntervalMs);
|
|
93
|
-
|
|
94
|
-
logger.log(`Periodic tasks started: sleep check every 5s, battery update every ${batteryUpdateIntervalMinutes} minutes`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async checkRecordingAction(newBatteryLevel: number) {
|
|
98
|
-
const nvrDeviceId = this.plugin.nvrDeviceId;
|
|
99
|
-
if (nvrDeviceId && this.mixins.includes(nvrDeviceId)) {
|
|
100
|
-
const logger = this.getBaichuanLogger();
|
|
101
|
-
|
|
102
|
-
const settings = await this.thisDevice.getSettings();
|
|
103
|
-
const isRecording = !settings.find(setting => setting.key === 'recording:privacyMode')?.value;
|
|
104
|
-
const { lowThresholdBatteryRecording, highThresholdBatteryRecording } = this.storageSettings.values;
|
|
105
|
-
|
|
106
|
-
if (isRecording && newBatteryLevel < lowThresholdBatteryRecording) {
|
|
107
|
-
logger.log(`Recording is enabled, but battery level is below low threshold (${newBatteryLevel}% < ${lowThresholdBatteryRecording}%), disabling recording`);
|
|
108
|
-
await this.thisDevice.putSetting('recording:privacyMode', true);
|
|
109
|
-
} else if (!isRecording && newBatteryLevel > highThresholdBatteryRecording) {
|
|
110
|
-
logger.log(`Recording is disabled, but battery level is above high threshold (${newBatteryLevel}% > ${highThresholdBatteryRecording}%), enabling recording`);
|
|
111
|
-
await this.thisDevice.putSetting('recording:privacyMode', false);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async updateBatteryInfo() {
|
|
118
|
-
const api = await this.ensureClient();
|
|
119
|
-
const channel = this.storageSettings.values.rtspChannel;
|
|
120
|
-
|
|
121
|
-
const batteryInfo = await api.getBatteryInfo(channel);
|
|
122
|
-
if (this.isDebugEnabled()) {
|
|
123
|
-
this.getBaichuanLogger().debug('getBatteryInfo result:', JSON.stringify(batteryInfo));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (batteryInfo.batteryPercent !== undefined) {
|
|
127
|
-
const oldLevel = this.lastBatteryLevel;
|
|
128
|
-
this.batteryLevel = batteryInfo.batteryPercent;
|
|
129
|
-
this.lastBatteryLevel = batteryInfo.batteryPercent;
|
|
130
|
-
|
|
131
|
-
let shouldCheckRecordingAction = true;
|
|
132
|
-
|
|
133
|
-
// Log only if battery level changed
|
|
134
|
-
if (oldLevel !== undefined && oldLevel !== batteryInfo.batteryPercent) {
|
|
135
|
-
if (batteryInfo.chargeStatus !== undefined) {
|
|
136
|
-
// chargeStatus: "0"=charging, "1"=discharging, "2"=full
|
|
137
|
-
const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
|
|
138
|
-
this.getBaichuanLogger().log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}% (charging: ${charging})`);
|
|
139
|
-
} else {
|
|
140
|
-
this.getBaichuanLogger().log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}%`);
|
|
141
|
-
}
|
|
142
|
-
} else if (oldLevel === undefined) {
|
|
143
|
-
// First time setting battery level
|
|
144
|
-
if (batteryInfo.chargeStatus !== undefined) {
|
|
145
|
-
const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
|
|
146
|
-
this.getBaichuanLogger().log(`Battery level set: ${batteryInfo.batteryPercent}% (charging: ${charging})`);
|
|
147
|
-
} else {
|
|
148
|
-
this.getBaichuanLogger().log(`Battery level set: ${batteryInfo.batteryPercent}%`);
|
|
149
|
-
}
|
|
150
|
-
} else {
|
|
151
|
-
shouldCheckRecordingAction = false;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (shouldCheckRecordingAction) {
|
|
155
|
-
await this.checkRecordingAction(batteryInfo.batteryPercent);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
private async updateBatteryAndSnapshot(): Promise<void> {
|
|
161
|
-
// Prevent multiple simultaneous calls
|
|
162
|
-
if (this.batteryUpdateInProgress) {
|
|
163
|
-
this.getBaichuanLogger().debug('Battery update already in progress, skipping');
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
this.batteryUpdateInProgress = true;
|
|
168
|
-
try {
|
|
169
|
-
const channel = this.storageSettings.values.rtspChannel;
|
|
170
|
-
const updateIntervalMinutes = this.storageSettings.values.batteryUpdateIntervalMinutes ?? 10;
|
|
171
|
-
this.getBaichuanLogger().log(`Force battery update interval started (every ${updateIntervalMinutes} minutes)`);
|
|
172
|
-
|
|
173
|
-
// Ensure we have a client connection
|
|
174
|
-
const api = await this.ensureClient();
|
|
175
|
-
if (!api) {
|
|
176
|
-
this.getBaichuanLogger().warn('Failed to ensure client connection for battery update');
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Check current sleep status
|
|
181
|
-
let sleepStatus = api.getSleepStatus({ channel });
|
|
182
|
-
|
|
183
|
-
// If camera is sleeping, wake it up
|
|
184
|
-
if (sleepStatus.state === 'sleeping') {
|
|
185
|
-
this.getBaichuanLogger().log('Camera is sleeping, waking up for periodic update...');
|
|
186
|
-
try {
|
|
187
|
-
await api.wakeUp(channel, { waitAfterWakeMs: 2000 });
|
|
188
|
-
this.getBaichuanLogger().log('Wake command sent, waiting for camera to wake up...');
|
|
189
|
-
} catch (wakeError) {
|
|
190
|
-
this.getBaichuanLogger().warn('Failed to wake up camera:', wakeError);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Poll until camera is awake (with timeout)
|
|
195
|
-
const wakeTimeoutMs = 30000; // 30 seconds max
|
|
196
|
-
const startWakePoll = Date.now();
|
|
197
|
-
let awake = false;
|
|
198
|
-
|
|
199
|
-
while (Date.now() - startWakePoll < wakeTimeoutMs) {
|
|
200
|
-
await new Promise(resolve => setTimeout(resolve, 1000)); // Check every second
|
|
201
|
-
sleepStatus = api.getSleepStatus({ channel });
|
|
202
|
-
if (sleepStatus.state === 'awake') {
|
|
203
|
-
awake = true;
|
|
204
|
-
this.getBaichuanLogger().log('Camera is now awake');
|
|
205
|
-
this.sleeping = false;
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (!awake) {
|
|
211
|
-
this.getBaichuanLogger().warn('Camera did not wake up within timeout, skipping update');
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
} else if (sleepStatus.state === 'awake') {
|
|
215
|
-
this.sleeping = false;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Now that camera is awake, update all states
|
|
219
|
-
// 1. Update battery info
|
|
220
|
-
try {
|
|
221
|
-
await this.updateBatteryInfo();
|
|
222
|
-
} catch (e) {
|
|
223
|
-
this.getBaichuanLogger().warn('Failed to get battery info during periodic update:', e);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// 2. Align auxiliary devices state
|
|
227
|
-
try {
|
|
228
|
-
await this.alignAuxDevicesState();
|
|
229
|
-
} catch (e) {
|
|
230
|
-
this.getBaichuanLogger().warn('Failed to align auxiliary devices state:', e);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// 3. Update snapshot
|
|
234
|
-
try {
|
|
235
|
-
this.forceNewSnapshot = true;
|
|
236
|
-
await this.takePicture();
|
|
237
|
-
this.getBaichuanLogger().log('Snapshot updated during periodic update');
|
|
238
|
-
} catch (snapshotError) {
|
|
239
|
-
this.getBaichuanLogger().warn('Failed to update snapshot during periodic update:', snapshotError);
|
|
240
|
-
}
|
|
241
|
-
} catch (e) {
|
|
242
|
-
this.getBaichuanLogger().warn('Failed to update battery and snapshot', e);
|
|
243
|
-
} finally {
|
|
244
|
-
this.batteryUpdateInProgress = false;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async resetBaichuanClient(reason?: any): Promise<void> {
|
|
249
|
-
try {
|
|
250
|
-
this.unsubscribedToEvents?.();
|
|
251
|
-
|
|
252
|
-
// Close all stream servers before closing the main connection
|
|
253
|
-
// This ensures streams are properly cleaned up when using shared connection
|
|
254
|
-
if (this.streamManager) {
|
|
255
|
-
const reasonStr = reason?.message || reason?.toString?.() || 'connection reset';
|
|
256
|
-
await this.streamManager.closeAllStreams(reasonStr);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
await this.baichuanApi?.close();
|
|
260
|
-
}
|
|
261
|
-
catch (e) {
|
|
262
|
-
this.getBaichuanLogger().warn('Error closing Baichuan client during reset', e);
|
|
263
|
-
}
|
|
264
|
-
finally {
|
|
265
|
-
this.baichuanApi = undefined;
|
|
266
|
-
this.connectionTime = undefined;
|
|
267
|
-
this.ensureClientPromise = undefined;
|
|
268
|
-
if (this.sleepCheckTimer) {
|
|
269
|
-
clearInterval(this.sleepCheckTimer);
|
|
270
|
-
this.sleepCheckTimer = undefined;
|
|
271
|
-
}
|
|
272
|
-
if (this.batteryUpdateTimer) {
|
|
273
|
-
clearInterval(this.batteryUpdateTimer);
|
|
274
|
-
this.batteryUpdateTimer = undefined;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (reason) {
|
|
279
|
-
const message = reason?.message || reason?.toString?.() || reason;
|
|
280
|
-
this.getBaichuanLogger().warn(`Baichuan client reset requested: ${message}`);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|