@apocaliss92/scrypted-reolink-native 0.0.3 → 0.0.4

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/src/main.ts CHANGED
@@ -1,6 +1,7 @@
1
- import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting } from "@scrypted/sdk";
1
+ import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedNativeId, Setting } from "@scrypted/sdk";
2
2
  import { ReolinkNativeCamera } from "./camera";
3
3
  import { connectBaichuanWithTcpUdpFallback, maskUid } from "./connect";
4
+ import { getDeviceInterfaces } from "./utils";
4
5
 
5
6
  class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
6
7
  devices = new Map<string, ReolinkNativeCamera>();
@@ -9,17 +10,6 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
9
10
  return 'Reolink Native camera';
10
11
  }
11
12
 
12
- getCameraInterfaces() {
13
- return [
14
- ScryptedInterface.Reboot,
15
- ScryptedInterface.VideoCameraConfiguration,
16
- ScryptedInterface.Camera,
17
- ScryptedInterface.AudioSensor,
18
- ScryptedInterface.MotionSensor,
19
- ScryptedInterface.VideoTextOverlays,
20
- ];
21
- }
22
-
23
13
  async getDevice(nativeId: ScryptedNativeId): Promise<ReolinkNativeCamera> {
24
14
  if (this.devices.has(nativeId)) {
25
15
  return this.devices.get(nativeId);
@@ -59,19 +49,25 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
59
49
  const deviceInfo = await api.getInfo();
60
50
  const name = deviceInfo?.name;
61
51
  const rtspChannel = 0;
62
- const { abilities, capabilities } = await api.getDeviceCapabilities(rtspChannel);
52
+ const { abilities, capabilities } = await api.getDeviceCapabilities(rtspChannel, { probeAi: false });
63
53
 
64
54
  this.console.log(JSON.stringify({ abilities, capabilities, deviceInfo }));
65
55
 
66
56
  nativeId = deviceInfo.serialNumber;
57
+ const type = capabilities.isDoorbell ? ScryptedDeviceType.DataSource : ScryptedDeviceType.Camera;
67
58
 
68
59
  settings.newCamera ||= name;
69
60
 
61
+ const interfaces = getDeviceInterfaces({
62
+ capabilities,
63
+ logger: this.console,
64
+ });
65
+
70
66
  await sdk.deviceManager.onDeviceDiscovered({
71
67
  nativeId,
72
68
  name,
73
- interfaces: this.getCameraInterfaces(),
74
- type: ScryptedDeviceType.Camera,
69
+ interfaces,
70
+ type,
75
71
  providerNativeId: this.nativeId,
76
72
  });
77
73
 
package/src/presets.ts CHANGED
@@ -113,54 +113,39 @@ export class ReolinkPtzPresets {
113
113
  const existing = await client.getPtzPresets(channel);
114
114
  const existingIds = new Set(existing.map((p) => p.id));
115
115
 
116
- const maxAttempts = 5;
117
- let lastUpdated: PtzPreset[] = [];
118
- let chosenId: number | undefined;
119
-
120
- const initialId = presetId ?? this.nextFreePresetId(existing);
121
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
122
- const id = presetId ?? (initialId + attempt);
123
- if (!presetId && existingIds.has(id)) continue;
124
-
125
- await client.setPtzPreset(channel, id, trimmed);
116
+ const id = presetId ?? this.nextFreePresetId(existing);
117
+ if (presetId === undefined && existingIds.has(id)) {
118
+ // Should not happen because nextFreePresetId returns unused id.
119
+ throw new Error(`PTZ preset id already in use: ${id}`);
120
+ }
126
121
 
127
- const updated = await client.getPtzPresets(channel);
128
- lastUpdated = updated;
122
+ await client.setPtzPreset(channel, id, trimmed);
129
123
 
130
- const persisted = updated.some((p) => p.id === id);
131
- if (persisted) {
132
- chosenId = id;
133
- break;
134
- }
124
+ const updated = await client.getPtzPresets(channel);
125
+ const persisted = updated.some((p) => p.id === id);
126
+ this.setCachedPtzPresets(updated);
135
127
 
136
- // If the camera isn't exposing any presets at all, it may be keeping low IDs reserved/disabled.
137
- // Try the next ID.
138
- }
139
-
140
- if (chosenId === undefined) {
141
- this.setCachedPtzPresets(lastUpdated);
142
- this.syncEnabledPresetsSettingAndCaps(lastUpdated);
128
+ if (!persisted) {
129
+ this.syncEnabledPresetsSettingAndCaps(updated);
143
130
  throw new Error(
144
- `PTZ preset save did not persist (camera returned an empty/unchanged preset list). Try a different preset id.`
131
+ `PTZ preset save did not persist (camera returned an empty/unchanged preset list). Try again.`
145
132
  );
146
133
  }
147
134
 
148
- this.setCachedPtzPresets(lastUpdated);
149
-
150
135
  // If the "presets" setting is in use, auto-add the newly created preset so it becomes
151
136
  // immediately selectable/visible in the UI. If it's empty/unconfigured, don't start using it.
152
137
  const enabled = (this.storageSettings.values.presets ?? []) as string[];
153
138
  if (enabled.length) {
154
- const already = enabled.some((e) => this.parsePresetIdFromSettingValue(e) === chosenId);
139
+ const already = enabled.some((e) => this.parsePresetIdFromSettingValue(e) === id);
155
140
  if (!already) {
156
- enabled.push(`${chosenId}=${trimmed}`);
141
+ enabled.push(`${id}=${trimmed}`);
157
142
  this.storageSettings.values.presets = enabled;
158
143
  }
159
144
  }
160
145
 
161
146
  // Re-apply enabled mapping (so custom names/filters remain effective after setCachedPtzPresets).
162
- this.syncEnabledPresetsSettingAndCaps(lastUpdated);
163
- return { id: chosenId, name: trimmed };
147
+ this.syncEnabledPresetsSettingAndCaps(updated);
148
+ return { id, name: trimmed };
164
149
  }
165
150
 
166
151
  /** Overwrite an existing preset with the current PTZ position (and keep its current name). */
package/src/utils.ts CHANGED
@@ -1,60 +1,52 @@
1
-
2
- import MqttClient from '../../scrypted-apocaliss-base/src/mqtt-client';
3
-
4
- export const getMqttTopics = (cameraName: string) => {
5
- const statusTopic = `neolink/${cameraName}/status`;
6
- const batteryStatusTopic = `neolink/${cameraName}/status/battery_level`;
7
- const motionStatusTopic = `neolink/${cameraName}/status/motion`;
8
- const disconnecteStatusdTopic = `neolink/${cameraName}/status/disconnected`;
9
- const previewStatusTopic = `neolink/${cameraName}/status/preview`;
10
- const ptzPresetsStatusTopic = `neolink/${cameraName}/status/ptz/preset`;
11
-
12
- const batteryQueryTopic = `neolink/${cameraName}/query/battery`;
13
- const previewQueryTopic = `neolink/${cameraName}/query/preview`;
14
- const ptzPreviewQueryTopic = `neolink/${cameraName}/query/ptz/preset`;
15
-
16
- const ptzControlTopic = `neolink/${cameraName}/control/ptz`;
17
- const ptzPresetControlTopic = `neolink/${cameraName}/control/preset`;
18
- const floodlightControlTopic = `neolink/${cameraName}/control/floodlight`;
19
- const floodlightTasksControlTopic = `neolink/${cameraName}/control/floodlight_tasks`;
20
- const sirenControlTopic = `neolink/${cameraName}/control/siren`;
21
- const rebootControlTopic = `neolink/${cameraName}/control/reboot`;
22
- const ledControlTopic = `neolink/${cameraName}/control/led`;
23
- const irControlTopic = `neolink/${cameraName}/control/ir`;
24
- const pirControlTopic = `neolink/${cameraName}/control/pir`;
25
-
26
- return {
27
- statusTopic,
28
- batteryStatusTopic,
29
- motionStatusTopic,
30
- disconnecteStatusdTopic,
31
- ptzPresetsStatusTopic,
32
- previewStatusTopic,
33
- batteryQueryTopic,
34
- previewQueryTopic,
35
- ptzPreviewQueryTopic,
36
- ptzControlTopic,
37
- ptzPresetControlTopic,
38
- floodlightControlTopic,
39
- floodlightTasksControlTopic,
40
- sirenControlTopic,
41
- rebootControlTopic,
42
- ledControlTopic,
43
- irControlTopic,
44
- pirControlTopic,
45
- }
46
- }
47
-
48
- export const subscribeToNeolinkTopic = async (client: MqttClient, topic: string, console: Console, cb: (value?: any) => void) => {
49
- client.subscribe([topic], async (messageTopic, message) => {
50
- const messageString = message.toString();
51
- if (messageTopic === topic) {
52
- cb(messageString);
1
+ import type { DeviceCapabilities } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
+ import { ScryptedInterface } from "@scrypted/sdk";
3
+
4
+ export const getDeviceInterfaces = (props: {
5
+ capabilities: DeviceCapabilities,
6
+ logger: Console
7
+ }) => {
8
+ const { capabilities, logger } = props;
9
+
10
+ const interfaces = [
11
+ ScryptedInterface.VideoCamera,
12
+ ScryptedInterface.Settings,
13
+ ScryptedInterface.Reboot,
14
+ ScryptedInterface.VideoCameraConfiguration,
15
+ ScryptedInterface.Camera,
16
+ ScryptedInterface.AudioSensor,
17
+ ScryptedInterface.MotionSensor,
18
+ ScryptedInterface.VideoTextOverlays,
19
+ ];
20
+
21
+ try {
22
+ const {
23
+ hasPtz,
24
+ hasSiren,
25
+ hasFloodlight,
26
+ hasPir,
27
+ hasBattery,
28
+ hasIntercom,
29
+ isDoorbell,
30
+ } = capabilities;
31
+
32
+ if (hasPtz) {
33
+ interfaces.push(ScryptedInterface.PanTiltZoom);
53
34
  }
54
- });
55
-
56
- }
35
+ interfaces.push(ScryptedInterface.ObjectDetector);
36
+ if (hasSiren || hasFloodlight || hasPir)
37
+ interfaces.push(ScryptedInterface.DeviceProvider);
38
+ if (hasBattery) {
39
+ interfaces.push(ScryptedInterface.Battery, ScryptedInterface.Sleep);
40
+ }
41
+ if (hasIntercom) {
42
+ interfaces.push(ScryptedInterface.Intercom);
43
+ }
44
+ if (isDoorbell) {
45
+ interfaces.push(ScryptedInterface.BinarySensor);
46
+ }
47
+ } catch (e) {
48
+ logger.error('Error getting device interfaces', e);
49
+ }
57
50
 
58
- export const unsubscribeFromNeolinkTopic = async (client: MqttClient, topic: string, console: Console) => {
59
- client.unsubscribe([topic]);
51
+ return interfaces;
60
52
  }