mirror of
https://github.com/koush/scrypted.git
synced 2026-02-03 06:03:27 +00:00
Merge branch 'main' of github.com:koush/scrypted
This commit is contained in:
@@ -15,6 +15,8 @@ class ClipEmbedding(PredictPlugin, scrypted_sdk.TextEmbedding, scrypted_sdk.Imag
|
||||
def __init__(self, plugin: PredictPlugin, nativeId: str):
|
||||
super().__init__(nativeId=nativeId, plugin=plugin)
|
||||
|
||||
hf_id = "openai/clip-vit-base-patch32"
|
||||
|
||||
self.inputwidth = 224
|
||||
self.inputheight = 224
|
||||
|
||||
@@ -23,10 +25,35 @@ class ClipEmbedding(PredictPlugin, scrypted_sdk.TextEmbedding, scrypted_sdk.Imag
|
||||
self.minThreshold = 0.5
|
||||
|
||||
self.model = self.initModel()
|
||||
self.processor = CLIPProcessor.from_pretrained(
|
||||
"openai/clip-vit-base-patch32",
|
||||
cache_dir=os.path.join(os.environ["SCRYPTED_PLUGIN_VOLUME"], "files", "hf"),
|
||||
)
|
||||
cache_dir = os.path.join(os.environ["SCRYPTED_PLUGIN_VOLUME"], "files", "hf")
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
|
||||
self.processor = None
|
||||
print("Loading CLIP processor from local cache.")
|
||||
try:
|
||||
self.processor = CLIPProcessor.from_pretrained(
|
||||
hf_id,
|
||||
cache_dir=cache_dir,
|
||||
local_files_only=True,
|
||||
)
|
||||
print("Loaded CLIP processor from local cache.")
|
||||
except Exception:
|
||||
print("CLIP processor not available in local cache yet.")
|
||||
|
||||
asyncio.ensure_future(self.refreshClipProcessor(hf_id, cache_dir), loop=self.loop)
|
||||
|
||||
async def refreshClipProcessor(self, hf_id: str, cache_dir: str):
|
||||
try:
|
||||
print("Refreshing CLIP processor cache (online).")
|
||||
processor = await asyncio.to_thread(
|
||||
CLIPProcessor.from_pretrained,
|
||||
hf_id,
|
||||
cache_dir=cache_dir,
|
||||
)
|
||||
self.processor = processor
|
||||
print("Refreshed CLIP processor cache.")
|
||||
except Exception:
|
||||
print("CLIP processor cache refresh failed.")
|
||||
|
||||
def getFiles(self):
|
||||
pass
|
||||
|
||||
@@ -8,6 +8,8 @@ import { listenEvents } from './onvif-events';
|
||||
import { OnvifIntercom } from './onvif-intercom';
|
||||
import { DevInfo } from './probe';
|
||||
import { AIState, Enc, isDeviceHomeHub, isDeviceNvr, ReolinkCameraClient } from './reolink-api';
|
||||
import { ReolinkNvrDevice } from './nvr/nvr';
|
||||
import { ReolinkNvrClient } from './nvr/api';
|
||||
|
||||
class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
|
||||
sirenTimeout: NodeJS.Timeout;
|
||||
@@ -1134,8 +1136,10 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
}
|
||||
|
||||
class ReolinkProvider extends RtspProvider {
|
||||
nvrDevices = new Map<string, ReolinkNvrDevice>();
|
||||
|
||||
getScryptedDeviceCreator(): string {
|
||||
return 'Reolink Camera';
|
||||
return 'Reolink Camera/NVR';
|
||||
}
|
||||
|
||||
getAdditionalInterfaces() {
|
||||
@@ -1149,10 +1153,31 @@ class ReolinkProvider extends RtspProvider {
|
||||
];
|
||||
}
|
||||
|
||||
getDevice(nativeId: string) {
|
||||
if (nativeId.endsWith('-reolink-nvr')) {
|
||||
let ret = this.nvrDevices.get(nativeId);
|
||||
if (!ret) {
|
||||
ret = new ReolinkNvrDevice(nativeId, this);
|
||||
if (ret)
|
||||
this.nvrDevices.set(nativeId, ret);
|
||||
}
|
||||
|
||||
return ret;
|
||||
} else {
|
||||
return super.getDevice(nativeId);
|
||||
}
|
||||
}
|
||||
|
||||
async createDevice(settings: DeviceCreatorSettings, nativeId?: string): Promise<string> {
|
||||
const httpAddress = `${settings.ip}:${settings.httpPort || 80}`;
|
||||
let info: DeviceInformation = {};
|
||||
|
||||
const isNvr = settings.isNvr?.toString() === 'true';
|
||||
|
||||
if (isNvr) {
|
||||
return this.createNvrDeviceFromSettings(settings);
|
||||
}
|
||||
|
||||
const skipValidate = settings.skipValidate?.toString() === 'true';
|
||||
const username = settings.username?.toString();
|
||||
const password = settings.password?.toString();
|
||||
@@ -1224,6 +1249,12 @@ class ReolinkProvider extends RtspProvider {
|
||||
title: 'IP Address',
|
||||
placeholder: '192.168.2.222',
|
||||
},
|
||||
{
|
||||
key: 'isNvr',
|
||||
title: 'Is NVR',
|
||||
description: 'Set if adding a Reolink NVR device. This will allow adding cameras connected to the NVR.',
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
subgroup: 'Advanced',
|
||||
key: 'rtspChannel',
|
||||
@@ -1252,6 +1283,49 @@ class ReolinkProvider extends RtspProvider {
|
||||
createCamera(nativeId: string) {
|
||||
return new ReolinkCamera(nativeId, this);
|
||||
}
|
||||
|
||||
async createNvrDeviceFromSettings(settings: DeviceCreatorSettings) {
|
||||
const username = settings.username?.toString();
|
||||
const password = settings.password?.toString();
|
||||
const ip = settings.ip?.toString();
|
||||
const httpPort = settings.httpPort;
|
||||
const rtspPort = settings.rtspPort;
|
||||
const httpAddress = `${ip}:${httpPort || 80}`;
|
||||
|
||||
const client = new ReolinkNvrClient(httpAddress, username, password, this.console);
|
||||
const { devInfo } = await client.getHubInfo();
|
||||
|
||||
if (!devInfo) {
|
||||
throw new Error('Unable to connect to Reolink NVR. Please verify the IP address, port, username, and password are correct.');
|
||||
}
|
||||
|
||||
const { detail, name } = devInfo;
|
||||
const nativeId = `${detail}-reolink-nvr`;
|
||||
|
||||
await sdk.deviceManager.onDeviceDiscovered({
|
||||
nativeId,
|
||||
name,
|
||||
interfaces: [
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.DeviceDiscovery,
|
||||
ScryptedInterface.DeviceProvider,
|
||||
ScryptedInterface.Reboot,
|
||||
],
|
||||
type: ScryptedDeviceType.API,
|
||||
});
|
||||
|
||||
const nvrDevice = this.getDevice(nativeId);
|
||||
|
||||
nvrDevice.storageSettings.values.ipAddress = ip;
|
||||
nvrDevice.storageSettings.values.username = username;
|
||||
nvrDevice.storageSettings.values.password = password;
|
||||
nvrDevice.storageSettings.values.httpPort = httpPort;
|
||||
nvrDevice.storageSettings.values.rtspPort = rtspPort;
|
||||
|
||||
nvrDevice.updateDeviceInfo(devInfo);
|
||||
|
||||
return nativeId;
|
||||
}
|
||||
}
|
||||
|
||||
export default ReolinkProvider;
|
||||
|
||||
1126
plugins/reolink/src/nvr/api.ts
Normal file
1126
plugins/reolink/src/nvr/api.ts
Normal file
File diff suppressed because it is too large
Load Diff
885
plugins/reolink/src/nvr/camera.ts
Normal file
885
plugins/reolink/src/nvr/camera.ts
Normal file
@@ -0,0 +1,885 @@
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
import sdk, { Brightness, Camera, Device, DeviceProvider, Intercom, MediaObject, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, RequestPictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, Sleep, VideoTextOverlay, VideoTextOverlays } from "@scrypted/sdk";
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
import { createRtspMediaStreamOptions, RtspSmartCamera, UrlMediaStreamOptions } from "../../../rtsp/src/rtsp";
|
||||
import { connectCameraAPI, OnvifCameraAPI } from '../onvif-api';
|
||||
import { OnvifIntercom } from '../onvif-intercom';
|
||||
import { AIState, BatteryInfoResponse, DeviceStatusResponse, Enc, EventsResponse } from './api';
|
||||
import { ReolinkNvrDevice } from './nvr';
|
||||
|
||||
export const moToB64 = async (mo: MediaObject) => {
|
||||
const bufferImage = await sdk.mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
|
||||
return bufferImage?.toString('base64');
|
||||
}
|
||||
|
||||
export const b64ToMo = async (b64: string) => {
|
||||
const buffer = Buffer.from(b64, 'base64');
|
||||
return await sdk.mediaManager.createMediaObject(buffer, 'image/jpeg');
|
||||
}
|
||||
|
||||
class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
|
||||
sirenTimeout: NodeJS.Timeout;
|
||||
|
||||
constructor(public camera: ReolinkNvrCamera, nativeId: string) {
|
||||
super(nativeId);
|
||||
}
|
||||
|
||||
async turnOff() {
|
||||
this.on = false;
|
||||
await this.setSiren(false);
|
||||
}
|
||||
|
||||
async turnOn() {
|
||||
this.on = true;
|
||||
await this.setSiren(true);
|
||||
}
|
||||
|
||||
private async setSiren(on: boolean) {
|
||||
const api = this.camera.getClient();
|
||||
|
||||
await api.setSiren(this.camera.getRtspChannel(), on);
|
||||
}
|
||||
}
|
||||
|
||||
class ReolinkCameraFloodlight extends ScryptedDeviceBase implements OnOff, Brightness {
|
||||
constructor(public camera: ReolinkNvrCamera, nativeId: string) {
|
||||
super(nativeId);
|
||||
}
|
||||
|
||||
async setBrightness(brightness: number): Promise<void> {
|
||||
this.brightness = brightness;
|
||||
await this.setFloodlight(undefined, brightness);
|
||||
}
|
||||
|
||||
async turnOff() {
|
||||
this.on = false;
|
||||
await this.setFloodlight(false);
|
||||
}
|
||||
|
||||
async turnOn() {
|
||||
this.on = true;
|
||||
await this.setFloodlight(true);
|
||||
}
|
||||
|
||||
private async setFloodlight(on?: boolean, brightness?: number) {
|
||||
const api = this.camera.getClient();
|
||||
|
||||
await api.setWhiteLedState(this.camera.getRtspChannel(), on, brightness);
|
||||
}
|
||||
}
|
||||
|
||||
class ReolinkCameraPirSensor extends ScryptedDeviceBase implements OnOff {
|
||||
constructor(public camera: ReolinkNvrCamera, nativeId: string) {
|
||||
super(nativeId);
|
||||
}
|
||||
|
||||
async turnOff() {
|
||||
this.on = false;
|
||||
await this.setPir(false);
|
||||
}
|
||||
|
||||
async turnOn() {
|
||||
this.on = true;
|
||||
await this.setPir(true);
|
||||
}
|
||||
|
||||
private async setPir(on: boolean) {
|
||||
const api = this.camera.getClient();
|
||||
|
||||
await api.setPirState(this.camera.getRtspChannel(), on);
|
||||
}
|
||||
}
|
||||
|
||||
export class ReolinkNvrCamera extends RtspSmartCamera implements Camera, DeviceProvider, Intercom, ObjectDetector, PanTiltZoom, Sleep, VideoTextOverlays {
|
||||
onvifClient: OnvifCameraAPI;
|
||||
onvifIntercom = new OnvifIntercom(this);
|
||||
videoStreamOptions: Promise<UrlMediaStreamOptions[]>;
|
||||
motionTimeout: NodeJS.Timeout;
|
||||
siren: ReolinkCameraSiren;
|
||||
floodlight: ReolinkCameraFloodlight;
|
||||
pirSensor: ReolinkCameraPirSensor;
|
||||
lastB64Snapshot: string;
|
||||
lastSnapshotTaken: number;
|
||||
nvrDevice: ReolinkNvrDevice;
|
||||
abilities: any;
|
||||
|
||||
storageSettings = new StorageSettings(this, {
|
||||
debugEvents: {
|
||||
title: 'Debug Events',
|
||||
type: 'boolean',
|
||||
immediate: true,
|
||||
},
|
||||
rtspChannel: {
|
||||
subgroup: 'Advanced',
|
||||
title: 'Channel',
|
||||
type: 'number',
|
||||
hide: true,
|
||||
},
|
||||
motionTimeout: {
|
||||
subgroup: 'Advanced',
|
||||
title: 'Motion Timeout',
|
||||
defaultValue: 20,
|
||||
type: 'number',
|
||||
},
|
||||
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: [],
|
||||
},
|
||||
cachedOsd: {
|
||||
multiple: true,
|
||||
hide: true,
|
||||
json: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
useOnvifTwoWayAudio: {
|
||||
subgroup: 'Advanced',
|
||||
title: 'Use ONVIF for Two-Way Audio',
|
||||
type: 'boolean',
|
||||
},
|
||||
prebufferSet: {
|
||||
type: 'boolean',
|
||||
hide: true
|
||||
}
|
||||
});
|
||||
|
||||
constructor(nativeId: string, nvrDevice: ReolinkNvrDevice) {
|
||||
super(nativeId, nvrDevice.plugin);
|
||||
this.nvrDevice = nvrDevice;
|
||||
|
||||
this.storageSettings.settings.presets.onGet = async () => {
|
||||
const choices = this.storageSettings.values.cachedPresets.map((preset) => preset.id + '=' + preset.name);
|
||||
return {
|
||||
choices,
|
||||
};
|
||||
};
|
||||
|
||||
const channel = Number(this.storageSettings.values.rtspChannel);
|
||||
if (!Number.isNaN(channel)) {
|
||||
this.nvrDevice.cameraNativeMap.set(this.nativeId, this);
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
await this.init();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
public getLogger() {
|
||||
return this.console;
|
||||
}
|
||||
|
||||
async init() {
|
||||
const logger = this.getLogger();
|
||||
|
||||
while (!this.nvrDevice.client || !this.nvrDevice.client.loggedIn) {
|
||||
logger.log('Waiting for plugin connection');
|
||||
await sleep(3000);
|
||||
}
|
||||
|
||||
await this.reportDevices();
|
||||
this.updateDeviceInfo();
|
||||
this.updatePtzCaps();
|
||||
|
||||
const interfaces = await this.getDeviceInterfaces();
|
||||
|
||||
const device = {
|
||||
nativeId: this.nativeId,
|
||||
providerNativeId: this.nvrDevice.nativeId,
|
||||
name: this.name,
|
||||
interfaces,
|
||||
type: this.type,
|
||||
info: this.info,
|
||||
};
|
||||
|
||||
logger.log(`Updating device interfaces: ${JSON.stringify(interfaces)}`);
|
||||
|
||||
await sdk.deviceManager.onDeviceDiscovered(device);
|
||||
// await this.nvrDevice.plugin.updateDevice(this.nativeId, this.name, interfaces, ScryptedDeviceType.Camera);
|
||||
|
||||
if (this.hasBattery() && !this.storageSettings.getItem('prebufferSet')) {
|
||||
const device = sdk.systemManager.getDeviceById<Settings>(this.id);
|
||||
logger.log('Disabling prebbufer for battery cam');
|
||||
await device.putSetting('prebuffer:enabledStreams', '[]');
|
||||
this.storageSettings.values.prebufferSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
getClient() {
|
||||
return this.nvrDevice.getClient();
|
||||
}
|
||||
|
||||
async getVideoTextOverlays(): Promise<Record<string, VideoTextOverlay>> {
|
||||
const client = this.getClient();
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
const { cachedOsd } = this.storageSettings.values;
|
||||
|
||||
return {
|
||||
osdChannel: {
|
||||
text: cachedOsd.value.Osd.osdChannel.enable ? cachedOsd.value.Osd.osdChannel.name : undefined,
|
||||
},
|
||||
osdTime: {
|
||||
text: !!cachedOsd.value.Osd.osdTime.enable,
|
||||
readonly: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async setVideoTextOverlay(id: 'osdChannel' | 'osdTime', value: VideoTextOverlay): Promise<void> {
|
||||
const client = this.getClient();
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
const osd = await client.getOsd(this.getRtspChannel());
|
||||
if (id === 'osdChannel') {
|
||||
const osdValue = osd.value.Osd.osdChannel;
|
||||
osdValue.enable = value.text ? 1 : 0;
|
||||
// name must always be valid.
|
||||
osdValue.name = typeof value.text === 'string' && value.text
|
||||
? value.text
|
||||
: osdValue.name || 'Camera';
|
||||
}
|
||||
else if (id === 'osdTime') {
|
||||
const osdValue = osd.value.Osd.osdTime;
|
||||
osdValue.enable = value.text ? 1 : 0;
|
||||
}
|
||||
else {
|
||||
throw new Error('unknown overlay: ' + id);
|
||||
}
|
||||
|
||||
await client.setOsd(this.getRtspChannel(), osd);
|
||||
}
|
||||
|
||||
updatePtzCaps() {
|
||||
const { hasPanTilt, hasZoom } = this.getPtzCapabilities();
|
||||
this.ptzCapabilities = {
|
||||
...this.ptzCapabilities,
|
||||
pan: hasPanTilt,
|
||||
tilt: hasPanTilt,
|
||||
zoom: hasZoom,
|
||||
}
|
||||
}
|
||||
|
||||
getAbilities() {
|
||||
if (!this.abilities) {
|
||||
const channel = Number(this.getRtspChannel());
|
||||
this.abilities = this.nvrDevice.storageSettings.values.abilities?.Ability?.abilityChn?.[channel];
|
||||
}
|
||||
|
||||
return this.abilities;
|
||||
}
|
||||
|
||||
getEncoderSettings() {
|
||||
return this.getDeviceData()?.enc?.Enc;
|
||||
}
|
||||
|
||||
async getDetectionInput(detectionId: string, eventId?: any): Promise<MediaObject> {
|
||||
return;
|
||||
}
|
||||
|
||||
async ptzCommand(command: PanTiltZoomCommand): Promise<void> {
|
||||
const client = this.getClient();
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
client.ptz(this.getRtspChannel(), command);
|
||||
}
|
||||
|
||||
getDeviceData() {
|
||||
const channel = this.getRtspChannel();
|
||||
return this.nvrDevice.storageSettings.values.devicesData?.[channel];
|
||||
}
|
||||
|
||||
async getObjectTypes(): Promise<ObjectDetectionTypes> {
|
||||
try {
|
||||
const deviceData = this.getDeviceData();
|
||||
const ai: AIState = deviceData?.ai;
|
||||
const classes: string[] = [];
|
||||
|
||||
for (const key of Object.keys(ai ?? {})) {
|
||||
if (key === 'channel')
|
||||
continue;
|
||||
const { alarm_state, support } = ai[key];
|
||||
if (support)
|
||||
classes.push(key);
|
||||
}
|
||||
return {
|
||||
classes,
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
return {
|
||||
classes: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async startIntercom(media: MediaObject): Promise<void> {
|
||||
if (!this.onvifIntercom.url) {
|
||||
const client = await this.getOnvifClient();
|
||||
const streamUrl = await client.getStreamUrl();
|
||||
this.onvifIntercom.url = streamUrl;
|
||||
}
|
||||
return this.onvifIntercom.startIntercom(media);
|
||||
}
|
||||
|
||||
stopIntercom(): Promise<void> {
|
||||
return this.onvifIntercom.stopIntercom();
|
||||
}
|
||||
|
||||
hasSiren() {
|
||||
const abilities = this.getAbilities();
|
||||
const hasAbility = abilities?.supportAudioAlarm;
|
||||
|
||||
return (hasAbility && hasAbility?.ver !== 0);
|
||||
}
|
||||
|
||||
hasFloodlight() {
|
||||
const channelData = this.getAbilities();
|
||||
|
||||
const floodLightConfigVer = channelData?.floodLight?.ver ?? 0;
|
||||
const supportFLswitchConfigVer = channelData?.supportFLswitch?.ver ?? 0;
|
||||
const supportFLBrightnessConfigVer = channelData?.supportFLBrightness?.ver ?? 0;
|
||||
|
||||
return floodLightConfigVer > 0 || supportFLswitchConfigVer > 0 || supportFLBrightnessConfigVer > 0;
|
||||
}
|
||||
|
||||
hasBattery() {
|
||||
const abilities = this.getAbilities();
|
||||
const batteryConfigVer = abilities?.battery?.ver ?? 0;
|
||||
return batteryConfigVer > 0;
|
||||
}
|
||||
|
||||
getPtzCapabilities() {
|
||||
const abilities = this.getAbilities();
|
||||
const hasZoom = (abilities?.supportDigitalZoom?.ver ?? 0) > 0;
|
||||
const hasPanTilt = (abilities?.ptzCtrl?.ver ?? 0) > 0;
|
||||
const hasPresets = (abilities?.ptzPreset?.ver ?? 0) > 0;
|
||||
|
||||
return {
|
||||
hasZoom,
|
||||
hasPanTilt,
|
||||
hasPresets,
|
||||
hasPtz: hasZoom || hasPanTilt || hasPresets
|
||||
};
|
||||
}
|
||||
|
||||
hasPtzCtrl() {
|
||||
const abilities = this.getAbilities();
|
||||
const zoomVer = abilities?.supportDigitalZoom?.ver ?? 0;
|
||||
return zoomVer > 0;
|
||||
}
|
||||
|
||||
hasPirEvents() {
|
||||
const abilities = this.getAbilities();
|
||||
const pirEvents = abilities?.mdWithPir?.ver ?? 0;
|
||||
return pirEvents > 0;
|
||||
}
|
||||
|
||||
async getDeviceInterfaces() {
|
||||
const interfaces = [
|
||||
ScryptedInterface.VideoCamera,
|
||||
ScryptedInterface.Settings,
|
||||
...this.nvrDevice.plugin.getAdditionalInterfaces(),
|
||||
];
|
||||
|
||||
try {
|
||||
if (this.storageSettings.values.useOnvifTwoWayAudio) {
|
||||
interfaces.push(
|
||||
ScryptedInterface.Intercom
|
||||
);
|
||||
}
|
||||
|
||||
const { hasPtz } = this.getPtzCapabilities();
|
||||
|
||||
if (hasPtz) {
|
||||
interfaces.push(ScryptedInterface.PanTiltZoom);
|
||||
}
|
||||
if ((await this.getObjectTypes()).classes.length > 0) {
|
||||
interfaces.push(ScryptedInterface.ObjectDetector);
|
||||
}
|
||||
if (this.hasSiren() || this.hasFloodlight() || this.hasPirEvents())
|
||||
interfaces.push(ScryptedInterface.DeviceProvider);
|
||||
if (this.hasBattery()) {
|
||||
interfaces.push(ScryptedInterface.Battery, ScryptedInterface.Sleep);
|
||||
}
|
||||
} catch (e) {
|
||||
this.getLogger().error('Error getting device interfaces', e);
|
||||
}
|
||||
|
||||
return interfaces;
|
||||
}
|
||||
|
||||
async processBatteryData(data: BatteryInfoResponse) {
|
||||
const logger = this.getLogger();
|
||||
const { batteryLevel, sleeping } = data;
|
||||
const { debugEvents } = this.storageSettings.values;
|
||||
|
||||
if (debugEvents) {
|
||||
logger.debug(`Battery info received: ${JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
if (sleeping !== this.sleeping) {
|
||||
this.sleeping = sleeping;
|
||||
}
|
||||
|
||||
if (batteryLevel !== this.batteryLevel) {
|
||||
this.batteryLevel = batteryLevel ?? this.batteryLevel;
|
||||
}
|
||||
}
|
||||
|
||||
async processDeviceStatusData(data: DeviceStatusResponse) {
|
||||
const { floodlightEnabled, pirEnabled, ptzPresets, osd } = data;
|
||||
const logger = this.getLogger();
|
||||
|
||||
const { debugEvents } = this.storageSettings.values;
|
||||
if (debugEvents) {
|
||||
logger.info(`Device status received: ${JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
if (this.floodlight && floodlightEnabled !== this.floodlight.on) {
|
||||
this.floodlight.on = floodlightEnabled;
|
||||
}
|
||||
|
||||
if (this.pirSensor && pirEnabled !== this.pirSensor.on) {
|
||||
this.pirSensor.on = pirEnabled;
|
||||
}
|
||||
|
||||
if (ptzPresets) {
|
||||
this.storageSettings.values.cachedPresets = ptzPresets
|
||||
}
|
||||
|
||||
if (osd) {
|
||||
this.storageSettings.values.cachedOsd = osd
|
||||
}
|
||||
}
|
||||
|
||||
updateDeviceInfo() {
|
||||
const ip = this.nvrDevice.storageSettings.values.ipAddress
|
||||
if (!ip)
|
||||
return;
|
||||
const info = this.info || {};
|
||||
info.ip = ip;
|
||||
|
||||
const deviceData = this.getDeviceData();
|
||||
|
||||
info.serialNumber = deviceData?.serial;
|
||||
info.firmware = deviceData?.firmVer;
|
||||
info.version = deviceData?.boardInfo;
|
||||
info.model = deviceData?.typeInfo;
|
||||
info.manufacturer = 'Reolink';
|
||||
info.managementUrl = `http://${ip}`;
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
async getOnvifClient() {
|
||||
if (!this.onvifClient)
|
||||
this.onvifClient = await this.createOnvifClient();
|
||||
return this.onvifClient;
|
||||
}
|
||||
|
||||
createOnvifClient() {
|
||||
const { username, password, httpPort, ipAddress } = this.nvrDevice.storageSettings.values;
|
||||
const address = `${ipAddress}:${httpPort}`;
|
||||
return connectCameraAPI(address, username, password, this.getLogger(), undefined);
|
||||
}
|
||||
|
||||
async processEvents(events: EventsResponse) {
|
||||
const logger = this.getLogger();
|
||||
|
||||
const { debugEvents } = this.storageSettings.values;
|
||||
if (debugEvents) {
|
||||
logger.debug(`Events received: ${JSON.stringify(events)}`);
|
||||
}
|
||||
|
||||
if (events.motion !== this.motionDetected) {
|
||||
if (events.motion) {
|
||||
|
||||
this.motionDetected = true;
|
||||
this.motionTimeout && clearTimeout(this.motionTimeout);
|
||||
this.motionTimeout = setTimeout(() => this.motionDetected = false, this.storageSettings.values.motionTimeout * 1000);
|
||||
} else {
|
||||
this.motionDetected = false;
|
||||
this.motionTimeout && clearTimeout(this.motionTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
if (events.objects.length) {
|
||||
const od: ObjectsDetected = {
|
||||
timestamp: Date.now(),
|
||||
detections: [],
|
||||
};
|
||||
for (const c of events.objects) {
|
||||
od.detections.push({
|
||||
className: c,
|
||||
score: 1,
|
||||
});
|
||||
}
|
||||
sdk.deviceManager.onDeviceEvent(this.nativeId, ScryptedInterface.ObjectDetector, od);
|
||||
}
|
||||
}
|
||||
|
||||
async listenLoop() {
|
||||
return null;
|
||||
}
|
||||
|
||||
async listenEvents() {
|
||||
return null;
|
||||
}
|
||||
|
||||
async takeSnapshotInternal(timeout?: number) {
|
||||
const now = Date.now();
|
||||
const client = this.getClient();
|
||||
const mo = await this.createMediaObject(await client.jpegSnapshot(this.getRtspChannel(), timeout), 'image/jpeg');
|
||||
this.lastB64Snapshot = await moToB64(mo);
|
||||
this.lastSnapshotTaken = now;
|
||||
|
||||
return mo;
|
||||
}
|
||||
|
||||
async takeSmartCameraPicture(options?: RequestPictureOptions): Promise<MediaObject> {
|
||||
const isBattery = this.hasBattery();
|
||||
const now = Date.now();
|
||||
const logger = this.getLogger();
|
||||
|
||||
const isMaxTimePassed = !this.lastSnapshotTaken || ((now - this.lastSnapshotTaken) > 1000 * 60 * 60);
|
||||
const isBatteryTimePassed = !this.lastSnapshotTaken || ((now - this.lastSnapshotTaken) > 1000 * 15);
|
||||
let canTake = false;
|
||||
|
||||
if (!this.lastB64Snapshot || !this.lastSnapshotTaken) {
|
||||
logger.log('Allowing new snapshot because not taken yet');
|
||||
canTake = true;
|
||||
} else if (this.sleeping && isMaxTimePassed) {
|
||||
logger.log('Allowing new snapshot while sleeping because older than 1 hour');
|
||||
canTake = true;
|
||||
} else if (!this.sleeping && isBattery && isBatteryTimePassed) {
|
||||
logger.log('Allowing new snapshot because older than 15 seconds');
|
||||
canTake = true;
|
||||
} else {
|
||||
canTake = true;
|
||||
}
|
||||
|
||||
if (canTake) {
|
||||
return this.takeSnapshotInternal(options?.timeout);
|
||||
} else if (this.lastB64Snapshot) {
|
||||
const mo = await b64ToMo(this.lastB64Snapshot);
|
||||
|
||||
return mo;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getRtspChannel() {
|
||||
return this.storageSettings.values.rtspChannel;
|
||||
}
|
||||
|
||||
createRtspMediaStreamOptions(url: string, index: number) {
|
||||
const ret = createRtspMediaStreamOptions(url, index);
|
||||
ret.tool = 'scrypted';
|
||||
return ret;
|
||||
}
|
||||
|
||||
addRtspCredentials(rtspUrl: string) {
|
||||
const { username, password } = this.nvrDevice.storageSettings.values;
|
||||
const url = new URL(rtspUrl);
|
||||
// if (url.protocol !== 'rtmp:') {
|
||||
url.username = username;
|
||||
url.password = password;
|
||||
// } else {
|
||||
// const params = url.searchParams;
|
||||
// for (const [k, v] of Object.entries(this.plugin.client.parameters)) {
|
||||
// params.set(k, v);
|
||||
// }
|
||||
// }
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
async createVideoStream(vso: UrlMediaStreamOptions): Promise<MediaObject> {
|
||||
await this.nvrDevice.client.login();
|
||||
return super.createVideoStream(vso);
|
||||
}
|
||||
|
||||
async getConstructedVideoStreamOptions(): Promise<UrlMediaStreamOptions[]> {
|
||||
const encoderConfig: Enc = this.getEncoderSettings();
|
||||
|
||||
const rtspChannel = this.getRtspChannel();
|
||||
const channel = (rtspChannel + 1).toString().padStart(2, '0');
|
||||
|
||||
const streams: UrlMediaStreamOptions[] = [
|
||||
{
|
||||
name: '',
|
||||
id: 'main.bcs',
|
||||
container: 'rtmp',
|
||||
video: { width: 2560, height: 1920 },
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
id: 'ext.bcs',
|
||||
container: 'rtmp',
|
||||
video: { width: 896, height: 672 },
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
id: 'sub.bcs',
|
||||
container: 'rtmp',
|
||||
video: { width: 640, height: 480 },
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
id: `h264Preview_${channel}_main`,
|
||||
container: 'rtsp',
|
||||
video: { codec: 'h264', width: 2560, height: 1920 },
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
id: `h264Preview_${channel}_sub`,
|
||||
container: 'rtsp',
|
||||
video: { codec: 'h264', width: 640, height: 480 },
|
||||
url: ''
|
||||
}
|
||||
];
|
||||
|
||||
// abilityChn->live
|
||||
// 0: not support
|
||||
// 1: support main/extern/sub stream
|
||||
// 2: support main/sub stream
|
||||
|
||||
const abilities = this.getAbilities();
|
||||
const { channelInfo } = this.getDeviceData();
|
||||
|
||||
const live = abilities?.live?.ver;
|
||||
const [rtmpMain, rtmpExt, rtmpSub, rtspMain, rtspSub] = streams;
|
||||
streams.splice(0, streams.length);
|
||||
|
||||
const mainEncType = abilities?.mainEncType?.ver;
|
||||
|
||||
const isHomeHub = this.nvrDevice.info.model === 'Reolink Home Hub';
|
||||
if (isHomeHub) {
|
||||
streams.push(...[rtspMain, rtspSub]);
|
||||
} else if (live === 2) {
|
||||
if (mainEncType === 1) {
|
||||
streams.push(rtmpSub, rtspMain, rtspSub);
|
||||
}
|
||||
else {
|
||||
streams.push(rtmpMain, rtmpSub, rtspMain, rtspSub);
|
||||
}
|
||||
}
|
||||
else if (mainEncType === 1) {
|
||||
streams.push(rtmpExt, rtmpSub, rtspMain, rtspSub);
|
||||
}
|
||||
else {
|
||||
streams.push(rtmpMain, rtmpExt, rtmpSub, rtspMain, rtspSub);
|
||||
}
|
||||
|
||||
if (channelInfo.typeInfo &&
|
||||
[
|
||||
"Reolink TrackMix PoE",
|
||||
"Reolink TrackMix WiFi",
|
||||
"RLC-81MA",
|
||||
"Trackmix Series W760"
|
||||
].includes(channelInfo.typeInfo)) {
|
||||
streams.push({
|
||||
name: '',
|
||||
id: 'autotrack.bcs',
|
||||
container: 'rtmp',
|
||||
video: { width: 896, height: 512 },
|
||||
url: '',
|
||||
});
|
||||
|
||||
if (rtspChannel === 0) {
|
||||
streams.push({
|
||||
name: '',
|
||||
id: `h264Preview_02_main`,
|
||||
container: 'rtsp',
|
||||
video: { codec: 'h264', width: 3840, height: 2160 },
|
||||
url: ''
|
||||
}, {
|
||||
name: '',
|
||||
id: `h264Preview_02_sub`,
|
||||
container: 'rtsp',
|
||||
video: { codec: 'h264', width: 640, height: 480 },
|
||||
url: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const stream of streams) {
|
||||
var streamUrl;
|
||||
if (stream.container === 'rtmp') {
|
||||
streamUrl = new URL(`rtmp://${this.getRtmpAddress()}/bcs/channel${rtspChannel}_${stream.id}`)
|
||||
const params = streamUrl.searchParams;
|
||||
params.set("channel", rtspChannel.toString())
|
||||
params.set("stream", '0')
|
||||
stream.url = streamUrl.toString();
|
||||
stream.name = `RTMP ${stream.id}`;
|
||||
} else if (stream.container === 'rtsp') {
|
||||
streamUrl = new URL(`rtsp://${this.getRtspAddress()}/${stream.id}`)
|
||||
stream.url = streamUrl.toString();
|
||||
stream.name = `RTSP ${stream.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (encoderConfig) {
|
||||
const { mainStream } = encoderConfig;
|
||||
if (mainStream?.width && mainStream?.height) {
|
||||
for (const stream of streams) {
|
||||
if (stream.id === 'main.bcs' || stream.id === `h264Preview_${channel}_main`) {
|
||||
stream.video.width = mainStream.width;
|
||||
stream.video.height = mainStream.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return streams;
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
const settings = await this.storageSettings.getSettings();
|
||||
return settings;
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: string) {
|
||||
if (this.storageSettings.keys[key]) {
|
||||
await this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
else {
|
||||
await super.putSetting(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
showRtspUrlOverride() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getRtspAddress() {
|
||||
const { ipAddress, rtspPort } = this.nvrDevice.storageSettings.values;
|
||||
return `${ipAddress}:${rtspPort}`;
|
||||
}
|
||||
|
||||
getRtmpAddress() {
|
||||
const { ipAddress, rtmpPort } = this.nvrDevice.storageSettings.values;
|
||||
return `${ipAddress}:${rtmpPort}`;
|
||||
}
|
||||
|
||||
async reportDevices() {
|
||||
const hasSiren = this.hasSiren();
|
||||
const hasFloodlight = this.hasFloodlight();
|
||||
const hasPirEvents = this.hasPirEvents();
|
||||
|
||||
const devices: Device[] = [];
|
||||
|
||||
if (hasSiren) {
|
||||
const sirenNativeId = `${this.nativeId}-siren`;
|
||||
const sirenDevice: Device = {
|
||||
providerNativeId: this.nativeId,
|
||||
name: `${this.name} Siren`,
|
||||
nativeId: sirenNativeId,
|
||||
info: {
|
||||
...this.info,
|
||||
},
|
||||
interfaces: [
|
||||
ScryptedInterface.OnOff
|
||||
],
|
||||
type: ScryptedDeviceType.Siren,
|
||||
};
|
||||
|
||||
devices.push(sirenDevice);
|
||||
}
|
||||
|
||||
if (hasFloodlight) {
|
||||
const floodlightNativeId = `${this.nativeId}-floodlight`;
|
||||
const floodlightDevice: Device = {
|
||||
providerNativeId: this.nativeId,
|
||||
name: `${this.name} Floodlight`,
|
||||
nativeId: floodlightNativeId,
|
||||
info: {
|
||||
...this.info,
|
||||
},
|
||||
interfaces: [
|
||||
ScryptedInterface.OnOff
|
||||
],
|
||||
type: ScryptedDeviceType.Light,
|
||||
};
|
||||
|
||||
devices.push(floodlightDevice);
|
||||
}
|
||||
|
||||
if (hasPirEvents) {
|
||||
const pirNativeId = `${this.nativeId}-pir`;
|
||||
const pirDevice: Device = {
|
||||
providerNativeId: this.nativeId,
|
||||
name: `${this.name} PIR sensor`,
|
||||
nativeId: pirNativeId,
|
||||
info: {
|
||||
...this.info,
|
||||
},
|
||||
interfaces: [
|
||||
ScryptedInterface.OnOff
|
||||
],
|
||||
type: ScryptedDeviceType.Switch,
|
||||
};
|
||||
|
||||
devices.push(pirDevice);
|
||||
}
|
||||
|
||||
sdk.deviceManager.onDevicesChanged({
|
||||
providerNativeId: this.nativeId,
|
||||
devices
|
||||
});
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string): Promise<any> {
|
||||
if (nativeId.endsWith('-siren')) {
|
||||
this.siren ||= new ReolinkCameraSiren(this, nativeId);
|
||||
return this.siren;
|
||||
} else if (nativeId.endsWith('-floodlight')) {
|
||||
this.floodlight ||= new ReolinkCameraFloodlight(this, nativeId);
|
||||
return this.floodlight;
|
||||
} else if (nativeId.endsWith('-pir')) {
|
||||
this.pirSensor ||= new ReolinkCameraPirSensor(this, nativeId);
|
||||
return this.pirSensor;
|
||||
}
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string) {
|
||||
if (nativeId.endsWith('-siren')) {
|
||||
delete this.siren;
|
||||
} else if (nativeId.endsWith('-floodlight')) {
|
||||
delete this.floodlight;
|
||||
} else if (nativeId.endsWith('-pir')) {
|
||||
delete this.pirSensor;
|
||||
}
|
||||
}
|
||||
}
|
||||
381
plugins/reolink/src/nvr/nvr.ts
Normal file
381
plugins/reolink/src/nvr/nvr.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
import sdk, { Settings, ScryptedDeviceBase, Setting, SettingValue, DeviceDiscovery, AdoptDevice, DiscoveredDevice, Device, ScryptedInterface, ScryptedDeviceType, DeviceProvider, Reboot, DeviceCreatorSettings } from "@scrypted/sdk";
|
||||
import ReolinkProvider from "../main";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { DevInfo } from "../probe";
|
||||
import { ReolinkNvrCamera } from "./camera";
|
||||
import { DeviceInputData, ReolinkNvrClient } from "./api";
|
||||
|
||||
export class ReolinkNvrDevice extends ScryptedDeviceBase implements Settings, DeviceDiscovery, DeviceProvider, Reboot {
|
||||
storageSettings = new StorageSettings(this, {
|
||||
debugEvents: {
|
||||
title: 'Debug Events',
|
||||
type: 'boolean',
|
||||
immediate: true,
|
||||
},
|
||||
ipAddress: {
|
||||
title: 'IP address',
|
||||
type: 'string',
|
||||
onPut: async () => await this.reinit()
|
||||
},
|
||||
username: {
|
||||
title: 'Username',
|
||||
placeholder: 'admin',
|
||||
defaultValue: 'admin',
|
||||
type: 'string',
|
||||
onPut: async () => await this.reinit()
|
||||
},
|
||||
password: {
|
||||
title: 'Password',
|
||||
type: 'password',
|
||||
onPut: async () => await this.reinit()
|
||||
},
|
||||
httpPort: {
|
||||
title: 'HTTP Port',
|
||||
subgroup: 'Advanced',
|
||||
defaultValue: 80,
|
||||
placeholder: '80',
|
||||
type: 'number',
|
||||
onPut: async () => await this.reinit()
|
||||
},
|
||||
rtspPort: {
|
||||
subgroup: 'Advanced',
|
||||
title: 'RTSP Port',
|
||||
placeholder: '554',
|
||||
defaultValue: 554,
|
||||
type: 'number',
|
||||
onPut: async () => await this.reinit()
|
||||
},
|
||||
rtmpPort: {
|
||||
subgroup: 'Advanced',
|
||||
title: 'RTMP Port',
|
||||
placeholder: '1935',
|
||||
defaultValue: 1935,
|
||||
type: 'number',
|
||||
onPut: async () => await this.reinit()
|
||||
},
|
||||
abilities: {
|
||||
json: true,
|
||||
hide: true,
|
||||
defaultValue: {}
|
||||
},
|
||||
devicesData: {
|
||||
json: true,
|
||||
hide: true,
|
||||
defaultValue: {}
|
||||
},
|
||||
hubData: {
|
||||
json: true,
|
||||
hide: true,
|
||||
defaultValue: {}
|
||||
},
|
||||
loginSession: {
|
||||
json: true,
|
||||
hide: true,
|
||||
},
|
||||
});
|
||||
plugin: ReolinkProvider;
|
||||
client: ReolinkNvrClient;
|
||||
discoveredDevices = new Map<string, {
|
||||
device: Device;
|
||||
description: string;
|
||||
rtspChannel: number;
|
||||
}>();
|
||||
lastHubInfoCheck = undefined;
|
||||
lastErrorsCheck = undefined;
|
||||
lastDevicesStatusCheck = undefined;
|
||||
cameraNativeMap = new Map<string, ReolinkNvrCamera>();
|
||||
processing = false;
|
||||
|
||||
constructor(nativeId: string, plugin: ReolinkProvider) {
|
||||
super(nativeId);
|
||||
this.plugin = plugin;
|
||||
|
||||
setTimeout(async () => {
|
||||
await this.init();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
async reboot(): Promise<void> {
|
||||
const client = this.getClient();
|
||||
await client.reboot();
|
||||
}
|
||||
|
||||
getLogger() {
|
||||
return this.console;
|
||||
}
|
||||
|
||||
async reinit() {
|
||||
this.client = undefined;
|
||||
// await this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
const client = this.getClient();
|
||||
await client.login();
|
||||
const logger = this.getLogger();
|
||||
|
||||
setInterval(async () => {
|
||||
if (this.processing || !client) {
|
||||
return;
|
||||
}
|
||||
this.processing = true;
|
||||
try {
|
||||
const now = Date.now();
|
||||
|
||||
if (!this.lastErrorsCheck || (now - this.lastErrorsCheck > 60 * 1000)) {
|
||||
this.lastErrorsCheck = now;
|
||||
await client.checkErrors();
|
||||
}
|
||||
|
||||
if (!this.lastHubInfoCheck || now - this.lastHubInfoCheck > 1000 * 60 * 5) {
|
||||
logger.log('Starting Hub info data fetch');
|
||||
this.lastHubInfoCheck = now;
|
||||
const { abilities, hubData, } = await client.getHubInfo();
|
||||
const { devicesData, channelsResponse, response } = await client.getDevicesInfo();
|
||||
logger.log('Hub info data fetched');
|
||||
if (this.storageSettings.values.debugEvents) {
|
||||
logger.log(`${JSON.stringify({ abilities, hubData, devicesData, channelsResponse, response })}`);
|
||||
}
|
||||
this.storageSettings.values.abilities = abilities;
|
||||
this.storageSettings.values.hubData = hubData;
|
||||
this.storageSettings.values.devicesData = devicesData;
|
||||
|
||||
await this.discoverDevices(true);
|
||||
}
|
||||
|
||||
const devicesMap = new Map<number, DeviceInputData>();
|
||||
let anyBattery = false;
|
||||
let anyAwaken = false;
|
||||
|
||||
this.cameraNativeMap.forEach((camera) => {
|
||||
if (camera) {
|
||||
const channel = camera.storageSettings.values.rtspChannel;
|
||||
|
||||
const abilities = camera.getAbilities();
|
||||
if (abilities) {
|
||||
const hasBattery = camera.hasBattery();
|
||||
const hasPirEvents = camera.hasPirEvents();
|
||||
const hasFloodlight = camera.hasFloodlight();
|
||||
const sleeping = camera.sleeping;
|
||||
const { hasPtz } = camera.getPtzCapabilities();
|
||||
devicesMap.set(Number(channel), {
|
||||
hasFloodlight,
|
||||
hasBattery,
|
||||
hasPirEvents,
|
||||
hasPtz,
|
||||
sleeping
|
||||
});
|
||||
|
||||
if (hasBattery && !anyBattery) {
|
||||
anyBattery = true;
|
||||
}
|
||||
|
||||
if (!sleeping && !anyAwaken) {
|
||||
anyAwaken = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const anyDeviceFound = devicesMap.size > 0;
|
||||
|
||||
if (anyDeviceFound) {
|
||||
const eventsRes = await client.getEvents(devicesMap);
|
||||
|
||||
if (this.storageSettings.values.debugEvents) {
|
||||
logger.debug(`Events call result: ${JSON.stringify(eventsRes)}`);
|
||||
}
|
||||
this.cameraNativeMap.forEach((camera) => {
|
||||
if (camera) {
|
||||
const channel = camera.storageSettings.values.rtspChannel;
|
||||
const cameraEventsData = eventsRes?.parsed[channel];
|
||||
if (cameraEventsData) {
|
||||
camera.processEvents(cameraEventsData);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (anyBattery) {
|
||||
const { batteryInfoData, response } = await client.getBatteryInfo(devicesMap);
|
||||
|
||||
if (this.storageSettings.values.debugEvents) {
|
||||
logger.debug(`Battery info call result: ${JSON.stringify({ batteryInfoData, response })}`);
|
||||
}
|
||||
|
||||
this.cameraNativeMap.forEach((camera) => {
|
||||
if (camera) {
|
||||
const channel = camera.storageSettings.values.rtspChannel;
|
||||
const cameraBatteryData = batteryInfoData[channel];
|
||||
if (cameraBatteryData) {
|
||||
camera.processBatteryData(cameraBatteryData);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (anyDeviceFound) {
|
||||
if (!this.lastDevicesStatusCheck || (now - this.lastDevicesStatusCheck > 15 * 1000) && anyAwaken) {
|
||||
this.lastDevicesStatusCheck = now;
|
||||
const { deviceStatusData, response } = await client.getStatusInfo(devicesMap);
|
||||
|
||||
if (this.storageSettings.values.debugEvents) {
|
||||
logger.info(`Status info raw result: ${JSON.stringify({ deviceStatusData, response })}`);
|
||||
}
|
||||
|
||||
this.cameraNativeMap.forEach((camera) => {
|
||||
if (camera) {
|
||||
const channel = camera.storageSettings.values.rtspChannel;
|
||||
const cameraDeviceStatusData = deviceStatusData[channel];
|
||||
if (cameraDeviceStatusData) {
|
||||
camera.processDeviceStatusData(cameraDeviceStatusData);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.console.error('Error on events flow', e);
|
||||
} finally {
|
||||
this.processing = false;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
getClient() {
|
||||
if (!this.client) {
|
||||
const { ipAddress, httpPort, password, username } = this.storageSettings.values;
|
||||
const address = `${ipAddress}:${httpPort}`;
|
||||
this.client = new ReolinkNvrClient(
|
||||
address,
|
||||
username,
|
||||
password,
|
||||
this.console,
|
||||
this,
|
||||
);
|
||||
}
|
||||
return this.client;
|
||||
}
|
||||
|
||||
updateDeviceInfo(devInfo: DevInfo) {
|
||||
const info = this.info || {};
|
||||
info.ip = this.storageSettings.values.ipAddress;
|
||||
info.serialNumber = devInfo.serial;
|
||||
info.firmware = devInfo.firmVer;
|
||||
info.version = devInfo.firmVer;
|
||||
info.model = devInfo.model;
|
||||
info.manufacturer = 'Reolink';
|
||||
info.managementUrl = `http://${info.ip}`;
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
const settings = await this.storageSettings.getSettings();
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
|
||||
async releaseDevice(id: string, nativeId: string) {
|
||||
this.cameraNativeMap.delete(nativeId);
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string): Promise<ReolinkNvrCamera> {
|
||||
let device = this.cameraNativeMap.get(nativeId);
|
||||
|
||||
if (!device) {
|
||||
device = new ReolinkNvrCamera(nativeId, this);
|
||||
this.cameraNativeMap.set(nativeId, device);
|
||||
}
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
buildNativeId(uid: string) {
|
||||
return `${this.nativeId}-${uid}`;
|
||||
}
|
||||
|
||||
getCameraInterfaces() {
|
||||
return [
|
||||
ScryptedInterface.VideoCameraConfiguration,
|
||||
ScryptedInterface.Camera,
|
||||
ScryptedInterface.MotionSensor,
|
||||
ScryptedInterface.VideoTextOverlays,
|
||||
ScryptedInterface.MixinProvider,
|
||||
ScryptedInterface.VideoCamera,
|
||||
ScryptedInterface.Settings,
|
||||
];
|
||||
}
|
||||
|
||||
async syncEntitiesFromRemote() {
|
||||
const api = this.getClient();
|
||||
const { channels, devicesData } = await api.getDevicesInfo();
|
||||
|
||||
for (const channel of channels) {
|
||||
const { channelStatus, channelInfo } = devicesData[channel];
|
||||
const name = channelStatus.name || `Channel ${channel}`;
|
||||
|
||||
const nativeId = this.buildNativeId(channelStatus.uid);
|
||||
const device: Device = {
|
||||
nativeId,
|
||||
name,
|
||||
providerNativeId: this.nativeId,
|
||||
interfaces: this.getCameraInterfaces() ?? [],
|
||||
type: ScryptedDeviceType.Camera,
|
||||
info: {
|
||||
manufacturer: 'Reolink',
|
||||
model: channelInfo.typeInfo
|
||||
}
|
||||
};
|
||||
|
||||
if (sdk.deviceManager.getNativeIds().includes(nativeId)) {
|
||||
// const device = sdk.systemManager.getDeviceById<Device>(this.pluginId, nativeId);
|
||||
// sdk.deviceManager.onDeviceDiscovered(device);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.discoveredDevices.has(nativeId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.discoveredDevices.set(nativeId, {
|
||||
device,
|
||||
description: `${name}`,
|
||||
rtspChannel: channel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async discoverDevices(scan?: boolean): Promise<DiscoveredDevice[]> {
|
||||
if (scan) {
|
||||
await this.syncEntitiesFromRemote();
|
||||
}
|
||||
|
||||
return [...this.discoveredDevices.values()].map(d => ({
|
||||
...d.device,
|
||||
description: d.description,
|
||||
}));
|
||||
}
|
||||
|
||||
async adoptDevice(adopt: AdoptDevice): Promise<string> {
|
||||
const entry = this.discoveredDevices.get(adopt.nativeId);
|
||||
|
||||
if (!entry)
|
||||
throw new Error('device not found');
|
||||
|
||||
await this.onDeviceEvent(ScryptedInterface.DeviceDiscovery, await this.discoverDevices());
|
||||
|
||||
await sdk.deviceManager.onDeviceDiscovered(entry.device);
|
||||
|
||||
const device = await this.getDevice(adopt.nativeId);
|
||||
this.console.log('Adopted device', entry, device?.name);
|
||||
device.storageSettings.values.rtspChannel = entry.rtspChannel;
|
||||
|
||||
this.discoveredDevices.delete(adopt.nativeId);
|
||||
return device?.id;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user