@apocaliss92/scrypted-reolink-native 0.1.42 → 0.2.1

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.
@@ -1,336 +0,0 @@
1
- import type { ReolinkBaichuanApi, SleepStatus } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
- import sdk, {
3
- type MediaObject,
4
- RequestPictureOptions,
5
- ResponsePictureOptions
6
- } from "@scrypted/sdk";
7
- import {
8
- CommonCameraMixin,
9
- } from "./common";
10
- import { DebugLogOption } from "./debug-options";
11
- import type ReolinkNativePlugin from "./main";
12
- import { ReolinkNativeNvrDevice } from "./nvr";
13
- import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
14
-
15
- export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
16
- doorbellBinaryTimeout?: NodeJS.Timeout;
17
- motionDetected: boolean = false;
18
- motionTimeout: NodeJS.Timeout | undefined;
19
- private periodicStarted = false;
20
- private sleepCheckTimer: NodeJS.Timeout | undefined;
21
- private batteryUpdateTimer: NodeJS.Timeout | undefined;
22
- private lastBatteryLevel: number | undefined;
23
- private batteryUpdateInProgress: boolean = false;
24
-
25
- constructor(
26
- nativeId: string,
27
- public plugin: ReolinkNativePlugin,
28
- nvrDevice?: ReolinkNativeNvrDevice,
29
- multiFocalDevice?: ReolinkNativeMultiFocalDevice
30
- ) {
31
- super(nativeId, plugin, {
32
- type: 'battery',
33
- nvrDevice,
34
- multiFocalDevice,
35
- });
36
- }
37
-
38
- async init(): Promise<void> {
39
- this.startPeriodicTasks();
40
- await this.alignAuxDevicesState();
41
- await this.updateBatteryInfo();
42
- }
43
-
44
- async release(): Promise<void> {
45
- this.stopPeriodicTasks();
46
- return this.resetBaichuanClient();
47
- }
48
-
49
- private stopPeriodicTasks(): void {
50
- if (this.sleepCheckTimer) {
51
- clearInterval(this.sleepCheckTimer);
52
- this.sleepCheckTimer = undefined;
53
- }
54
- if (this.batteryUpdateTimer) {
55
- clearInterval(this.batteryUpdateTimer);
56
- this.batteryUpdateTimer = undefined;
57
- }
58
- this.periodicStarted = false;
59
- }
60
-
61
- private startPeriodicTasks(): void {
62
- if (this.periodicStarted) return;
63
- const logger = this.getBaichuanLogger();
64
- this.periodicStarted = true;
65
-
66
- logger.log('Starting periodic tasks for battery camera');
67
-
68
- if (!this.nvrDevice && !this.multiFocalDevice) {
69
- this.sleepCheckTimer = setInterval(async () => {
70
- try {
71
- const api = this.baichuanApi;
72
- const channel = this.storageSettings.values.rtspChannel;
73
-
74
- if (!api) {
75
- if (!this.sleeping) {
76
- logger.log('Camera is sleeping: no active Baichuan client');
77
- this.sleeping = true;
78
- }
79
- return;
80
- }
81
-
82
- const sleepStatus = api.getSleepStatus({ channel });
83
- await this.updateSleepingState(sleepStatus);
84
- } catch (e) {
85
- logger.warn('Error checking sleeping state:', e);
86
- }
87
- }, 5_000);
88
- }
89
-
90
- // Update battery and snapshot every N minutes
91
- const { batteryUpdateIntervalMinutes = 10 } = this.storageSettings.values;
92
- const updateIntervalMs = batteryUpdateIntervalMinutes * 60_000;
93
- this.batteryUpdateTimer = setInterval(() => {
94
- this.updateBatteryAndSnapshot().catch(() => { });
95
- }, updateIntervalMs);
96
-
97
- logger.log(`Periodic tasks started: sleep check every 5s, battery update every ${batteryUpdateIntervalMinutes} minutes`);
98
- }
99
-
100
- async updateSleepingState(sleepStatus: SleepStatus): Promise<void> {
101
- try {
102
- if (this.isDebugEnabled()) {
103
- this.getBaichuanLogger().debug('getSleepStatus result:', JSON.stringify(sleepStatus));
104
- }
105
-
106
- if (sleepStatus.state === 'sleeping') {
107
- if (!this.sleeping) {
108
- this.getBaichuanLogger().log(`Camera is sleeping: ${sleepStatus.reason}`);
109
- this.sleeping = true;
110
- }
111
- } else if (sleepStatus.state === 'awake') {
112
- // Camera is awake
113
- const wasSleeping = this.sleeping;
114
- if (wasSleeping) {
115
- this.getBaichuanLogger().log(`Camera woke up: ${sleepStatus.reason}`);
116
- this.sleeping = false;
117
- }
118
-
119
- if (wasSleeping) {
120
- this.alignAuxDevicesState().catch(() => { });
121
- if (this.forceNewSnapshot) {
122
- this.takePicture().catch(() => { });
123
- }
124
- }
125
- } else {
126
- // Unknown state
127
- this.getBaichuanLogger().debug(`Sleep status unknown: ${sleepStatus.reason}`);
128
- }
129
- } catch (e) {
130
- // Silently ignore errors in sleep check to avoid spam
131
- this.getBaichuanLogger().debug('Error in updateSleepingState:', e);
132
- }
133
- }
134
-
135
- async updateOnlineState(isOnline: boolean): Promise<void> {
136
- try {
137
- if (this.isDebugEnabled()) {
138
- this.getBaichuanLogger().debug('updateOnlineState result:', isOnline);
139
- }
140
-
141
- if (isOnline !== this.online) {
142
- this.online = isOnline;
143
- }
144
- } catch (e) {
145
- // Silently ignore errors in sleep check to avoid spam
146
- this.getBaichuanLogger().debug('Error in updateOnlineState:', e);
147
- }
148
- }
149
-
150
- async checkRecordingAction(newBatteryLevel: number) {
151
- const nvrDeviceId = this.plugin.nvrDeviceId;
152
- if (nvrDeviceId && this.mixins.includes(nvrDeviceId)) {
153
- const logger = this.getBaichuanLogger();
154
-
155
- const settings = await this.thisDevice.getSettings();
156
- const isRecording = !settings.find(setting => setting.key === 'recording:privacyMode')?.value;
157
- const { lowThresholdBatteryRecording, highThresholdBatteryRecording } = this.storageSettings.values;
158
-
159
- if (isRecording && newBatteryLevel < lowThresholdBatteryRecording) {
160
- logger.log(`Recording is enabled, but battery level is below low threshold (${newBatteryLevel}% < ${lowThresholdBatteryRecording}%), disabling recording`);
161
- await this.thisDevice.putSetting('recording:privacyMode', true);
162
- } else if (!isRecording && newBatteryLevel > highThresholdBatteryRecording) {
163
- logger.log(`Recording is disabled, but battery level is above high threshold (${newBatteryLevel}% > ${highThresholdBatteryRecording}%), enabling recording`);
164
- await this.thisDevice.putSetting('recording:privacyMode', false);
165
- }
166
-
167
- }
168
- }
169
-
170
- async updateBatteryInfo() {
171
- const api = await this.ensureClient();
172
- const channel = this.storageSettings.values.rtspChannel;
173
-
174
- const batteryInfo = await api.getBatteryInfo(channel);
175
- if (this.isDebugEnabled()) {
176
- this.getBaichuanLogger().debug('getBatteryInfo result:', JSON.stringify(batteryInfo));
177
- }
178
-
179
- if (batteryInfo.batteryPercent !== undefined) {
180
- const oldLevel = this.lastBatteryLevel;
181
- this.batteryLevel = batteryInfo.batteryPercent;
182
- this.lastBatteryLevel = batteryInfo.batteryPercent;
183
-
184
- let shouldCheckRecordingAction = true;
185
-
186
- // Log only if battery level changed
187
- if (oldLevel !== undefined && oldLevel !== batteryInfo.batteryPercent) {
188
- if (batteryInfo.chargeStatus !== undefined) {
189
- // chargeStatus: "0"=charging, "1"=discharging, "2"=full
190
- const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
191
- this.getBaichuanLogger().log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}% (charging: ${charging})`);
192
- } else {
193
- this.getBaichuanLogger().log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}%`);
194
- }
195
- } else if (oldLevel === undefined) {
196
- // First time setting battery level
197
- if (batteryInfo.chargeStatus !== undefined) {
198
- const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
199
- this.getBaichuanLogger().log(`Battery level set: ${batteryInfo.batteryPercent}% (charging: ${charging})`);
200
- } else {
201
- this.getBaichuanLogger().log(`Battery level set: ${batteryInfo.batteryPercent}%`);
202
- }
203
- } else {
204
- shouldCheckRecordingAction = false;
205
- }
206
-
207
- if (shouldCheckRecordingAction) {
208
- await this.checkRecordingAction(batteryInfo.batteryPercent);
209
- }
210
- }
211
- }
212
-
213
- private async updateBatteryAndSnapshot(): Promise<void> {
214
- // Prevent multiple simultaneous calls
215
- if (this.batteryUpdateInProgress) {
216
- this.getBaichuanLogger().debug('Battery update already in progress, skipping');
217
- return;
218
- }
219
-
220
- this.batteryUpdateInProgress = true;
221
- try {
222
- const channel = this.storageSettings.values.rtspChannel;
223
- const updateIntervalMinutes = this.storageSettings.values.batteryUpdateIntervalMinutes ?? 10;
224
- this.getBaichuanLogger().log(`Force battery update interval started (every ${updateIntervalMinutes} minutes)`);
225
-
226
- // Ensure we have a client connection
227
- const api = await this.ensureClient();
228
- if (!api) {
229
- this.getBaichuanLogger().warn('Failed to ensure client connection for battery update');
230
- return;
231
- }
232
-
233
- // Check current sleep status
234
- let sleepStatus = api.getSleepStatus({ channel });
235
-
236
- // If camera is sleeping, wake it up
237
- if (sleepStatus.state === 'sleeping') {
238
- this.getBaichuanLogger().log('Camera is sleeping, waking up for periodic update...');
239
- try {
240
- await api.wakeUp(channel, { waitAfterWakeMs: 2000 });
241
- this.getBaichuanLogger().log('Wake command sent, waiting for camera to wake up...');
242
- } catch (wakeError) {
243
- this.getBaichuanLogger().warn('Failed to wake up camera:', wakeError);
244
- return;
245
- }
246
-
247
- // Poll until camera is awake (with timeout)
248
- const wakeTimeoutMs = 30000; // 30 seconds max
249
- const startWakePoll = Date.now();
250
- let awake = false;
251
-
252
- while (Date.now() - startWakePoll < wakeTimeoutMs) {
253
- await new Promise(resolve => setTimeout(resolve, 1000)); // Check every second
254
- sleepStatus = api.getSleepStatus({ channel });
255
- if (sleepStatus.state === 'awake') {
256
- awake = true;
257
- this.getBaichuanLogger().log('Camera is now awake');
258
- this.sleeping = false;
259
- break;
260
- }
261
- }
262
-
263
- if (!awake) {
264
- this.getBaichuanLogger().warn('Camera did not wake up within timeout, skipping update');
265
- return;
266
- }
267
- } else if (sleepStatus.state === 'awake') {
268
- this.sleeping = false;
269
- }
270
-
271
- // Now that camera is awake, update all states
272
- // 1. Update battery info
273
- try {
274
- await this.updateBatteryInfo();
275
- } catch (e) {
276
- this.getBaichuanLogger().warn('Failed to get battery info during periodic update:', e);
277
- }
278
-
279
- // 2. Align auxiliary devices state
280
- try {
281
- await this.alignAuxDevicesState();
282
- } catch (e) {
283
- this.getBaichuanLogger().warn('Failed to align auxiliary devices state:', e);
284
- }
285
-
286
- // 3. Update snapshot
287
- try {
288
- this.forceNewSnapshot = true;
289
- await this.takePicture();
290
- this.getBaichuanLogger().log('Snapshot updated during periodic update');
291
- } catch (snapshotError) {
292
- this.getBaichuanLogger().warn('Failed to update snapshot during periodic update:', snapshotError);
293
- }
294
- } catch (e) {
295
- this.getBaichuanLogger().warn('Failed to update battery and snapshot', e);
296
- } finally {
297
- this.batteryUpdateInProgress = false;
298
- }
299
- }
300
-
301
- async resetBaichuanClient(reason?: any): Promise<void> {
302
- try {
303
- this.unsubscribedToEvents?.();
304
-
305
- // Close all stream servers before closing the main connection
306
- // This ensures streams are properly cleaned up when using shared connection
307
- if (this.streamManager) {
308
- const reasonStr = reason?.message || reason?.toString?.() || 'connection reset';
309
- await this.streamManager.closeAllStreams(reasonStr);
310
- }
311
-
312
- await this.baichuanApi?.close();
313
- }
314
- catch (e) {
315
- this.getBaichuanLogger().warn('Error closing Baichuan client during reset', e);
316
- }
317
- finally {
318
- this.baichuanApi = undefined;
319
- this.connectionTime = undefined;
320
- this.ensureClientPromise = undefined;
321
- if (this.sleepCheckTimer) {
322
- clearInterval(this.sleepCheckTimer);
323
- this.sleepCheckTimer = undefined;
324
- }
325
- if (this.batteryUpdateTimer) {
326
- clearInterval(this.batteryUpdateTimer);
327
- this.batteryUpdateTimer = undefined;
328
- }
329
- }
330
-
331
- if (reason) {
332
- const message = reason?.message || reason?.toString?.() || reason;
333
- this.getBaichuanLogger().warn(`Baichuan client reset requested: ${message}`);
334
- }
335
- }
336
- }