diff --git a/plugins/reolink/package.json b/plugins/reolink/package.json index c8ea0c420..f1e3a7232 100644 --- a/plugins/reolink/package.json +++ b/plugins/reolink/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/reolink", - "version": "0.0.96", + "version": "0.0.97", "description": "Reolink Plugin for Scrypted", "author": "Scrypted", "license": "Apache", diff --git a/plugins/reolink/src/main.ts b/plugins/reolink/src/main.ts index cee282bcd..edb0bade2 100644 --- a/plugins/reolink/src/main.ts +++ b/plugins/reolink/src/main.ts @@ -94,6 +94,35 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R this.updatePtzCaps(); }, }, + presets: { + subgroup: 'Advanced', + title: 'Presets', + description: 'PTZ Presets in the format "id=name". Where id is the PTZ Preset identifier and name is a friendly name.', + multiple: true, + defaultValue: [], + combobox: true, + onPut: async (ov, presets: string[]) => { + const caps = { + ...this.ptzCapabilities, + presets: {}, + }; + for (const preset of presets) { + const [key, name] = preset.split('='); + caps.presets[key] = name; + } + this.ptzCapabilities = caps; + }, + mapGet: () => { + const presets = this.ptzCapabilities?.presets || {}; + return Object.entries(presets).map(([key, name]) => key + '=' + name); + }, + }, + cachedPresets: { + multiple: true, + hide: true, + json: true, + defaultValue: [], + }, deviceInfo: { json: true, hide: true @@ -134,9 +163,21 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R } }; + this.storageSettings.settings.presets.onGet = async () => { + const choices = this.storageSettings.values.cachedPresets.map((preset) => preset.id + '=' + preset.name); + return { + choices, + }; + }; + this.updateDeviceInfo(); (async () => { this.updatePtzCaps(); + try { + await this.getPresets(); + } catch (e) { + this.console.log('Fail fetching presets', e); + } const api = this.getClient(); const deviceInfo = await api.getDeviceInfo(); this.storageSettings.values.deviceInfo = deviceInfo; @@ -160,12 +201,20 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R updatePtzCaps() { const { ptz } = this.storageSettings.values; this.ptzCapabilities = { + ...this.ptzCapabilities, pan: ptz?.includes('Pan'), tilt: ptz?.includes('Tilt'), zoom: ptz?.includes('Zoom'), } } + async getPresets() { + const client = this.getClient(); + const ptzPresets = await client.getPtzPresets(); + this.console.log(`Presets: ${JSON.stringify(ptzPresets)}`) + this.storageSettings.values.cachedPresets = ptzPresets; + } + async updateAbilities() { const api = this.getClient(); const abilities = await api.getAbility(); diff --git a/plugins/reolink/src/reolink-api.ts b/plugins/reolink/src/reolink-api.ts index cc8ba1abc..4d1c41297 100644 --- a/plugins/reolink/src/reolink-api.ts +++ b/plugins/reolink/src/reolink-api.ts @@ -39,6 +39,11 @@ export type SirenResponse = { rspCode: number; } +export interface PtzPreset { + id: number; + name: string; +} + export class ReolinkCameraClient { credential: AuthFetchCredentialState; parameters: Record; @@ -61,6 +66,13 @@ export class ReolinkCameraClient { return response; } + private createReadable = (data: any) => { + const pt = new PassThrough(); + pt.write(Buffer.from(JSON.stringify(data))); + pt.end(); + return pt; + } + async login() { if (this.tokenLease > Date.now()) { return; @@ -201,23 +213,37 @@ export class ReolinkCameraClient { return response.body?.[0]?.value?.DevInfo; } - private async ptzOp(op: string, speed: number) { + async getPtzPresets(): Promise { + const url = new URL(`http://${this.host}/api.cgi`); + const params = url.searchParams; + params.set('cmd', 'GetPtzPreset'); + const body = [ + { + cmd: "GetPtzPreset", + action: 1, + param: { + channel: this.channelId + } + } + ]; + const response = await this.requestWithLogin({ + url, + responseType: 'json', + method: 'POST' + }, this.createReadable(body)); + return response.body?.[0]?.value?.PtzPreset?.filter(preset => preset.enable === 1); + } + + private async ptzOp(op: string, speed: number, id?: number) { const url = new URL(`http://${this.host}/api.cgi`); const params = url.searchParams; params.set('cmd', 'PtzCtrl'); - const createReadable = (data: any) => { - const pt = new PassThrough(); - pt.write(Buffer.from(JSON.stringify(data))); - pt.end(); - return pt; - } - const c1 = this.requestWithLogin({ url, method: 'POST', responseType: 'text', - }, createReadable([ + }, this.createReadable([ { cmd: "PtzCtrl", param: { @@ -225,6 +251,7 @@ export class ReolinkCameraClient { op, speed, timeout: 1, + id } }, ])); @@ -234,7 +261,7 @@ export class ReolinkCameraClient { const c2 = this.requestWithLogin({ url, method: 'POST', - }, createReadable([ + }, this.createReadable([ { cmd: "PtzCtrl", param: { @@ -248,10 +275,37 @@ export class ReolinkCameraClient { this.console.log(await c2); } + private async presetOp(speed: number, id: number) { + const url = new URL(`http://${this.host}/api.cgi`); + const params = url.searchParams; + params.set('cmd', 'PtzCtrl'); + + const c1 = this.requestWithLogin({ + url, + method: 'POST', + responseType: 'text', + }, this.createReadable([ + { + cmd: "PtzCtrl", + param: { + channel: this.channelId, + op: 'ToPos', + speed, + id + } + }, + ])); + } + async ptz(command: PanTiltZoomCommand) { // reolink doesnt accept signed values to ptz // in favor of explicit direction. // so we need to convert the signed values to abs explicit direction. + if (command.preset && !Number.isNaN(Number(command.preset))) { + await this.presetOp(1, Number(command.preset)); + return; + } + let op = ''; if (command.pan < 0) op += 'Left'; @@ -263,7 +317,7 @@ export class ReolinkCameraClient { op += 'Up'; if (op) { - await this.ptzOp(op, Math.round(Math.abs(command?.pan || command?.tilt || 1) * 10)); + await this.ptzOp(op, Math.ceil(Math.abs(command?.pan || command?.tilt || 1) * 10)); } op = undefined; @@ -273,7 +327,7 @@ export class ReolinkCameraClient { op = 'ZoomInc'; if (op) { - await this.ptzOp(op, Math.round(Math.abs(command?.zoom || 1) * 10)); + await this.ptzOp(op, Math.ceil(Math.abs(command?.zoom || 1) * 10)); } } @@ -281,12 +335,6 @@ export class ReolinkCameraClient { const url = new URL(`http://${this.host}/api.cgi`); const params = url.searchParams; params.set('cmd', 'AudioAlarmPlay'); - const createReadable = (data: any) => { - const pt = new PassThrough(); - pt.write(Buffer.from(JSON.stringify(data))); - pt.end(); - return pt; - } let alarmMode; if (duration) { @@ -306,7 +354,7 @@ export class ReolinkCameraClient { url, method: 'POST', responseType: 'json', - }, createReadable([ + }, this.createReadable([ { cmd: "AudioAlarmPlay", action: 0,