core: clean up device groups

This commit is contained in:
Koushik Dutta
2024-12-05 09:38:36 -08:00
parent 5f0cf6b6c2
commit ef53829ccc
5 changed files with 140 additions and 151 deletions

View File

@@ -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<string> {
@@ -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<void> {
const device = this.aggregate.get(nativeId);
device?.release();
}
}

View File

@@ -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<T> {
(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<Setting[]> {
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<void> {
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<Setting[]> {
return this.storageSettings.getSettings();
}
putSetting(key: string, value: SettingValue): Promise<void> {
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<string, string[]>();
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<any>[] = [];
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();
}
release() {
this.listeners.forEach(listener => listener.removeListener());
this.listeners = [];
}
computeInterfaces(): string[] {
this.release();
try {
const interfaces = new Map<string, string[]>();
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<any>[] = [];
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 [];
}
}
}