mirror of
https://github.com/koush/scrypted.git
synced 2026-02-11 09:34:27 +00:00
192 lines
9.1 KiB
TypeScript
192 lines
9.1 KiB
TypeScript
import { Device, DeviceManifest, EndpointAccessControlAllowOrigin, EventDetails, EventListenerOptions, EventListenerRegister, MediaManager, ScryptedDevice, ScryptedInterfaceDescriptor, ScryptedInterfaceProperty, ScryptedNativeId } from '@scrypted/types';
|
|
import debounce from 'lodash/debounce';
|
|
import { Plugin } from '../db-types';
|
|
import { Logger } from '../logger';
|
|
import { RpcPeer } from '../rpc';
|
|
import { ScryptedRuntime } from '../runtime';
|
|
import { getState } from '../state';
|
|
import { getPropertyInterfaces } from './descriptor';
|
|
import { PluginAPI, PluginAPIManagedListeners } from './plugin-api';
|
|
import { PluginHost } from './plugin-host';
|
|
import { checkProperty } from './plugin-state-check';
|
|
|
|
export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAPI {
|
|
pluginId: string;
|
|
typesVersion: string;
|
|
descriptors: { [scryptedInterface: string]: ScryptedInterfaceDescriptor };
|
|
propertyInterfaces: ReturnType<typeof getPropertyInterfaces>;
|
|
|
|
[RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS] = [
|
|
'onMixinEvent',
|
|
'onDeviceEvent',
|
|
'setStorage',
|
|
'setDeviceProperty',
|
|
'requestRestart',
|
|
"setState",
|
|
];
|
|
|
|
restartDebounced = debounce(async () => {
|
|
const host = this.scrypted.plugins[this.pluginId];
|
|
if (host?.api !== this) {
|
|
const logger = await this.getLogger(undefined);
|
|
logger.log('w', 'plugin restart was requested, but a different instance was found. restart cancelled.');
|
|
return;
|
|
}
|
|
|
|
const plugin = await this.scrypted.datastore.tryGet(Plugin, this.pluginId);
|
|
this.scrypted.runPlugin(plugin);
|
|
}, 15000);
|
|
|
|
constructor(public scrypted: ScryptedRuntime, pluginId: string, public pluginHost: PluginHost, public mediaManager: MediaManager) {
|
|
super();
|
|
this.pluginId = pluginId;
|
|
}
|
|
|
|
// do we care about mixin validation here?
|
|
// maybe to prevent/notify errant dangling events?
|
|
async onMixinEvent(id: string, nativeIdOrMixinDevice: ScryptedNativeId | any, eventInterface: string, eventData?: any) {
|
|
// nativeId code path has been deprecated in favor of mixin object 12/10/2021
|
|
const device = this.scrypted.findPluginDeviceById(id);
|
|
|
|
if (!nativeIdOrMixinDevice || typeof nativeIdOrMixinDevice === 'string') {
|
|
const nativeId: string = nativeIdOrMixinDevice;
|
|
// todo: deprecate this code path
|
|
const mixinProvider = this.scrypted.findPluginDevice(this.pluginId, nativeId);
|
|
const mixins: string[] = getState(device, ScryptedInterfaceProperty.mixins) || [];
|
|
if (!mixins.includes(mixinProvider._id))
|
|
throw new Error(`${mixinProvider._id} is not a mixin provider for ${id}`);
|
|
|
|
this.scrypted.findPluginDevice(this.pluginId, nativeId);
|
|
const tableEntry = this.scrypted.devices[device._id].handler.mixinTable.find(entry => entry.mixinProviderId === mixinProvider._id);
|
|
const { interfaces } = await tableEntry.entry;
|
|
if (!interfaces.has(eventInterface))
|
|
throw new Error(`${mixinProvider._id} does not mixin ${eventInterface} for ${id}`);
|
|
|
|
this.scrypted.stateManager.notifyInterfaceEvent(device, eventInterface, eventData);
|
|
}
|
|
else {
|
|
const mixin: object = nativeIdOrMixinDevice;
|
|
let mixinProviderId = await this.scrypted.devices[device._id]?.handler?.getMixinProviderId(id, mixin);
|
|
if (!mixinProviderId)
|
|
throw new Error(`${mixin} does not mixin ${eventInterface} for ${id}`);
|
|
|
|
if (mixinProviderId === true)
|
|
mixinProviderId = undefined;
|
|
// this.scrypted.stateManager.notifyInterfaceEvent(device, eventInterface, eventData);
|
|
this.scrypted.stateManager.notifyInterfaceEventFromMixin(device, eventInterface, eventData, mixinProviderId as string);
|
|
}
|
|
}
|
|
|
|
async getMediaManager(): Promise<MediaManager> {
|
|
return this.mediaManager;
|
|
}
|
|
|
|
async getLogger(nativeId: ScryptedNativeId): Promise<Logger> {
|
|
const device = this.scrypted.findPluginDevice(this.pluginId, nativeId);
|
|
return this.scrypted.getDeviceLogger(device);
|
|
}
|
|
|
|
async getComponent(id: string): Promise<any> {
|
|
if (id === 'setAccessControlAllowOrigin') {
|
|
return async (options: EndpointAccessControlAllowOrigin) => {
|
|
const { nativeId, origins } = options;
|
|
const device = this.scrypted.findPluginDevice(this.pluginId, nativeId);
|
|
if (!device)
|
|
throw new Error(`device not found for plugin id ${this.pluginId} native id ${nativeId}`);
|
|
return this.scrypted.corsControl.setCORS(device._id, origins);
|
|
}
|
|
}
|
|
return this.scrypted.getComponent(id);
|
|
}
|
|
|
|
setDeviceProperty(id: string, property: ScryptedInterfaceProperty, value: any): Promise<void> {
|
|
switch (property) {
|
|
case ScryptedInterfaceProperty.room:
|
|
case ScryptedInterfaceProperty.type:
|
|
case ScryptedInterfaceProperty.name:
|
|
const device = this.scrypted.findPluginDeviceById(id);
|
|
this.scrypted.stateManager.setPluginDeviceState(device, property, value);
|
|
this.scrypted.notifyPluginDeviceDescriptorChanged(device);
|
|
return;
|
|
default:
|
|
throw new Error(`Not allowed to set property ${property}`);
|
|
}
|
|
}
|
|
|
|
async setState(nativeId: ScryptedNativeId, key: string, value: any) {
|
|
checkProperty(key, value);
|
|
const { pluginId } = this;
|
|
const device = this.scrypted.findPluginDevice(pluginId, nativeId);
|
|
if (!device)
|
|
throw new Error(`device not found for plugin id ${pluginId} native id ${nativeId}`);
|
|
await this.scrypted.stateManager.setPluginDeviceStateFromMixin(device, key, value, this.propertyInterfaces?.[key], device._id);
|
|
}
|
|
|
|
async setStorage(nativeId: ScryptedNativeId, storage: { [key: string]: string }) {
|
|
const device = this.scrypted.findPluginDevice(this.pluginId, nativeId)
|
|
device.storage = storage;
|
|
this.scrypted.datastore.upsert(device);
|
|
this.scrypted.stateManager.notifyInterfaceEvent(device, 'Storage', undefined);
|
|
}
|
|
|
|
async onDevicesChanged(deviceManifest: DeviceManifest) {
|
|
const provider = this.scrypted.findPluginDevice(this.pluginId, deviceManifest.providerNativeId);
|
|
const existing = this.scrypted.findPluginDevices(this.pluginId).filter(p => p.state[ScryptedInterfaceProperty.providerId].value === provider._id);
|
|
const newIds = deviceManifest.devices.map(device => device.nativeId);
|
|
const toRemove = existing.filter(e => e.nativeId && !newIds.includes(e.nativeId));
|
|
|
|
for (const remove of toRemove) {
|
|
await this.scrypted.removeDevice(remove);
|
|
}
|
|
|
|
for (const upsert of deviceManifest.devices) {
|
|
upsert.providerNativeId = deviceManifest.providerNativeId;
|
|
const id = await this.pluginHost.upsertDevice(upsert);
|
|
this.scrypted.getDevice(id)?.probe().catch(() => { });
|
|
}
|
|
}
|
|
|
|
async onDeviceDiscovered(device: Device) {
|
|
return this.pluginHost.upsertDevice(device);
|
|
}
|
|
|
|
async onDeviceRemoved(nativeId: string) {
|
|
await this.scrypted.removeDevice(this.scrypted.findPluginDevice(this.pluginId, nativeId))
|
|
}
|
|
|
|
async onDeviceEvent(nativeId: any, eventInterface: any, eventData?: any) {
|
|
const plugin = this.scrypted.findPluginDevice(this.pluginId, nativeId);
|
|
this.scrypted.stateManager.notifyInterfaceEventFromMixin(plugin, eventInterface, eventData, plugin._id);
|
|
}
|
|
|
|
async getDeviceById<T>(id: string): Promise<T & ScryptedDevice> {
|
|
return this.scrypted.getDevice(id);
|
|
}
|
|
async listen(callback: (id: string, eventDetails: EventDetails, eventData: any) => void): Promise<EventListenerRegister> {
|
|
return this.manageListener(this.scrypted.stateManager.listen(callback));
|
|
}
|
|
async listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: any) => void): Promise<EventListenerRegister> {
|
|
// const device = this.scrypted.findPluginDeviceById(id);
|
|
// if (device) {
|
|
// const self = this.scrypted.findPluginDevice(this.pluginId);
|
|
// this.scrypted.getDeviceLogger(self).log('i', `requested listen ${getState(device, ScryptedInterfaceProperty.name)} ${JSON.stringify(event)}`);
|
|
// }
|
|
return this.manageListener(this.scrypted.stateManager.listenDevice(id, event, callback));
|
|
}
|
|
|
|
async removeDevice(id: string) {
|
|
return this.scrypted.removeDevice(this.scrypted.findPluginDeviceById(id));
|
|
}
|
|
|
|
async requestRestart() {
|
|
const logger = await this.getLogger(undefined);
|
|
logger.log('i', 'plugin restart was requested');
|
|
return this.restartDebounced();
|
|
}
|
|
|
|
async setScryptedInterfaceDescriptors(typesVersion: string, descriptors: { [scryptedInterface: string]: ScryptedInterfaceDescriptor }): Promise<void> {
|
|
this.typesVersion = typesVersion;
|
|
this.descriptors = descriptors;
|
|
this.propertyInterfaces = getPropertyInterfaces(descriptors);
|
|
}
|
|
} |