@apocaliss92/scrypted-reolink-native 0.4.5 → 0.4.6

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/plugin.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apocaliss92/scrypted-reolink-native",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "Use any reolink camera with Scrypted, even older/unsupported models without HTTP protocol support",
5
5
  "author": "@apocaliss92",
6
6
  "license": "Apache",
@@ -467,11 +467,11 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
467
467
  this.errorListener = undefined;
468
468
  }
469
469
 
470
- // Close connection if still connected
470
+ // Close connection best-effort.
471
+ // Don't rely on isSocketConnected(): if the local socket state is inconsistent,
472
+ // skipping close can leave a "ghost" session on the device.
471
473
  try {
472
- if (api.client.isSocketConnected()) {
473
- await api.close();
474
- }
474
+ await api.close();
475
475
  } catch {
476
476
  // ignore
477
477
  }
@@ -799,133 +799,8 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
799
799
  ? await (this as any).ensureClient()
800
800
  : await this.ensureBaichuanClient();
801
801
 
802
- logger.log("[Sessions] Fetching active user sessions from device...");
803
-
804
- const sessions = await api.getOnlineUserList();
805
-
806
- // Get current socket session ID if available
807
- let socketSessionId: string | undefined;
808
- try {
809
- socketSessionId = api.client.getSocketSessionId();
810
- } catch {
811
- // getSocketSessionId might not be available on all clients
812
- }
813
-
814
- // Format sessions as array of readable strings
815
- const sessionStrings: string[] = [];
816
-
817
- // Add header with timestamp and socket session ID
818
- const timestamp = new Date().toLocaleString();
819
- sessionStrings.push(`Last updated: ${timestamp}`);
820
- if (socketSessionId) {
821
- sessionStrings.push(`Current socket session ID: ${socketSessionId}`);
822
- }
823
- sessionStrings.push(""); // Empty line separator
824
-
825
- // Parse sessions data (handle different response formats)
826
- let sessionCount = 0;
827
- const parseSessions = (data: any, prefix: string = ""): void => {
828
- if (Array.isArray(data)) {
829
- data.forEach((session: any, index: number) => {
830
- sessionCount++;
831
- const sessionInfo = formatSessionInfo(session, index + 1);
832
- sessionStrings.push(sessionInfo);
833
- });
834
- } else if (data && typeof data === "object") {
835
- // Handle nested objects
836
- for (const [key, value] of Object.entries(data)) {
837
- if (Array.isArray(value)) {
838
- value.forEach((session: any, index: number) => {
839
- sessionCount++;
840
- const sessionInfo = formatSessionInfo(session, index + 1, key);
841
- sessionStrings.push(sessionInfo);
842
- });
843
- } else if (value && typeof value === "object") {
844
- parseSessions(value, prefix ? `${prefix}.${key}` : key);
845
- } else {
846
- // Single session object
847
- sessionCount++;
848
- const sessionInfo = formatSessionInfo(data, 1);
849
- sessionStrings.push(sessionInfo);
850
- return; // Only process once
851
- }
852
- }
853
- }
854
- };
855
-
856
- // Helper function to format session info as readable string
857
- const formatSessionInfo = (
858
- session: any,
859
- index: number,
860
- group?: string,
861
- ): string => {
862
- const parts: string[] = [];
863
-
864
- if (group) {
865
- parts.push(`[${group}]`);
866
- }
867
- parts.push(`Session ${index}:`);
868
-
869
- // Extract common fields
870
- if (session.userName !== undefined) {
871
- parts.push(`User: ${session.userName}`);
872
- }
873
- if (session.user !== undefined) {
874
- parts.push(`User: ${session.user}`);
875
- }
876
- if (session.ip !== undefined) {
877
- parts.push(`IP: ${session.ip}`);
878
- }
879
- if (session.ipAddress !== undefined) {
880
- parts.push(`IP: ${session.ipAddress}`);
881
- }
882
- if (session.port !== undefined) {
883
- parts.push(`Port: ${session.port}`);
884
- }
885
- if (session.sessionId !== undefined) {
886
- parts.push(`Session ID: ${session.sessionId}`);
887
- }
888
- if (session.id !== undefined) {
889
- parts.push(`ID: ${session.id}`);
890
- }
891
- if (session.loginTime !== undefined) {
892
- parts.push(`Login Time: ${session.loginTime}`);
893
- }
894
- if (session.time !== undefined) {
895
- parts.push(`Time: ${session.time}`);
896
- }
897
- if (session.status !== undefined) {
898
- parts.push(`Status: ${session.status}`);
899
- }
900
-
901
- // If no common fields found, show all fields
902
- if (parts.length === (group ? 2 : 1)) {
903
- const allFields = Object.entries(session)
904
- .map(([key, value]) => `${key}: ${value}`)
905
- .join(", ");
906
- parts.push(allFields);
907
- }
908
-
909
- return parts.join(" | ");
910
- };
911
-
912
- parseSessions(sessions);
913
-
914
- // If no sessions found, add a message
915
- if (sessionCount === 0) {
916
- sessionStrings.push("No active sessions found");
917
- }
918
-
919
- // Update the setting with the formatted session strings
920
- // Note: storageSettings must be defined in subclasses
802
+ const sessionStrings = await api.getOnlineUserSessionsForUi();
921
803
  (this as any).storageSettings.values.userSessions = sessionStrings;
922
-
923
- logger.log(`[Sessions] Retrieved ${sessionCount} active session(s)`);
924
- if (socketSessionId) {
925
- logger.debug(
926
- `[Sessions] Current socket session ID: ${socketSessionId}`,
927
- );
928
- }
929
804
  } catch (e) {
930
805
  const errorMsg = e?.message || String(e);
931
806
  logger.error(`[Sessions] Failed to fetch user sessions: ${errorMsg}`);
package/src/camera.ts CHANGED
@@ -1264,148 +1264,7 @@ export class ReolinkCamera
1264
1264
  * Refresh the list of active user sessions from the device
1265
1265
  */
1266
1266
  async refreshUserSessionsList(): Promise<void> {
1267
- const logger = this.getBaichuanLogger();
1268
-
1269
- try {
1270
- const api = await this.ensureClient();
1271
-
1272
- logger.log("[Sessions] Fetching active user sessions from device...");
1273
-
1274
- const sessions = await api.getOnlineUserList();
1275
-
1276
- // Get current socket session ID if available
1277
- let socketSessionId: string | undefined;
1278
- try {
1279
- socketSessionId = api.client.getSocketSessionId();
1280
- } catch {
1281
- // getSocketSessionId might not be available on all clients
1282
- }
1283
-
1284
- // Format sessions as array of readable strings
1285
- const sessionStrings: string[] = [];
1286
-
1287
- // Add header with timestamp and socket session ID
1288
- const timestamp = new Date().toLocaleString();
1289
- sessionStrings.push(`Last updated: ${timestamp}`);
1290
- if (socketSessionId) {
1291
- sessionStrings.push(`Current socket session ID: ${socketSessionId}`);
1292
- }
1293
- sessionStrings.push(""); // Empty line separator
1294
-
1295
- // Parse sessions data (handle different response formats)
1296
- let sessionCount = 0;
1297
- const parseSessions = (data: any, prefix: string = ""): void => {
1298
- if (Array.isArray(data)) {
1299
- data.forEach((session: any, index: number) => {
1300
- sessionCount++;
1301
- const sessionInfo = formatSessionInfo(session, index + 1);
1302
- sessionStrings.push(sessionInfo);
1303
- });
1304
- } else if (data && typeof data === "object") {
1305
- // Handle nested objects
1306
- for (const [key, value] of Object.entries(data)) {
1307
- if (Array.isArray(value)) {
1308
- value.forEach((session: any, index: number) => {
1309
- sessionCount++;
1310
- const sessionInfo = formatSessionInfo(session, index + 1, key);
1311
- sessionStrings.push(sessionInfo);
1312
- });
1313
- } else if (value && typeof value === "object") {
1314
- parseSessions(value, prefix ? `${prefix}.${key}` : key);
1315
- } else {
1316
- // Single session object
1317
- sessionCount++;
1318
- const sessionInfo = formatSessionInfo(data, 1);
1319
- sessionStrings.push(sessionInfo);
1320
- return; // Only process once
1321
- }
1322
- }
1323
- }
1324
- };
1325
-
1326
- // Helper function to format session info as readable string
1327
- const formatSessionInfo = (
1328
- session: any,
1329
- index: number,
1330
- group?: string,
1331
- ): string => {
1332
- const parts: string[] = [];
1333
-
1334
- if (group) {
1335
- parts.push(`[${group}]`);
1336
- }
1337
- parts.push(`Session ${index}:`);
1338
-
1339
- // Extract common fields
1340
- if (session.userName !== undefined) {
1341
- parts.push(`User: ${session.userName}`);
1342
- }
1343
- if (session.user !== undefined) {
1344
- parts.push(`User: ${session.user}`);
1345
- }
1346
- if (session.ip !== undefined) {
1347
- parts.push(`IP: ${session.ip}`);
1348
- }
1349
- if (session.ipAddress !== undefined) {
1350
- parts.push(`IP: ${session.ipAddress}`);
1351
- }
1352
- if (session.port !== undefined) {
1353
- parts.push(`Port: ${session.port}`);
1354
- }
1355
- if (session.sessionId !== undefined) {
1356
- parts.push(`Session ID: ${session.sessionId}`);
1357
- }
1358
- if (session.id !== undefined) {
1359
- parts.push(`ID: ${session.id}`);
1360
- }
1361
- if (session.loginTime !== undefined) {
1362
- parts.push(`Login Time: ${session.loginTime}`);
1363
- }
1364
- if (session.time !== undefined) {
1365
- parts.push(`Time: ${session.time}`);
1366
- }
1367
- if (session.status !== undefined) {
1368
- parts.push(`Status: ${session.status}`);
1369
- }
1370
-
1371
- // If no common fields found, show all fields
1372
- if (parts.length === (group ? 2 : 1)) {
1373
- const allFields = Object.entries(session)
1374
- .map(([key, value]) => `${key}: ${value}`)
1375
- .join(", ");
1376
- parts.push(allFields);
1377
- }
1378
-
1379
- return parts.join(" | ");
1380
- };
1381
-
1382
- parseSessions(sessions);
1383
-
1384
- // If no sessions found, add a message
1385
- if (sessionCount === 0) {
1386
- sessionStrings.push("No active sessions found");
1387
- }
1388
-
1389
- // Update the setting with the formatted session strings
1390
- this.storageSettings.values.userSessions = sessionStrings;
1391
-
1392
- logger.log(`[Sessions] Retrieved ${sessionCount} active session(s)`);
1393
- if (socketSessionId) {
1394
- logger.debug(
1395
- `[Sessions] Current socket session ID: ${socketSessionId}`,
1396
- );
1397
- }
1398
- } catch (e) {
1399
- const errorMsg = e?.message || String(e);
1400
- logger.error(`[Sessions] Failed to fetch user sessions: ${errorMsg}`);
1401
-
1402
- // Update setting with error message
1403
- this.storageSettings.values.userSessions = [
1404
- `Error fetching sessions: ${errorMsg}`,
1405
- `Timestamp: ${new Date().toLocaleString()}`,
1406
- ];
1407
- throw e;
1408
- }
1267
+ return super.refreshUserSessionsList();
1409
1268
  }
1410
1269
 
1411
1270
  /**
package/src/nvr.ts CHANGED
@@ -120,8 +120,7 @@ export class ReolinkNativeNvrDevice
120
120
  this.debugLogsResetTimeout = setTimeout(async () => {
121
121
  this.debugLogsResetTimeout = undefined;
122
122
  try {
123
- this.baichuanApi = undefined;
124
- this.ensureClientPromise = undefined;
123
+ await this.cleanupBaichuanApi();
125
124
  await this.ensureBaichuanClient();
126
125
  } catch (e) {
127
126
  logger.warn(
@@ -600,9 +599,21 @@ export class ReolinkNativeNvrDevice
600
599
  channel: entry.rtspChannel,
601
600
  uid,
602
601
  });
603
- await baichuanApi.login();
604
- const { capabilities, objects, presets } =
605
- await baichuanApi.getDeviceCapabilities(entry.rtspChannel);
602
+ let capabilities: any;
603
+ let objects: any;
604
+ let presets: any;
605
+ try {
606
+ await baichuanApi.login();
607
+ ({ capabilities, objects, presets } =
608
+ await baichuanApi.getDeviceCapabilities(entry.rtspChannel));
609
+ } finally {
610
+ // Ensure the temporary socket used for adoption is not leaked.
611
+ try {
612
+ await baichuanApi.close({ reason: "adoptDevice" });
613
+ } catch {
614
+ // ignore
615
+ }
616
+ }
606
617
  const { interfaces, type } = getDeviceInterfaces({
607
618
  capabilities,
608
619
  logger: this.getBaichuanLogger(),
@@ -420,7 +420,7 @@ export class StreamManager {
420
420
  ? Boolean(compositeApis) && !(this.opts.sharedConnection ?? false)
421
421
  : !(this.opts.sharedConnection ?? false);
422
422
 
423
- let created: any;
423
+ let created: Rfc4571TcpServer;
424
424
  try {
425
425
  const compositeOptions = isComposite
426
426
  ? options.compositeOptions