Files
scrypted/server/src/plugin/system.ts
2024-02-14 15:43:21 -08:00

245 lines
9.6 KiB
TypeScript

import { EventListener, EventListenerOptions, EventListenerRegister, Logger, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptor, ScryptedInterfaceDescriptors, ScryptedInterfaceProperty, ScryptedNativeId, SystemDeviceState, SystemManager } from "@scrypted/types";
import { EventRegistry } from "../event-registry";
import { PrimitiveProxyHandler, RpcPeer } from '../rpc';
// import type { PluginComponent } from "../services/plugin";
import { getInterfaceMethods, getInterfaceProperties, getPropertyInterfaces, isValidInterfaceMethod, propertyInterfaces } from "./descriptor";
import { PluginAPI } from "./plugin-api";
function newDeviceProxy(id: string, systemManager: SystemManagerImpl) {
const handler = new DeviceProxyHandler(id, systemManager);
return new Proxy(handler, handler);
}
class DeviceProxyHandler implements PrimitiveProxyHandler<any> {
customProperties: Map<string | number | symbol, any>;
device: Promise<ScryptedDevice>;
constructor(public id: string, public systemManager: SystemManagerImpl) {
}
toPrimitive() {
return `ScryptedDevice-${this.id}`
}
ownKeys(target: any): ArrayLike<string | symbol> {
const interfaces = new Set<string>(this.systemManager.state[this.id].interfaces.value);
const methods = getInterfaceMethods(this.systemManager.descriptors || ScryptedInterfaceDescriptors, interfaces);
const properties = getInterfaceProperties(this.systemManager.descriptors || ScryptedInterfaceDescriptors, interfaces);
return [...methods, ...properties];
}
getOwnPropertyDescriptor(target: any, p: string | symbol): PropertyDescriptor {
const interfaces = new Set<string>(this.systemManager.state[this.id].interfaces.value);
const methods = getInterfaceMethods(this.systemManager.descriptors || ScryptedInterfaceDescriptors, interfaces);
const prop = p.toString();
if (methods.includes(prop)) {
return {
configurable: true,
};
}
const properties = getInterfaceProperties(this.systemManager.descriptors || ScryptedInterfaceDescriptors, interfaces);
if (properties.includes(prop)) {
return {
configurable: true,
value: this.systemManager.state[this.id][prop]?.value
}
}
}
deleteProperty(target: any, p: string | symbol): boolean {
const prop = p.toString();
if (Object.keys(ScryptedInterfaceProperty).includes(prop))
return false;
this.customProperties ||= new Map();
this.customProperties.set(p, undefined);
return true;
}
set(target: any, p: string | symbol, newValue: any, receiver: any): boolean {
const prop = p.toString();
if (Object.keys(ScryptedInterfaceProperty).includes(prop))
return false;
this.customProperties ||= new Map();
this.customProperties.set(p, newValue);
return true;
}
get(target: any, p: PropertyKey, receiver: any): any {
if (p === 'id')
return this.id;
if (this.customProperties?.has(p))
return this.customProperties.get(p);
const handled = RpcPeer.handleFunctionInvocations(this, target, p, receiver);
if (handled)
return handled;
const interfaces = new Set<string>(this.systemManager.state[this.id].interfaces?.value || []);
const prop = p.toString();
const isValidProperty = this.systemManager.propertyInterfaces?.[prop] || propertyInterfaces[prop];
// this will also return old properties that should not exist on a device. ie, a disabled mixin provider.
// should this change?
if (isValidProperty)
return (this.systemManager.state[this.id] as any)?.[p]?.value;
if (!isValidInterfaceMethod(this.systemManager.descriptors || ScryptedInterfaceDescriptors, interfaces, 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<void> {
return this.systemManager.api.setDeviceProperty(this.id, ScryptedInterfaceProperty.name, name);
}
async setRoom(room: string): Promise<void> {
return this.systemManager.api.setDeviceProperty(this.id, ScryptedInterfaceProperty.room, room);
}
async setType(type: ScryptedDeviceType): Promise<void> {
return this.systemManager.api.setDeviceProperty(this.id, ScryptedInterfaceProperty.type, type);
}
async setMixins(mixins: string[]) {
const plugins = await this.systemManager.getComponent('plugins');// as PluginComponent;
await plugins.setMixins(this.id, mixins);
}
async probe(): Promise<boolean> {
return this.apply(() => 'probe', undefined, []);
}
}
class EventListenerRegisterImpl implements EventListenerRegister {
promise: Promise<EventListenerRegister>;
constructor(promise: Promise<EventListenerRegister>) {
this.promise = promise;
}
async removeListener(): Promise<void> {
try {
const register = await this.promise;
this.promise = undefined;
register?.removeListener();
}
catch (e) {
console.error('removeListener', e);
}
}
}
function makeOneWayCallback<T>(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();
typesVersion: string;
descriptors: { [scryptedInterface: string]: ScryptedInterfaceDescriptor };
propertyInterfaces: ReturnType<typeof getPropertyInterfaces>;
getDeviceState(id: string) {
return this.state[id];
}
getSystemState(): { [id: string]: { [property: string]: SystemDeviceState } } {
return this.state;
}
getDeviceById(idOrPluginId: string, nativeId?: ScryptedNativeId): any {
let id: string;
if (this.state[idOrPluginId]) {
// don't allow invalid input on nativeId, must be nullish if there is an exact id match.
if (nativeId != null)
return;
id = idOrPluginId;
}
else {
for (const check of Object.keys(this.state)) {
const state = this.state[check];
if (!state)
continue;
if (state[ScryptedInterfaceProperty.pluginId]?.value === idOrPluginId) {
// null and undefined should match here.
if (state[ScryptedInterfaceProperty.nativeId]?.value == nativeId) {
id = check;
break;
}
}
}
}
if (!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 { watch } = (options || {}) as EventListenerOptions;
// passive watching can be fast pathed to observe local state
if (watch)
return this.events.listenDevice(id, options, (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<any> {
return this.api.getComponent(id);
}
setScryptedInterfaceDescriptors(typesVersion: string, descriptors: { [scryptedInterface: string]: ScryptedInterfaceDescriptor }): Promise<void> {
this.typesVersion = typesVersion;
this.descriptors = descriptors;
this.propertyInterfaces = getPropertyInterfaces(descriptors);
return this.api.setScryptedInterfaceDescriptors(typesVersion, descriptors);
}
}