@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/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/camera.ts +76 -67
- package/src/main.ts +1 -2
- package/src/utils.ts +2 -2
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
528
|
+
this.ensureClientPromise = (async () => {
|
|
529
|
+
if (this.baichuanApi && this.baichuanApi.client.loggedIn) return this.baichuanApi;
|
|
529
530
|
|
|
530
|
-
|
|
531
|
-
await this.baichuanApi.close();
|
|
532
|
-
}
|
|
531
|
+
const { ipAddress, username, password, uid } = this.storageSettings.values;
|
|
533
532
|
|
|
534
|
-
|
|
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
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
|
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.
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
}
|