@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.
- package/.vscode/settings.json +1 -1
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/pcap/network_cam.txt +108321 -0
- package/pcap/wifi0ap0-2412MHz-UnifiRuocco.pcap +0 -0
- package/src/camera-battery.ts +106 -106
- package/src/camera.ts +1 -2
- package/src/common.ts +49 -27
- package/src/connect.ts +9 -4
- package/src/debug-options.ts +105 -0
- package/src/intercom.ts +16 -0
- package/src/stream-utils.ts +74 -8
- package/pcap/analyze_json.py +0 -248
- package/pcap/analyze_pcap.py +0 -135
- package/pcap/compare_pcaps.py +0 -274
- package/pcap/compare_stream_flow.py +0 -222
- package/pcap/scrypted.json +0 -307560
- package/pcap/scrypted.pcapng +0 -0
- package/pcap/simple_compare.py +0 -178
|
Binary file
|
package/src/camera-battery.ts
CHANGED
|
@@ -7,7 +7,7 @@ import sdk, {
|
|
|
7
7
|
import {
|
|
8
8
|
CommonCameraMixin,
|
|
9
9
|
} from "./common";
|
|
10
|
-
import {
|
|
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(
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
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.
|
|
264
|
+
this.console.warn('Failed to get battery info during periodic update:', e);
|
|
227
265
|
}
|
|
228
266
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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,
|
|
3
|
-
import { StorageSettings
|
|
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:
|
|
272
|
+
choices: getDebugLogChoices(),
|
|
272
273
|
onPut: async (ov, value) => {
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
oldSel
|
|
277
|
-
newSel
|
|
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
|
-
|
|
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
|
|
509
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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?:
|
|
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
|
-
|
|
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
|
});
|