@apocaliss92/scrypted-reolink-native 0.1.0 → 0.1.2

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.
@@ -7,7 +7,7 @@ import sdk, {
7
7
  import {
8
8
  CommonCameraMixin,
9
9
  } from "./common";
10
- import { createBaichuanApi, normalizeUid } from "./connect";
10
+ import { DebugLogOption } from "./debug-options";
11
11
  import type ReolinkNativePlugin from "./main";
12
12
 
13
13
  export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
@@ -21,10 +21,11 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
21
21
  private batteryUpdateTimer: NodeJS.Timeout | undefined;
22
22
  private lastBatteryLevel: number | undefined;
23
23
  private forceNewSnapshot: boolean = false;
24
+ private batteryUpdateInProgress: boolean = false;
24
25
 
25
26
  private isBatteryInfoLoggingEnabled(): boolean {
26
27
  const debugLogs = this.storageSettings.values.debugLogs || [];
27
- return debugLogs.includes('batteryInfo');
28
+ return debugLogs.includes(DebugLogOption.BatteryInfo);
28
29
  }
29
30
 
30
31
  constructor(nativeId: string, public plugin: ReolinkNativePlugin) {
@@ -150,43 +151,15 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
150
151
  this.sleeping = false;
151
152
  }
152
153
 
153
- // Try to get battery info only when camera is awake.
154
- // NOTE: getBatteryInfo in UDP mode is best-effort and should not force reconnect/login.
155
- try {
156
- const batteryInfo = await api.getBatteryInfo(channel);
157
- if (this.isBatteryInfoLoggingEnabled()) {
158
- this.console.log('getBatteryInfo result:', JSON.stringify(batteryInfo));
159
- }
160
-
161
- // Update battery percentage and charge status
162
- if (batteryInfo.batteryPercent !== undefined) {
163
- const oldLevel = this.lastBatteryLevel;
164
- this.batteryLevel = batteryInfo.batteryPercent;
165
- this.lastBatteryLevel = batteryInfo.batteryPercent;
166
-
167
- // Log only if battery level changed
168
- if (oldLevel !== undefined && oldLevel !== batteryInfo.batteryPercent) {
169
- if (batteryInfo.chargeStatus !== undefined) {
170
- // chargeStatus: "0"=charging, "1"=discharging, "2"=full
171
- const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
172
- this.console.log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}% (charging: ${charging})`);
173
- } else {
174
- this.console.log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}%`);
175
- }
176
- }
177
- }
178
- } catch (batteryError) {
179
- // Silently ignore battery info errors to avoid spam
180
- this.console.debug('Failed to get battery info:', batteryError);
181
- }
182
-
183
- // When camera wakes up, align auxiliary devices state and force snapshot (once)
154
+ // When camera wakes up (transition from sleeping to awake), align auxiliary devices state and force snapshot (once)
184
155
  if (wasSleeping) {
185
156
  this.alignAuxDevicesState().catch(() => { });
186
157
  if (this.forceNewSnapshot) {
187
158
  this.takePicture().catch(() => { });
188
159
  }
189
160
  }
161
+ // NOTE: We don't call getBatteryInfo() here anymore to avoid timeouts.
162
+ // Battery updates are handled by updateBatteryAndSnapshot() which properly wakes the camera.
190
163
  } else {
191
164
  // Unknown state
192
165
  this.console.debug(`Sleep status unknown: ${sleepStatus.reason}`);
@@ -198,83 +171,132 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
198
171
  }
199
172
 
200
173
  private async updateBatteryAndSnapshot(): Promise<void> {
174
+ // Prevent multiple simultaneous calls
175
+ if (this.batteryUpdateInProgress) {
176
+ this.console.debug('Battery update already in progress, skipping');
177
+ return;
178
+ }
179
+
180
+ this.batteryUpdateInProgress = true;
201
181
  try {
202
182
  const channel = this.getRtspChannel();
203
-
204
183
  const updateIntervalMinutes = this.storageSettings.values.batteryUpdateIntervalMinutes ?? 10;
205
184
  this.console.log(`Force battery update interval started (every ${updateIntervalMinutes} minutes)`);
206
185
 
207
- if (!this.baichuanApi) {
186
+ // Ensure we have a client connection
187
+ const api = await this.ensureClient();
188
+ if (!api) {
189
+ this.console.warn('Failed to ensure client connection for battery update');
208
190
  return;
209
191
  }
210
192
 
211
- const sleepStatus = this.baichuanApi.getSleepStatus({ channel });
212
- if (sleepStatus.state !== 'awake') {
213
- return;
193
+ // Check current sleep status
194
+ let sleepStatus = api.getSleepStatus({ channel });
195
+
196
+ // If camera is sleeping, wake it up
197
+ if (sleepStatus.state === 'sleeping') {
198
+ this.console.log('Camera is sleeping, waking up for periodic update...');
199
+ try {
200
+ await api.wakeUp(channel, { waitAfterWakeMs: 2000 });
201
+ this.console.log('Wake command sent, waiting for camera to wake up...');
202
+ } catch (wakeError) {
203
+ this.console.warn('Failed to wake up camera:', wakeError);
204
+ return;
205
+ }
206
+
207
+ // Poll until camera is awake (with timeout)
208
+ const wakeTimeoutMs = 30000; // 30 seconds max
209
+ const startWakePoll = Date.now();
210
+ let awake = false;
211
+
212
+ while (Date.now() - startWakePoll < wakeTimeoutMs) {
213
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Check every second
214
+ sleepStatus = api.getSleepStatus({ channel });
215
+ if (sleepStatus.state === 'awake') {
216
+ awake = true;
217
+ this.console.log('Camera is now awake');
218
+ this.sleeping = false;
219
+ break;
220
+ }
221
+ }
222
+
223
+ if (!awake) {
224
+ this.console.warn('Camera did not wake up within timeout, skipping update');
225
+ return;
226
+ }
227
+ } else if (sleepStatus.state === 'awake') {
228
+ this.sleeping = false;
214
229
  }
215
230
 
231
+ // Now that camera is awake, update all states
232
+ // 1. Update battery info
216
233
  try {
217
- const batteryInfo = await this.baichuanApi.getBatteryInfo(channel);
234
+ const batteryInfo = await api.getBatteryInfo(channel);
218
235
  if (this.isBatteryInfoLoggingEnabled()) {
219
236
  this.console.log('getBatteryInfo result:', JSON.stringify(batteryInfo));
220
237
  }
238
+
221
239
  if (batteryInfo.batteryPercent !== undefined) {
240
+ const oldLevel = this.lastBatteryLevel;
222
241
  this.batteryLevel = batteryInfo.batteryPercent;
223
242
  this.lastBatteryLevel = batteryInfo.batteryPercent;
243
+
244
+ // Log only if battery level changed
245
+ if (oldLevel !== undefined && oldLevel !== batteryInfo.batteryPercent) {
246
+ if (batteryInfo.chargeStatus !== undefined) {
247
+ // chargeStatus: "0"=charging, "1"=discharging, "2"=full
248
+ const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
249
+ this.console.log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}% (charging: ${charging})`);
250
+ } else {
251
+ this.console.log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}%`);
252
+ }
253
+ } else if (oldLevel === undefined) {
254
+ // First time setting battery level
255
+ if (batteryInfo.chargeStatus !== undefined) {
256
+ const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
257
+ this.console.log(`Battery level set: ${batteryInfo.batteryPercent}% (charging: ${charging})`);
258
+ } else {
259
+ this.console.log(`Battery level set: ${batteryInfo.batteryPercent}%`);
260
+ }
261
+ }
224
262
  }
225
263
  } catch (e) {
226
- this.console.debug('Failed to get battery info during periodic update:', e);
264
+ this.console.warn('Failed to get battery info during periodic update:', e);
227
265
  }
228
266
 
229
- // // Wait a bit for the camera to fully wake up
230
- // await new Promise(resolve => setTimeout(resolve, 2000));
231
-
232
- // // Get battery info
233
- // const batteryInfo = await api.getBatteryStatus(channel);
234
- // if (batteryInfo.batteryPercent !== undefined) {
235
- // const oldLevel = this.lastBatteryLevel;
236
- // this.batteryLevel = batteryInfo.batteryPercent;
237
- // this.lastBatteryLevel = batteryInfo.batteryPercent;
238
-
239
- // // Log only if battery level changed
240
- // if (oldLevel !== undefined && oldLevel !== batteryInfo.batteryPercent) {
241
- // if (batteryInfo.chargeStatus !== undefined) {
242
- // // chargeStatus: "0"=charging, "1"=discharging, "2"=full
243
- // const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
244
- // this.console.log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}% (charging: ${charging})`);
245
- // } else {
246
- // this.console.log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}%`);
247
- // }
248
- // } else if (oldLevel === undefined) {
249
- // // First time setting battery level
250
- // if (batteryInfo.chargeStatus !== undefined) {
251
- // const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
252
- // this.console.log(`Battery level set: ${batteryInfo.batteryPercent}% (charging: ${charging})`);
253
- // } else {
254
- // this.console.log(`Battery level set: ${batteryInfo.batteryPercent}%`);
255
- // }
256
- // }
257
- // }
258
-
259
- // // Update snapshot
260
- // try {
261
- // const snapshotBuffer = await api.getSnapshot(channel);
262
- // const mo = await sdk.mediaManager.createMediaObject(snapshotBuffer, 'image/jpeg');
263
- // this.lastPicture = { mo, atMs: Date.now() };
264
- // this.console.log('Snapshot updated');
265
- // } catch (snapshotError) {
266
- // this.console.warn('Failed to update snapshot during periodic update', snapshotError);
267
- // }
268
-
269
- // this.sleeping = false;
267
+ // 2. Align auxiliary devices state
268
+ try {
269
+ await this.alignAuxDevicesState();
270
+ } catch (e) {
271
+ this.console.warn('Failed to align auxiliary devices state:', e);
272
+ }
273
+
274
+ // 3. Update snapshot
275
+ try {
276
+ this.forceNewSnapshot = true;
277
+ await this.takePicture();
278
+ this.console.log('Snapshot updated during periodic update');
279
+ } catch (snapshotError) {
280
+ this.console.warn('Failed to update snapshot during periodic update:', snapshotError);
281
+ }
270
282
  } catch (e) {
271
283
  this.console.warn('Failed to update battery and snapshot', e);
284
+ } finally {
285
+ this.batteryUpdateInProgress = false;
272
286
  }
273
287
  }
274
288
 
275
289
  async resetBaichuanClient(reason?: any): Promise<void> {
276
290
  try {
277
291
  this.unsubscribedToEvents?.();
292
+
293
+ // Close all stream servers before closing the main connection
294
+ // This ensures streams are properly cleaned up when using shared connection
295
+ if (this.streamManager) {
296
+ const reasonStr = reason?.message || reason?.toString?.() || 'connection reset';
297
+ await this.streamManager.closeAllStreams(reasonStr);
298
+ }
299
+
278
300
  await this.baichuanApi?.close();
279
301
  }
280
302
  catch (e) {
@@ -310,30 +332,8 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
310
332
  }
311
333
 
312
334
  async createStreamClient(): Promise<ReolinkBaichuanApi> {
313
- const { ipAddress, username, password, uid } = this.storageSettings.values;
314
- if (!ipAddress || !username || !password) {
315
- throw new Error('Missing camera credentials');
316
- }
317
- const normalizedUid = normalizeUid(uid);
318
- if (!normalizedUid) throw new Error("UID is required for battery cameras (BCUDP)");
319
-
320
- const debugOptions = this.getBaichuanDebugOptions();
321
- const api = await createBaichuanApi(
322
- {
323
- inputs: {
324
- host: ipAddress,
325
- username,
326
- password,
327
- uid: normalizedUid,
328
- logger: this.console,
329
- ...(debugOptions ? { debugOptions } : {}),
330
- },
331
- transport: 'udp',
332
- logger: this.console,
333
- }
334
- );
335
- await api.login();
336
-
337
- return api;
335
+ // Reuse the main Baichuan client connection instead of creating a new one
336
+ // This ensures we use a single session for everything (general + streams)
337
+ return await this.ensureClient();
338
338
  }
339
339
  }
package/src/camera.ts CHANGED
@@ -95,7 +95,6 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
95
95
 
96
96
  async init() {
97
97
  this.startPeriodicTasks();
98
- // Align auxiliary devices state on init
99
98
  await this.alignAuxDevicesState();
100
99
  }
101
100
 
@@ -114,7 +113,7 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
114
113
  username,
115
114
  password,
116
115
  logger: this.console,
117
- ...(debugOptions ? { debugOptions } : {}),
116
+ debugOptions
118
117
  },
119
118
  transport: 'tcp',
120
119
  logger: this.console,
package/src/common.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { DeviceCapabilities, PtzCommand, PtzPreset, ReolinkBaichuanApi, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
- import sdk, { BinarySensor, Brightness, Camera, Device, DeviceProvider, Intercom, MediaObject, MediaStreamUrl, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, RequestMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue, Settings, VideoCamera, VideoTextOverlay, VideoTextOverlays } from "@scrypted/sdk";
3
- import { StorageSettings, StorageSettingsDict } from "@scrypted/sdk/storage-settings";
2
+ import sdk, { BinarySensor, Brightness, Camera, Device, DeviceProvider, Intercom, MediaObject, MediaStreamUrl, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, RequestMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoTextOverlay, VideoTextOverlays } from "@scrypted/sdk";
3
+ import { StorageSettings } from "@scrypted/sdk/storage-settings";
4
4
  import type { UrlMediaStreamOptions } from "../../scrypted/plugins/rtsp/src/rtsp";
5
5
  import { createBaichuanApi, normalizeUid, type BaichuanTransport } from "./connect";
6
+ import { convertDebugLogsToApiOptions, DebugLogOption, getApiRelevantDebugLogs, getDebugLogChoices } from "./debug-options";
6
7
  import { ReolinkBaichuanIntercom } from "./intercom";
7
8
  import ReolinkNativePlugin from "./main";
8
9
  import { ReolinkPtzPresets } from "./presets";
@@ -268,17 +269,36 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
268
269
  combobox: true,
269
270
  immediate: true,
270
271
  defaultValue: [],
271
- choices: ['enabled', 'debugRtsp', 'traceStream', 'traceTalk', 'traceEvents', 'debugH264', 'debugParamSets', 'eventLogs', 'batteryInfo'],
272
+ choices: getDebugLogChoices(),
272
273
  onPut: async (ov, value) => {
273
- // Only reconnect if Baichuan-client flags changed; toggling event logs should be immediate.
274
- const oldSel = new Set(ov);
275
- const newSel = new Set(value);
276
- oldSel.delete('eventLogs');
277
- newSel.delete('eventLogs');
274
+ const oldApiOptions = getApiRelevantDebugLogs(ov || []);
275
+ const newApiOptions = getApiRelevantDebugLogs(value || []);
276
+
277
+ const oldSel = new Set(oldApiOptions);
278
+ const newSel = new Set(newApiOptions);
278
279
 
279
280
  const changed = oldSel.size !== newSel.size || Array.from(oldSel).some((k) => !newSel.has(k));
280
281
  if (changed && this.resetBaichuanClient) {
281
- await this.resetBaichuanClient('debugLogs changed');
282
+ // Clear any existing timeout
283
+ if (this.debugLogsResetTimeout) {
284
+ clearTimeout(this.debugLogsResetTimeout);
285
+ this.debugLogsResetTimeout = undefined;
286
+ }
287
+
288
+ // Defer reset by 2 seconds to allow settings to settle
289
+ this.debugLogsResetTimeout = setTimeout(async () => {
290
+ this.debugLogsResetTimeout = undefined;
291
+ try {
292
+ await this.resetBaichuanClient('debugLogs changed');
293
+ // Force reconnection with new debug options
294
+ this.baichuanApi = undefined;
295
+ this.ensureClientPromise = undefined;
296
+ // Trigger reconnection
297
+ await this.ensureClient();
298
+ } catch (e) {
299
+ this.getLogger().warn('Failed to reset client after debug logs change', e);
300
+ }
301
+ }, 2000);
282
302
  }
283
303
  },
284
304
  },
@@ -461,6 +481,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
461
481
  protected ensureClientPromise: Promise<ReolinkBaichuanApi> | undefined;
462
482
  protected connectionTime: number | undefined;
463
483
  protected readonly protocol: BaichuanTransport;
484
+ private debugLogsResetTimeout: NodeJS.Timeout | undefined;
464
485
 
465
486
  // Abstract init method that subclasses must implement
466
487
  abstract init(): Promise<void>;
@@ -480,6 +501,12 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
480
501
  this.streamManager = new StreamManager({
481
502
  createStreamClient: () => this.createStreamClient(),
482
503
  getLogger: () => this.getLogger(),
504
+ credentials: {
505
+ username: this.storageSettings.values.username || '',
506
+ password: this.storageSettings.values.password || '',
507
+ },
508
+ // For battery cameras, we use a shared connection
509
+ sharedConnection: options.type === 'battery',
483
510
  });
484
511
 
485
512
  setTimeout(async () => {
@@ -505,17 +532,8 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
505
532
  }
506
533
 
507
534
  getBaichuanDebugOptions(): any | undefined {
508
- const sel = new Set<string>(this.storageSettings.values.debugLogs);
509
- if (!sel.size) return undefined;
510
-
511
- const debugOptions: any = {};
512
- // Only pass through Baichuan client debug flags.
513
- const clientKeys = new Set(['enabled', 'debugRtsp', 'traceStream', 'traceTalk', 'traceEvents', 'debugH264', 'debugParamSets']);
514
- for (const k of sel) {
515
- if (!clientKeys.has(k)) continue;
516
- debugOptions[k] = true;
517
- }
518
- return Object.keys(debugOptions).length ? debugOptions : undefined;
535
+ const debugLogs = this.storageSettings.values.debugLogs || [];
536
+ return convertDebugLogsToApiOptions(debugLogs);
519
537
  }
520
538
 
521
539
  isRecoverableBaichuanError(e: any): boolean {
@@ -560,12 +578,12 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
560
578
  try {
561
579
  const logger = this.getLogger();
562
580
 
563
- if (this.storageSettings.values.dispatchEvents.includes('eventLogs')) {
581
+ if (this.isEventLogsEnabled()) {
564
582
  logger.log(`Baichuan event: ${JSON.stringify(ev)}`);
565
583
  }
566
584
 
567
585
  if (!this.isEventDispatchEnabled()) {
568
- if (this.storageSettings.values.dispatchEvents.includes('eventLogs')) {
586
+ if (this.isEventLogsEnabled()) {
569
587
  logger.debug('Event dispatch is disabled, ignoring event');
570
588
  }
571
589
  return;
@@ -573,7 +591,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
573
591
 
574
592
  const channel = this.getRtspChannel();
575
593
  if (ev?.channel !== undefined && ev.channel !== channel) {
576
- if (this.storageSettings.values.dispatchEvents.includes('eventLogs')) {
594
+ if (this.isEventLogsEnabled()) {
577
595
  logger.debug(`Event channel ${ev.channel} does not match camera channel ${channel}, ignoring`);
578
596
  }
579
597
  return;
@@ -585,7 +603,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
585
603
  switch (ev?.type) {
586
604
  case 'motion':
587
605
  motion = true;
588
- if (this.storageSettings.values.dispatchEvents.includes('eventLogs')) {
606
+ if (this.isEventLogsEnabled()) {
589
607
  logger.log(`Motion event received (may be PIR or MD)`);
590
608
  }
591
609
  break;
@@ -603,7 +621,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
603
621
  motion = true;
604
622
  break;
605
623
  default:
606
- if (this.storageSettings.values.dispatchEvents.includes('eventLogs')) {
624
+ if (this.isEventLogsEnabled()) {
607
625
  logger.debug(`Unknown event type: ${ev?.type}`);
608
626
  }
609
627
  return;
@@ -859,6 +877,11 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
859
877
  }
860
878
  }
861
879
 
880
+ isEventLogsEnabled(): boolean {
881
+ const debugLogs = this.storageSettings.values.debugLogs || [];
882
+ return debugLogs.includes(DebugLogOption.EventLogs);
883
+ }
884
+
862
885
  // BinarySensor interface implementation (for doorbell)
863
886
  handleDoorbellEvent(): void {
864
887
  if (!this.doorbellBinaryTimeout) {
@@ -1317,7 +1340,6 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1317
1340
  });
1318
1341
  };
1319
1342
 
1320
- // Use withBaichuanRetry (regular cameras have retry logic, battery cameras just execute)
1321
1343
  return await this.withBaichuanRetry(createStreamFn);
1322
1344
  }
1323
1345
 
@@ -1391,7 +1413,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1391
1413
  password,
1392
1414
  uid: normalizedUid,
1393
1415
  logger: this.console,
1394
- ...(debugOptions ? { debugOptions } : {}),
1416
+ debugOptions,
1395
1417
  },
1396
1418
  transport: this.protocol,
1397
1419
  logger: this.console,
package/src/connect.ts CHANGED
@@ -8,7 +8,7 @@ export type BaichuanConnectInputs = {
8
8
  password: string;
9
9
  uid?: string;
10
10
  logger?: Console;
11
- debugOptions?: unknown;
11
+ debugOptions?: BaichuanClientOptions['debugOptions'];
12
12
  };
13
13
 
14
14
  export function normalizeUid(uid?: string): string | undefined {
@@ -52,7 +52,7 @@ export async function createBaichuanApi(props: {
52
52
  username: inputs.username,
53
53
  password: inputs.password,
54
54
  logger: inputs.logger,
55
- ...(inputs.debugOptions ? { debugOptions: inputs.debugOptions } : {}),
55
+ debugOptions: inputs.debugOptions ?? {}
56
56
  };
57
57
 
58
58
  const attachErrorHandler = (api: ReolinkBaichuanApi) => {
@@ -66,9 +66,11 @@ export async function createBaichuanApi(props: {
66
66
  // Only log if it's not a recoverable error to avoid spam
67
67
  if (typeof msg === 'string' && (
68
68
  msg.includes('Baichuan socket closed') ||
69
- msg.includes('Baichuan UDP stream closed')
69
+ msg.includes('Baichuan UDP stream closed') ||
70
+ msg.includes('Not running')
70
71
  )) {
71
- // Silently ignore recoverable socket close errors
72
+ // Silently ignore recoverable socket close errors and "Not running" errors
73
+ // "Not running" is common for UDP/battery cameras when sleeping or during initialization
72
74
  return;
73
75
  }
74
76
  logger.error(`[BaichuanClient] error (${transport}) ${inputs.host}: ${msg}`);
@@ -84,6 +86,8 @@ export async function createBaichuanApi(props: {
84
86
  }
85
87
  };
86
88
 
89
+ logger.log('Connecting with options:', JSON.stringify(base, null, 2));
90
+
87
91
  if (transport === "tcp") {
88
92
  const api = new ReolinkBaichuanApi({
89
93
  ...base,
@@ -102,6 +106,7 @@ export async function createBaichuanApi(props: {
102
106
  ...base,
103
107
  transport: "udp",
104
108
  uid,
109
+ idleDisconnect: true,
105
110
  });
106
111
  attachErrorHandler(api);
107
112
  return api;
@@ -0,0 +1,105 @@
1
+ import type { DebugOptions } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
+
3
+ /**
4
+ * User-friendly debug log options enum
5
+ */
6
+ export enum DebugLogOption {
7
+ /** General debug logs */
8
+ General = 'general',
9
+ /** RTSP proxy/server debug logs */
10
+ DebugRtsp = 'debugRtsp',
11
+ /** Stream command tracing */
12
+ TraceStream = 'traceStream',
13
+ /** Talkback tracing */
14
+ TraceTalk = 'traceTalk',
15
+ /** Event tracing */
16
+ TraceEvents = 'traceEvents',
17
+ /** H.264 debug logs */
18
+ DebugH264 = 'debugH264',
19
+ /** SPS/PPS parameter sets debug logs */
20
+ DebugParamSets = 'debugParamSets',
21
+ /** Event logs (plugin-specific, not passed to API) */
22
+ EventLogs = 'eventLogs',
23
+ /** Battery info logs (plugin-specific, not passed to API) */
24
+ BatteryInfo = 'batteryInfo',
25
+ }
26
+
27
+ /**
28
+ * Maps user-friendly enum values to API DebugOptions keys
29
+ */
30
+ export function mapDebugLogToApiOption(option: DebugLogOption): keyof DebugOptions | null {
31
+ const mapping: Record<DebugLogOption, keyof DebugOptions | null> = {
32
+ [DebugLogOption.General]: 'general',
33
+ [DebugLogOption.DebugRtsp]: 'debugRtsp',
34
+ [DebugLogOption.TraceStream]: 'traceStream',
35
+ [DebugLogOption.TraceTalk]: 'traceTalk',
36
+ [DebugLogOption.TraceEvents]: 'traceEvents',
37
+ [DebugLogOption.DebugH264]: 'debugH264',
38
+ [DebugLogOption.DebugParamSets]: 'debugParamSets',
39
+ [DebugLogOption.EventLogs]: null, // Plugin-specific, not passed to API
40
+ [DebugLogOption.BatteryInfo]: null, // Plugin-specific, not passed to API
41
+ };
42
+ return mapping[option];
43
+ }
44
+
45
+ /**
46
+ * Convert array of DebugLogOption enum values to API DebugOptions
47
+ * Only includes options that are relevant to the API (excludes plugin-specific options)
48
+ */
49
+ export function convertDebugLogsToApiOptions(debugLogs: string[]): DebugOptions | undefined {
50
+ const apiOptions: DebugOptions = {};
51
+ const debugLogsSet = new Set(debugLogs);
52
+
53
+ // Iterate over enum values and build API options based on what's selected
54
+ for (const [key, friendlyName] of Object.entries(DebugLogDisplayNames)) {
55
+ if (debugLogsSet.has(friendlyName)) {
56
+ const apiKey = mapDebugLogToApiOption(key as DebugLogOption);
57
+ if (apiKey) {
58
+ apiOptions[apiKey] = true;
59
+ }
60
+ }
61
+ }
62
+
63
+ console.log(debugLogs, apiOptions);
64
+ return Object.keys(apiOptions).length > 0 ? apiOptions : undefined;
65
+ }
66
+
67
+ /**
68
+ * Get only the API-relevant debug log options (excludes plugin-specific options)
69
+ * Used to determine if reconnection is needed when debug options change
70
+ */
71
+ export function getApiRelevantDebugLogs(debugLogs: string[]): string[] {
72
+ return debugLogs.filter(log => {
73
+ const option = log as DebugLogOption;
74
+ const apiKey = mapDebugLogToApiOption(option);
75
+ // Only include options that map to API keys (exclude plugin-specific options)
76
+ return apiKey !== null;
77
+ });
78
+ }
79
+
80
+ /**
81
+ * User-friendly display names for debug log options
82
+ */
83
+ export const DebugLogDisplayNames: Record<DebugLogOption, string> = {
84
+ [DebugLogOption.General]: 'General',
85
+ [DebugLogOption.DebugRtsp]: 'RTSP',
86
+ [DebugLogOption.TraceStream]: 'Trace stream',
87
+ [DebugLogOption.TraceTalk]: 'Trace talk',
88
+ [DebugLogOption.TraceEvents]: 'Trace events XML',
89
+ [DebugLogOption.DebugH264]: 'H264',
90
+ [DebugLogOption.DebugParamSets]: 'Video param sets',
91
+ [DebugLogOption.EventLogs]: 'Object detection events',
92
+ [DebugLogOption.BatteryInfo]: 'Battery info update',
93
+ };
94
+
95
+ /**
96
+ * Get debug log choices with user-friendly names
97
+ * Returns array of strings in format "value=displayName" for Scrypted settings
98
+ */
99
+ export function getDebugLogChoices(): string[] {
100
+ return Object.values(DebugLogOption).map(option => {
101
+ const displayName = DebugLogDisplayNames[option];
102
+ return `${displayName}`;
103
+ });
104
+ }
105
+
package/src/intercom.ts CHANGED
@@ -69,6 +69,22 @@ export class ReolinkBaichuanIntercom {
69
69
 
70
70
  const session = await this.camera.withBaichuanRetry(async () => {
71
71
  const api = await this.camera.ensureClient();
72
+
73
+ // For UDP/battery cameras, wake up the camera if it's sleeping before creating talk session
74
+ if (this.camera.options?.type === 'battery') {
75
+ try {
76
+ const sleepStatus = api.getSleepStatus({ channel });
77
+ if (sleepStatus.state === 'sleeping') {
78
+ logger.log('Camera is sleeping, waking up for intercom...');
79
+ await api.wakeUp(channel, { waitAfterWakeMs: 2000 });
80
+ // Wait a bit more to ensure camera is fully awake
81
+ await new Promise(resolve => setTimeout(resolve, 1000));
82
+ }
83
+ } catch (e) {
84
+ logger.debug('Failed to check/wake camera for intercom, proceeding anyway', e);
85
+ }
86
+ }
87
+
72
88
  return await api.createTalkSession(channel, {
73
89
  blocksPerPayload: this.blocksPerPayload,
74
90
  });