@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/src/nvr.ts CHANGED
@@ -1,18 +1,16 @@
1
- import type { EventsResponse, NativeVideoStreamVariant, ReolinkBaichuanApi, ReolinkBaichuanDeviceSummary, ReolinkSimpleEvent, StreamProfile } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
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 { ReolinkNativeCamera } from "./camera";
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, CommonCameraMixin>();
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: (ev) => this.forwardNativeEvent(ev),
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.unsubscribeFromAllEvents();
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
- // If target is a MultiFocal device, forward the event to it (it will forward to its camera children)
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
- // await this.reinitEventSubscriptions();
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<CommonCameraMixin> {
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 ReolinkNativeBatteryCamera(nativeId, this.plugin, this);
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 ReolinkNativeCamera(nativeId, this.plugin, this);
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 { CommonCameraMixin } from "./common";
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: CommonCameraMixin & { ptzCapabilities?: any }) { }
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 { CommonCameraMixin } from "./common";
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?: CommonCameraMixin;
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: CommonCameraMixin;
491
+ device: ReolinkCamera;
493
492
  deviceId: string;
494
493
  fileId: string;
495
494
  request: HttpRequest;
@@ -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
- }