From ef53829ccca10417373183cc3ca1787cd46cbbca Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Thu, 5 Dec 2024 09:38:36 -0800 Subject: [PATCH] core: clean up device groups --- plugins/core/.vscode/settings.json | 2 +- plugins/core/package-lock.json | 4 +- plugins/core/package.json | 2 +- plugins/core/src/aggregate-core.ts | 33 ++-- plugins/core/src/aggregate.ts | 250 ++++++++++++++--------------- 5 files changed, 140 insertions(+), 151 deletions(-) diff --git a/plugins/core/.vscode/settings.json b/plugins/core/.vscode/settings.json index 79c896063..7cad3e305 100644 --- a/plugins/core/.vscode/settings.json +++ b/plugins/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "scrypted.debugHost": "127.0.0.1", + "scrypted.debugHost": "scrypted-nvr", } \ No newline at end of file diff --git a/plugins/core/package-lock.json b/plugins/core/package-lock.json index 75d598c24..fb960bcbe 100644 --- a/plugins/core/package-lock.json +++ b/plugins/core/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/core", - "version": "0.3.86", + "version": "0.3.87", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/core", - "version": "0.3.86", + "version": "0.3.87", "license": "Apache-2.0", "dependencies": { "@scrypted/common": "file:../../common", diff --git a/plugins/core/package.json b/plugins/core/package.json index 48f0a63bf..dc1c98ea9 100644 --- a/plugins/core/package.json +++ b/plugins/core/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/core", - "version": "0.3.86", + "version": "0.3.87", "description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.", "author": "Scrypted", "license": "Apache-2.0", diff --git a/plugins/core/src/aggregate-core.ts b/plugins/core/src/aggregate-core.ts index 3198446c2..79d8647dc 100644 --- a/plugins/core/src/aggregate-core.ts +++ b/plugins/core/src/aggregate-core.ts @@ -1,5 +1,5 @@ import sdk, { Device, DeviceCreator, DeviceCreatorSettings, DeviceProvider, Readme, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from '@scrypted/sdk'; -import { AggregateDevice, createAggregateDevice } from './aggregate'; +import { AggregateDevice } from './aggregate'; const { deviceManager } = sdk; export const AggregateCoreNativeId = 'aggregatecore'; @@ -13,24 +13,6 @@ export class AggregateCore extends ScryptedDeviceBase implements DeviceProvider, this.systemDevice = { deviceCreator: 'Device Group', }; - - for (const nativeId of deviceManager.getNativeIds()) { - if (nativeId?.startsWith('aggregate:')) { - const aggregate = createAggregateDevice(nativeId); - this.aggregate.set(nativeId, aggregate); - this.reportAggregate(nativeId, aggregate.computeInterfaces(), aggregate.providedName); - } - } - - sdk.systemManager.listen((eventSource, eventDetails, eventData) => { - if (eventDetails.eventInterface === 'Storage') { - const ids = [...this.aggregate.values()].map(a => a.id); - if (ids.includes(eventSource.id)) { - const aggregate = [...this.aggregate.values()].find(a => a.id === eventSource.id); - this.reportAggregate(aggregate.nativeId, aggregate.computeInterfaces(), aggregate.providedName); - } - } - }); } async getReadmeMarkdown(): Promise { @@ -51,7 +33,8 @@ export class AggregateCore extends ScryptedDeviceBase implements DeviceProvider, const { name } = settings; const nativeId = `aggregate:${Math.random()}`; await this.reportAggregate(nativeId, [], name?.toString()); - const aggregate = createAggregateDevice(nativeId); + const aggregate = new AggregateDevice(this, nativeId); + aggregate.computeInterfaces(); this.aggregate.set(nativeId, aggregate); return nativeId; } @@ -68,9 +51,17 @@ export class AggregateCore extends ScryptedDeviceBase implements DeviceProvider, } async getDevice(nativeId: string) { - return this.aggregate.get(nativeId); + let device = this.aggregate.get(nativeId); + if (device) + return device; + device = new AggregateDevice(this, nativeId); + device.computeInterfaces(); + this.aggregate.set(nativeId, device); + return device; } async releaseDevice(id: string, nativeId: string): Promise { + const device = this.aggregate.get(nativeId); + device?.release(); } } diff --git a/plugins/core/src/aggregate.ts b/plugins/core/src/aggregate.ts index b97703260..ecf980a44 100644 --- a/plugins/core/src/aggregate.ts +++ b/plugins/core/src/aggregate.ts @@ -1,11 +1,8 @@ import sdk, { EventListener, EventListenerRegister, FFmpegInput, LockState, MediaStreamDestination, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceBase, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera } from "@scrypted/sdk"; import { StorageSettings } from "@scrypted/sdk/storage-settings"; +import type { AggregateCore } from "./aggregate-core"; const { systemManager, mediaManager, deviceManager } = sdk; -export interface AggregateDevice extends ScryptedDeviceBase { - computeInterfaces(): string[]; -} - interface Aggregator { (values: T[]): T; } @@ -141,143 +138,144 @@ function createVideoCamera(devices: VideoCamera[], console: Console): VideoCamer } } -export function createAggregateDevice(nativeId: string): AggregateDevice { - class AggregateDeviceImpl extends ScryptedDeviceBase implements Settings { - listeners: EventListenerRegister[] = []; - storageSettings = new StorageSettings(this, { - deviceInterfaces: { - title: 'Selected Device Interfaces', - description: 'The components of other devices to combine into this device group.', - type: 'interface', - multiple: true, - deviceFilter: `id !== '${this.id}' && deviceInterface !== '${ScryptedInterface.Settings}'`, +export class AggregateDevice extends ScryptedDeviceBase implements Settings { + listeners: EventListenerRegister[] = []; + storageSettings = new StorageSettings(this, { + deviceInterfaces: { + title: 'Selected Device Interfaces', + description: 'The components of other devices to combine into this device group.', + type: 'interface', + multiple: true, + deviceFilter: `id !== '${this.id}' && deviceInterface !== '${ScryptedInterface.Settings}'`, + onPut: () => { + this.core.reportAggregate(this.nativeId, this.computeInterfaces(), this.providedName); } - }) - - constructor() { - super(nativeId); - - try { - const data = this.storage.getItem('data'); - if (data) { - const { deviceInterfaces } = JSON.parse(data); - this.storageSettings.values.deviceInterfaces = deviceInterfaces; - } - } - catch (e) { - } - this.storage.removeItem('data'); } + }) - getSettings(): Promise { - return this.storageSettings.getSettings(); + constructor(public core: AggregateCore, nativeId: string) { + super(nativeId); + + try { + const data = this.storage.getItem('data'); + if (data) { + const { deviceInterfaces } = JSON.parse(data); + this.storageSettings.values.deviceInterfaces = deviceInterfaces; + } } - putSetting(key: string, value: SettingValue): Promise { - return this.storageSettings.putSetting(key, value); + catch (e) { } + this.storage.removeItem('data'); + } - makeListener(iface: string, devices: ScryptedDevice[]) { - const aggregator = aggregators.get(iface); - if (!aggregator) { - const ds = deviceManager.getDeviceState(this.nativeId); - // if this device can't be aggregated for whatever reason, pass property through. - for (const device of devices) { - const register = device.listen({ - event: iface, - watch: true, - }, (source, details, data) => { - if (details.property) - ds[details.property] = data; - }); - this.listeners.push(register); - } - return; - } - - const property = ScryptedInterfaceDescriptors[iface]?.properties?.[0]; - if (!property) { - this.console.warn('aggregating interface with no property?', iface); - return; - } - - const runAggregator = () => { - const values = devices.map(device => device[property]); - (this as any)[property] = aggregator(values); - } - - const listener: EventListener = () => runAggregator(); + getSettings(): Promise { + return this.storageSettings.getSettings(); + } + putSetting(key: string, value: SettingValue): Promise { + return this.storageSettings.putSetting(key, value); + } + makeListener(iface: string, devices: ScryptedDevice[]) { + const aggregator = aggregators.get(iface); + if (!aggregator) { + const ds = deviceManager.getDeviceState(this.nativeId); + // if this device can't be aggregated for whatever reason, pass property through. for (const device of devices) { const register = device.listen({ event: iface, watch: true, - }, listener); + }, (source, details, data) => { + if (details.property) + ds[details.property] = data; + }); this.listeners.push(register); } - - return runAggregator; + return; } - computeInterfaces(): string[] { - this.listeners.forEach(listener => listener.removeListener()); - this.listeners = []; - - try { - const interfaces = new Map(); - for (const deviceInterface of this.storageSettings.values.deviceInterfaces as string[]) { - const parts = deviceInterface.split('#'); - const id = parts[0]; - const iface = parts[1]; - if (!interfaces.has(iface)) - interfaces.set(iface, []); - interfaces.get(iface).push(id); - } - - for (const [iface, ids] of interfaces.entries()) { - const devices = ids.map(id => systemManager.getDeviceById(id)); - const runAggregator = this.makeListener(iface, devices); - runAggregator?.(); - } - - for (const [iface, ids] of interfaces.entries()) { - const devices = ids.map(id => systemManager.getDeviceById(id)); - const descriptor = ScryptedInterfaceDescriptors[iface]; - if (!descriptor) { - this.console.warn(`descriptor not found for ${iface}, skipping method generation`); - continue; - } - - if (iface === ScryptedInterface.VideoCamera) { - const camera = createVideoCamera(devices as any, this.console); - for (const method of descriptor.methods) { - AggregateDeviceImpl.prototype[method] = (...args: any[]) => camera[method](...args); - } - continue; - } - - for (const method of descriptor.methods) { - AggregateDeviceImpl.prototype[method] = async function (...args: any[]) { - const ret: Promise[] = []; - for (const device of devices) { - ret.push(device[method](...args)); - } - - const results = await Promise.all(ret); - return results[0]; - } - } - } - - return [...interfaces.keys()]; - } - catch (e) { - // this.console.error('error loading aggregate device', e); - return []; - } + const property = ScryptedInterfaceDescriptors[iface]?.properties?.[0]; + if (!property) { + this.console.warn('aggregating interface with no property?', iface); + return; } + + const runAggregator = () => { + const values = devices.map(device => device[property]); + (this as any)[property] = aggregator(values); + } + + const listener: EventListener = () => runAggregator(); + + for (const device of devices) { + const register = device.listen({ + event: iface, + watch: true, + }, listener); + this.listeners.push(register); + } + + return runAggregator; } - const ret = new AggregateDeviceImpl(); - ret.computeInterfaces(); - return new AggregateDeviceImpl(); -} \ No newline at end of file + release() { + this.listeners.forEach(listener => listener.removeListener()); + this.listeners = []; + } + + computeInterfaces(): string[] { + this.release(); + + try { + const interfaces = new Map(); + for (const deviceInterface of this.storageSettings.values.deviceInterfaces as string[]) { + const parts = deviceInterface.split('#'); + const id = parts[0]; + const iface = parts[1]; + if (!interfaces.has(iface)) + interfaces.set(iface, []); + interfaces.get(iface).push(id); + } + + for (const [iface, ids] of interfaces.entries()) { + const devices = ids.map(id => systemManager.getDeviceById(id)); + const runAggregator = this.makeListener(iface, devices); + runAggregator?.(); + } + + for (const [iface, ids] of interfaces.entries()) { + const devices = ids.map(id => systemManager.getDeviceById(id)); + const descriptor = ScryptedInterfaceDescriptors[iface]; + if (!descriptor) { + this.console.warn(`descriptor not found for ${iface}, skipping method generation`); + continue; + } + + if (iface === ScryptedInterface.VideoCamera) { + const camera = createVideoCamera(devices as any, this.console); + for (const method of descriptor.methods) { + AggregateDevice.prototype[method] = (...args: any[]) => camera[method](...args); + } + continue; + } + + for (const method of descriptor.methods) { + AggregateDevice.prototype[method] = async function (...args: any[]) { + const ret: Promise[] = []; + for (const device of devices) { + ret.push(device[method](...args)); + } + + const results = await Promise.all(ret); + return results[0]; + } + } + } + + return [...interfaces.keys()]; + } + catch (e) { + // this.console.error('error loading aggregate device', e); + return []; + } + } +}