mirror of
https://github.com/koush/scrypted.git
synced 2026-06-12 05:40:24 +01:00
Merge branch 'main' of github.com:koush/scrypted
This commit is contained in:
@@ -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";
|
||||
@@ -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: {
|
||||
@@ -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',
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -482,7 +523,6 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, Reboot, Intercom,
|
||||
}
|
||||
|
||||
return streams;
|
||||
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: string) {
|
||||
@@ -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 {
|
||||
@@ -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) {
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,10 @@ export type AIState = {
|
||||
channel: number;
|
||||
};
|
||||
|
||||
export type SirenResponse = {
|
||||
rspCode: number;
|
||||
}
|
||||
|
||||
export class ReolinkCameraClient {
|
||||
credential: AuthFetchCredentialState;
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user