Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reolink: add siren support #1506

Merged
merged 1 commit into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 97 additions & 3 deletions plugins/reolink/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { sleep } from '@scrypted/common/src/sleep';
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, Intercom, MediaObject, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, PanTiltZoom, PanTiltZoomCommand, PictureOptions, Reboot, RequestPictureOptions, ScryptedDeviceType, ScryptedInterface, Setting } from "@scrypted/sdk";
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, DeviceProvider, Device, Intercom, MediaObject, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, PictureOptions, Reboot, RequestPictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from "@scrypted/sdk";
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import { EventEmitter } from "stream";
import { Destroyable, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
Expand All @@ -8,12 +8,46 @@ import { listenEvents } from './onvif-events';
import { OnvifIntercom } from './onvif-intercom';
import { AIState, DevInfo, Enc, ReolinkCameraClient } from './reolink-api';

class ReolinkCamera extends RtspSmartCamera implements Camera, Reboot, Intercom, ObjectDetector, PanTiltZoom {
class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
intervalId: NodeJS.Timeout;

constructor(public camera: ReolinkCamera, nativeId: string) {
super(nativeId);
}

async turnOff() {
await this.setSiren(false);
}

async turnOn() {
await this.setSiren(true);
}

private async setSiren(on: boolean) {
// doorbell doesn't seem to support alarm_mode = 'manul', so let's pump the API every second and run the siren in timed mode.
if (this.camera.storageSettings.values.doorbell) {
if (!on) {
clearInterval(this.intervalId);
return;
}
this.intervalId = setInterval(async () => {
const api = this.camera.getClient();
await api.setSiren(on, 1);
}, 1000);
return;
}
const api = this.camera.getClient();
await api.setSiren(on);
}
}

class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, Reboot, Intercom, ObjectDetector, PanTiltZoom {
client: ReolinkCameraClient;
onvifClient: OnvifCameraAPI;
onvifIntercom = new OnvifIntercom(this);
videoStreamOptions: Promise<UrlMediaStreamOptions[]>;
motionTimeout: NodeJS.Timeout;
siren: ReolinkCameraSiren;

storageSettings = new StorageSettings(this, {
doorbell: {
Expand Down Expand Up @@ -55,6 +89,10 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, Reboot, Intercom,
json: true,
hide: true
},
abilities: {
json: true,
hide: true
},
useOnvifDetections: {
subgroup: 'Advanced',
title: 'Use ONVIF for Object Detection',
Expand Down Expand Up @@ -162,6 +200,9 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, Reboot, Intercom,
if (this.storageSettings.values.hasObjectDetector) {
interfaces.push(ScryptedInterface.ObjectDetector);
}
if (this.storageSettings.values.abilities?.Ability?.supportAudioAlarm?.ver !== 0) {
interfaces.push(ScryptedInterface.DeviceProvider);
}
await this.provider.updateDevice(this.nativeId, name, interfaces, type);
}

Expand Down Expand Up @@ -482,7 +523,6 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, Reboot, Intercom,
}

return streams;

}

async putSetting(key: string, value: string) {
Expand Down Expand Up @@ -511,6 +551,44 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, Reboot, Intercom,
getRtmpAddress() {
return `${this.getIPAddress()}:${this.storage.getItem('rtmpPort') || 1935}`;
}

createSiren() {
const sirenNativeId = `${this.nativeId}-siren`;
this.siren = new ReolinkCameraSiren(this, sirenNativeId);

const sirenDevice: Device = {
providerNativeId: this.nativeId,
name: 'Reolink Siren',
nativeId: sirenNativeId,
info: {
manufacturer: 'Reolink',
serialNumber: this.nativeId,
},
interfaces: [
ScryptedInterface.OnOff
],
type: ScryptedDeviceType.Siren,
};
sdk.deviceManager.onDevicesChanged({
providerNativeId: this.nativeId,
devices: [sirenDevice]
});

return sirenNativeId;
}

async getDevice(nativeId: string): Promise<any> {
if (nativeId.endsWith('-siren')) {
return this.siren;
}
throw new Error(`${nativeId} is unknown`);
}

async releaseDevice(id: string, nativeId: string) {
if (nativeId.endsWith('-siren')) {
delete this.siren;
}
}
}

class ReolinkProvider extends RtspProvider {
Expand All @@ -534,6 +612,7 @@ class ReolinkProvider extends RtspProvider {
let name: string = 'Reolink Camera';
let deviceInfo: DevInfo;
let ai;
let abilities;
const skipValidate = settings.skipValidate?.toString() === 'true';
const rtspChannel = parseInt(settings.rtspChannel?.toString()) || 0;
if (!skipValidate) {
Expand All @@ -551,6 +630,7 @@ class ReolinkProvider extends RtspProvider {
doorbell = deviceInfo.type === 'BELL';
name = deviceInfo.name ?? 'Reolink Camera';
ai = await api.getAiState();
abilities = await api.getAbility();
}
catch (e) {
this.console.error('Reolink camera does not support AI events', e);
Expand All @@ -566,11 +646,18 @@ class ReolinkProvider extends RtspProvider {
device.putSetting('password', password);
device.putSetting('doorbell', doorbell.toString())
device.storageSettings.values.deviceInfo = deviceInfo;
device.storageSettings.values.abilities = abilities;
device.storageSettings.values.hasObjectDetector = ai;
device.setIPAddress(settings.ip?.toString());
device.putSetting('rtspChannel', settings.rtspChannel?.toString());
device.setHttpPortOverride(settings.httpPort?.toString());
device.updateDeviceInfo();

if (abilities?.Ability?.supportAudioAlarm?.ver !== 0) {
const sirenNativeId = device.createSiren();
this.devices.set(sirenNativeId, device.siren);
}

return nativeId;
}

Expand Down Expand Up @@ -613,6 +700,13 @@ class ReolinkProvider extends RtspProvider {
}

createCamera(nativeId: string) {
if (nativeId.endsWith('-siren')) {
const camera = this.devices.get(nativeId.replace(/-siren/, '')) as ReolinkCamera;
if (!camera.siren) {
camera.siren = new ReolinkCameraSiren(camera, nativeId);
}
return camera.siren;
}
return new ReolinkCamera(nativeId, this);
}
}
Expand Down
68 changes: 68 additions & 0 deletions plugins/reolink/src/reolink-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export type AIState = {
channel: number;
};

export type SirenResponse = {
rspCode: number;
}

export class ReolinkCameraClient {
credential: AuthFetchCredentialState;

Expand Down Expand Up @@ -127,6 +131,23 @@ export class ReolinkCameraClient {
};
}

async getAbility() {
const url = new URL(`http://${this.host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'GetAbility');
params.set('channel', this.channelId.toString());
params.set('user', this.username);
params.set('password', this.password);
const response = await this.request({
url,
responseType: 'json',
});
return {
value: response.body?.[0]?.value || response.body?.value,
data: response.body,
};
}

async jpegSnapshot(timeout = 10000) {
const url = new URL(`http://${this.host}/cgi-bin/api.cgi`);
const params = url.searchParams;
Expand Down Expand Up @@ -247,4 +268,51 @@ export class ReolinkCameraClient {
await this.ptzOp(op);
}
}

async setSiren(on: boolean, duration?: number) {
const url = new URL(`http://${this.host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'AudioAlarmPlay');
params.set('user', this.username);
params.set('password', this.password);
const createReadable = (data: any) => {
const pt = new PassThrough();
pt.write(Buffer.from(JSON.stringify(data)));
pt.end();
return pt;
}

let alarmMode;
if (duration) {
alarmMode = {
alarm_mode: 'times',
times: duration
};
}
else {
alarmMode = {
alarm_mode: 'manul',
manual_switch: on? 1 : 0
};
}

const response = await this.request({
url,
method: 'POST',
responseType: 'json',
}, createReadable([
{
cmd: "AudioAlarmPlay",
action: 0,
param: {
channel: this.channelId,
...alarmMode
}
},
]));
return {
value: (response.body?.[0]?.value || response.body?.value) as SirenResponse,
data: response.body,
};
}
}