import { EventListenerOptions, EventDetails, EventListenerRegister, ScryptedDevice, ScryptedInterface, ScryptedInterfaceDescriptors, SystemDeviceState, SystemManager, ScryptedInterfaceProperty, ScryptedDeviceType, Logger, EventListener } from "@scrypted/types"; import { PluginAPI } from "./plugin-api"; import { PrimitiveProxyHandler, RpcPeer } from '../rpc'; import { EventRegistry } from "../event-registry"; import { allInterfaceProperties, isValidInterfaceMethod } from "./descriptor"; function newDeviceProxy(id: string, systemManager: SystemManagerImpl) { const handler = new DeviceProxyHandler(id, systemManager); return new Proxy(handler, handler); } class DeviceProxyHandler implements PrimitiveProxyHandler, ScryptedDevice { device: Promise; constructor(public id: string, public systemManager: SystemManagerImpl) { } toPrimitive() { return `ScryptedDevice-${this.id}` } get(target: any, p: PropertyKey, receiver: any): any { if (p === 'id') return this.id; const handled = RpcPeer.handleFunctionInvocations(this, target, p, receiver); if (handled) return handled; if (allInterfaceProperties.includes(p.toString())) return (this.systemManager.state[this.id] as any)?.[p]?.value; const prop = p.toString(); if (!isValidInterfaceMethod(this.systemManager.state[this.id].interfaces.value, prop)) return; if (ScryptedInterfaceDescriptors[ScryptedInterface.ScryptedDevice].methods.includes(prop)) return (this as any)[p].bind(this); return new Proxy(() => p, this); } ensureDevice() { if (!this.device) this.device = this.systemManager.api.getDeviceById(this.id); return this.device; } async apply(target: any, thisArg: any, argArray?: any) { const method = target(); const device = await this.ensureDevice(); return (device as any)[method](...argArray); } listen(event: string | EventListenerOptions, callback: EventListener): EventListenerRegister { return this.systemManager.listenDevice(this.id, event, callback); } async setName(name: string): Promise { return this.systemManager.api.setDeviceProperty(this.id, ScryptedInterfaceProperty.name, name); } async setRoom(room: string): Promise { return this.systemManager.api.setDeviceProperty(this.id, ScryptedInterfaceProperty.room, room); } async setType(type: ScryptedDeviceType): Promise { return this.systemManager.api.setDeviceProperty(this.id, ScryptedInterfaceProperty.type, type); } async probe(): Promise { return this.apply(() => 'probe', undefined, []); } } class EventListenerRegisterImpl implements EventListenerRegister { promise: Promise; constructor(promise: Promise) { this.promise = promise; } async removeListener(): Promise { try { const register = await this.promise; this.promise = undefined; register?.removeListener(); } catch (e) { console.error('removeListener', e); } } } function makeOneWayCallback(input: T): T { const f: any = input; const oneways: string[] = f[RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS] || []; if (!oneways.includes(null)) oneways.push(null); f[RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS] = oneways; return input; } export class SystemManagerImpl implements SystemManager { api: PluginAPI; state: {[id: string]: {[property: string]: SystemDeviceState}}; deviceProxies: {[id: string]: ScryptedDevice} = {}; log: Logger; events = new EventRegistry(); getDeviceState(id: string) { return this.state[id]; } getSystemState(): {[id: string]: {[property: string]: SystemDeviceState}} { return this.state; } getDeviceById(id: string): any { if (!this.state[id]) return; let proxy = this.deviceProxies[id]; if (!proxy) proxy = this.deviceProxies[id] = newDeviceProxy(id, this); return proxy; } getDeviceByName(name: string): any { for (const id of Object.keys(this.state)) { const s = this.state[id]; if ((s.interfaces?.value as string[])?.includes(ScryptedInterface.ScryptedPlugin) && s.pluginId?.value === name) return this.getDeviceById(id); if (s.name.value === name) return this.getDeviceById(id); } } listen(callback: EventListener): EventListenerRegister { return this.events.listen(makeOneWayCallback((id, eventDetails, eventData) => callback(this.getDeviceById(id), eventDetails, eventData))); } listenDevice(id: string, options: string | EventListenerOptions, callback: EventListener): EventListenerRegister { let { event, watch } = (options || {}) as EventListenerOptions; if (!event && typeof options === 'string') event = options as string; if (!event) event = undefined; // passive watching can be fast pathed to observe local state if (watch) return this.events.listenDevice(id, event, (eventDetails, eventData) => callback(this.getDeviceById(id), eventDetails, eventData)); return new EventListenerRegisterImpl(this.api.listenDevice(id, options, makeOneWayCallback((eventDetails, eventData) => callback(this.getDeviceById(id), eventDetails, eventData)))); } async removeDevice(id: string) { return this.api.removeDevice(id); } getComponent(id: string): Promise { return this.api.getComponent(id); } }