@apocaliss92/scrypted-reolink-native 0.0.4 → 0.0.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.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Reolink Native plugin for Scrypted",
5
5
  "author": "@apocaliss92",
6
6
  "license": "Apache",
package/src/camera.ts CHANGED
@@ -2,7 +2,7 @@ import type { BatteryInfo, DebugOptions, DeviceCapabilities, PtzCommand, Reolink
2
2
  import sdk, { BinarySensor, Brightness, Camera, Device, DeviceProvider, Intercom, MediaObject, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, RequestMediaStreamOptions, RequestPictureOptions, ResponseMediaStreamOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, Sleep, VideoCamera, VideoTextOverlay, VideoTextOverlays } from "@scrypted/sdk";
3
3
  import { StorageSettings } from '@scrypted/sdk/storage-settings';
4
4
  import { UrlMediaStreamOptions } from "../../scrypted/plugins/rtsp/src/rtsp";
5
- import { BaichuanTransport, connectBaichuanWithTcpUdpFallback, createBaichuanApi, maskUid } from './connect';
5
+ import { createBaichuanApi, maskUid, normalizeUid, type BaichuanTransport } from './connect';
6
6
  import { ReolinkBaichuanIntercom } from "./intercom";
7
7
  import ReolinkNativePlugin from "./main";
8
8
  import { ReolinkPtzPresets } from "./presets";
@@ -166,6 +166,7 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
166
166
  floodlight: ReolinkCameraFloodlight;
167
167
  pirSensor: ReolinkCameraPirSensor;
168
168
  private baichuanApi: ReolinkBaichuanApi | undefined;
169
+ private ensureClientPromise: Promise<ReolinkBaichuanApi> | undefined;
169
170
  private connectionTime: number | undefined;
170
171
  private refreshingState = false;
171
172
 
@@ -176,7 +177,7 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
176
177
  private lastSnapshotTaken: number | undefined;
177
178
  private streamManager: StreamManager;
178
179
 
179
- private udpFallbackAlerted = false;
180
+ private uidMissingAlerted = false;
180
181
 
181
182
  private intercom: ReolinkBaichuanIntercom;
182
183
 
@@ -230,7 +231,7 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
230
231
  combobox: true,
231
232
  immediate: true,
232
233
  defaultValue: [],
233
- choices: ['enabled', 'debugRtsp', 'traceStream', 'traceTalk', 'debugH264', 'debugParamSets', 'eventLogs'],
234
+ choices: ['enabled', 'debugRtsp', 'traceStream', 'traceTalk', 'traceEvents', 'debugH264', 'debugParamSets', 'eventLogs'],
234
235
  onPut: async (ov, value) => {
235
236
  // Only reconnect if Baichuan-client flags changed; toggling event logs should be immediate.
236
237
  const oldSel = new Set(ov);
@@ -238,7 +239,7 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
238
239
  oldSel.delete('eventLogs');
239
240
  newSel.delete('eventLogs');
240
241
 
241
- const changed = oldSel.size !== newSel.size;
242
+ const changed = oldSel.size !== newSel.size || Array.from(oldSel).some((k) => !newSel.has(k));
242
243
  if (changed) {
243
244
  await this.resetBaichuanClient('debugLogs changed');
244
245
  }
@@ -458,6 +459,7 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
458
459
  finally {
459
460
  this.baichuanApi = undefined;
460
461
  this.connectionTime = undefined;
462
+ this.ensureClientPromise = undefined;
461
463
  }
462
464
 
463
465
  if (reason) {
@@ -517,72 +519,73 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
517
519
  }
518
520
 
519
521
  async ensureClient(): Promise<ReolinkBaichuanApi> {
520
- if (this.baichuanApi && this.baichuanApi.client.loggedIn) {
521
- return this.baichuanApi;
522
- }
522
+ if (this.baichuanApi && this.baichuanApi.client.loggedIn) return this.baichuanApi;
523
523
 
524
- const { ipAddress, username, password, uid, useUdp } = this.storageSettings.values;
524
+ // Prevent concurrent login storms. Multiple callers (init, options queries, refresh, events)
525
+ // may race here and otherwise create multiple Baichuan sessions in parallel.
526
+ if (this.ensureClientPromise) return await this.ensureClientPromise;
525
527
 
526
- if (!ipAddress || !username || !password) {
527
- throw new Error('Missing camera credentials');
528
- }
528
+ this.ensureClientPromise = (async () => {
529
+ if (this.baichuanApi && this.baichuanApi.client.loggedIn) return this.baichuanApi;
529
530
 
530
- if (this.baichuanApi) {
531
- await this.baichuanApi.close();
532
- }
531
+ const { ipAddress, username, password, uid } = this.storageSettings.values;
533
532
 
534
- const debugOptions = this.getBaichuanDebugOptions();
533
+ if (!ipAddress || !username || !password) {
534
+ throw new Error('Missing camera credentials');
535
+ }
536
+
537
+ if (this.baichuanApi) {
538
+ await this.baichuanApi.close();
539
+ }
540
+
541
+ const debugOptions = this.getBaichuanDebugOptions();
542
+
543
+ // Transport selection is deterministic: battery cameras use UDP/BCUDP, others use TCP.
544
+ // No automatic fallback between transports.
545
+ const isBattery = this.hasBattery();
546
+ const transport: BaichuanTransport = isBattery ? 'udp' : 'tcp';
547
+ const normalizedUid = normalizeUid(uid);
548
+
549
+ if (transport === 'udp' && !normalizedUid) {
550
+ if (!this.uidMissingAlerted) {
551
+ this.uidMissingAlerted = true;
552
+ this.log.a(
553
+ `Battery camera ${this.name} (${ipAddress}) requires Reolink UID for UDP/BCUDP. Please set the UID in settings.`,
554
+ );
555
+ }
556
+ throw new Error('UID is required for battery cameras (BCUDP)');
557
+ }
558
+
559
+ if (transport === 'udp') {
560
+ this.getLogger().log(
561
+ `Connecting via UDP/BCUDP (battery detected, uid=${normalizedUid ? maskUid(normalizedUid) : 'missing'})`,
562
+ );
563
+ }
535
564
 
536
- // Se useUdp è impostato, usa direttamente UDP senza provare TCP
537
- if (useUdp) {
538
- this.getLogger().log(`Using UDP transport directly (useUdp=${useUdp})`);
539
565
  const api = await createBaichuanApi(
540
566
  {
541
567
  host: ipAddress,
542
568
  username,
543
569
  password,
544
- uid,
570
+ uid: normalizedUid,
545
571
  logger: this.console,
546
572
  ...(debugOptions ? { debugOptions } : {}),
547
573
  },
548
- 'udp',
574
+ transport,
549
575
  );
550
576
  await api.login();
551
577
  this.baichuanApi = api;
552
578
  this.connectionTime = Date.now();
553
579
  return api;
554
- }
580
+ })();
555
581
 
556
- // Altrimenti prova TCP con fallback a UDP
557
- const { api, transport } = await connectBaichuanWithTcpUdpFallback(
558
- {
559
- host: ipAddress,
560
- username,
561
- password,
562
- uid,
563
- logger: this.console,
564
- ...(debugOptions ? { debugOptions } : {}),
565
- },
566
- ({ uid: normalizedUid, uidMissing }) => {
567
- const uidMsg = !uidMissing && normalizedUid ? `UID ${maskUid(normalizedUid)}` : 'UID MISSING';
568
- if (!this.udpFallbackAlerted) {
569
- this.udpFallbackAlerted = true;
570
- this.log.a(
571
- `Baichuan TCP failed for camera ${this.name} (${ipAddress}). This appears to be a battery camera: UID is required and UDP/BCUDP will be used (${uidMsg}).`,
572
- );
573
- }
574
- },
575
- );
576
-
577
- // Se il fallback ha usato UDP, salva la setting per usare sempre UDP in futuro
578
- if (transport === 'udp') {
579
- await this.storageSettings.putSetting('useUdp', 'true');
580
- this.getLogger().log(`TCP fallback to UDP detected. Saving useUdp setting for future connections.`);
582
+ try {
583
+ return await this.ensureClientPromise;
584
+ }
585
+ finally {
586
+ // Allow future reconnects (e.g. after close/reset) and avoid pinning rejected promises.
587
+ this.ensureClientPromise = undefined;
581
588
  }
582
-
583
- this.baichuanApi = api;
584
- this.connectionTime = Date.now();
585
- return api;
586
589
  }
587
590
 
588
591
  private getBaichuanDebugOptions(): any | undefined {
@@ -591,7 +594,7 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
591
594
 
592
595
  const debugOptions: DebugOptions = {};
593
596
  // Only pass through Baichuan client debug flags.
594
- const clientKeys = new Set(['enabled', 'debugRtsp', 'traceStream', 'traceTalk', 'debugH264', 'debugParamSets']);
597
+ const clientKeys = new Set(['enabled', 'debugRtsp', 'traceStream', 'traceTalk', 'traceEvents', 'debugH264', 'debugParamSets']);
595
598
  for (const k of sel) {
596
599
  if (!clientKeys.has(k)) continue;
597
600
  debugOptions[k] = true;
@@ -600,17 +603,22 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
600
603
  }
601
604
 
602
605
  private async createStreamClient(): Promise<ReolinkBaichuanApi> {
603
- const { ipAddress, username, password, uid, useUdp } = this.storageSettings.values;
606
+ const { ipAddress, username, password, uid } = this.storageSettings.values;
604
607
  if (!ipAddress || !username || !password) {
605
608
  throw new Error('Missing camera credentials');
606
609
  }
607
610
 
608
- // Usa la setting useUdp per determinare il transport invece di ottenere dal client primario
609
- // Questo garantisce che lo stream client usi lo stesso transport del client principale
610
- const transport: BaichuanTransport = useUdp ? 'udp' : 'tcp';
611
-
612
- if (useUdp) {
613
- this.getLogger().log(`Creating stream client with UDP transport (useUdp=${useUdp})`);
611
+ const isBattery = this.hasBattery();
612
+ const transport: BaichuanTransport = isBattery ? 'udp' : 'tcp';
613
+ const normalizedUid = normalizeUid(uid);
614
+ if (transport === 'udp' && !normalizedUid) {
615
+ if (!this.uidMissingAlerted) {
616
+ this.uidMissingAlerted = true;
617
+ this.log.a(
618
+ `Battery camera ${this.name} (${ipAddress}) requires Reolink UID for UDP/BCUDP. Please set the UID in settings.`,
619
+ );
620
+ }
621
+ throw new Error('UID is required for battery cameras (BCUDP)');
614
622
  }
615
623
 
616
624
  const debugOptions = this.getBaichuanDebugOptions();
@@ -619,7 +627,7 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
619
627
  host: ipAddress,
620
628
  username,
621
629
  password,
622
- uid,
630
+ uid: normalizedUid,
623
631
  logger: this.console,
624
632
  ...(debugOptions ? { debugOptions } : {}),
625
633
  },
@@ -654,7 +662,7 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
654
662
 
655
663
 
656
664
  try {
657
- const interfaces = getDeviceInterfaces({
665
+ const { interfaces, type } = getDeviceInterfaces({
658
666
  capabilities,
659
667
  logger: this.console,
660
668
  });
@@ -664,7 +672,7 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
664
672
  providerNativeId: this.plugin.nativeId,
665
673
  name: this.name,
666
674
  interfaces,
667
- type: this.type as ScryptedDeviceType,
675
+ type,
668
676
  info: this.info,
669
677
  };
670
678
 
@@ -708,12 +716,13 @@ export class ReolinkNativeCamera extends ScryptedDeviceBase implements VideoCame
708
716
  motion = this.shouldDispatchMotion();
709
717
  break;
710
718
  case 'doorbell':
711
- this.binaryState = true;
712
- if (this.doorbellBinaryTimeout) clearTimeout(this.doorbellBinaryTimeout);
713
- this.doorbellBinaryTimeout = setTimeout(() => {
714
- this.binaryState = false;
715
- this.doorbellBinaryTimeout = undefined;
716
- }, 2000);
719
+ if (!this.doorbellBinaryTimeout) {
720
+ this.binaryState = true;
721
+ this.doorbellBinaryTimeout = setTimeout(() => {
722
+ this.binaryState = false;
723
+ this.doorbellBinaryTimeout = undefined;
724
+ }, 2000);
725
+ }
717
726
 
718
727
  motion = this.shouldDispatchMotion();
719
728
  break;
package/src/main.ts CHANGED
@@ -54,11 +54,10 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
54
54
  this.console.log(JSON.stringify({ abilities, capabilities, deviceInfo }));
55
55
 
56
56
  nativeId = deviceInfo.serialNumber;
57
- const type = capabilities.isDoorbell ? ScryptedDeviceType.DataSource : ScryptedDeviceType.Camera;
58
57
 
59
58
  settings.newCamera ||= name;
60
59
 
61
- const interfaces = getDeviceInterfaces({
60
+ const { interfaces, type } = getDeviceInterfaces({
62
61
  capabilities,
63
62
  logger: this.console,
64
63
  });
package/src/utils.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { DeviceCapabilities } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
- import { ScryptedInterface } from "@scrypted/sdk";
2
+ import { ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
3
3
 
4
4
  export const getDeviceInterfaces = (props: {
5
5
  capabilities: DeviceCapabilities,
@@ -48,5 +48,5 @@ export const getDeviceInterfaces = (props: {
48
48
  logger.error('Error getting device interfaces', e);
49
49
  }
50
50
 
51
- return interfaces;
51
+ return { interfaces, type: capabilities.isDoorbell ? ScryptedDeviceType.Doorbell : ScryptedDeviceType.Camera };
52
52
  }