Merge branch 'main' of github.com:koush/scrypted

This commit is contained in:
Koushik Dutta
2024-10-09 15:15:56 -07:00
3 changed files with 117 additions and 20 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/reolink",
"version": "0.0.96",
"version": "0.0.97",
"description": "Reolink Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -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();

View File

@@ -39,6 +39,11 @@ export type SirenResponse = {
rspCode: number;
}
export interface PtzPreset {
id: number;
name: string;
}
export class ReolinkCameraClient {
credential: AuthFetchCredentialState;
parameters: Record<string, string>;
@@ -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<PtzPreset[]> {
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,