import { ScryptedNativeId, ScryptedDevice, Device, DeviceManifest, EventDetails, EventListenerOptions, EventListenerRegister, ScryptedInterfaceProperty, MediaManager, HttpRequest } from '@scrypted/sdk/types' import { ScryptedRuntime } from '../runtime'; import { Plugin } from '../db-types'; import { PluginAPI, PluginAPIManagedListeners } from './plugin-api'; import { Logger } from '../logger'; import { getState } from '../state'; import { PluginHost } from './plugin-host'; import debounce from 'lodash/debounce'; import { PROPERTY_PROXY_ONEWAY_METHODS } from '../rpc'; export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAPI { pluginId: string; [PROPERTY_PROXY_ONEWAY_METHODS] = [ 'onMixinEvent', 'onDeviceEvent', 'setStorage', 'ioSend', 'ioClose', 'setDeviceProperty', 'deliverPush', 'requestRestart', "setState", ]; restartDebounced = debounce(async () => { const host = this.scrypted.plugins[this.pluginId]; const logger = await this.getLogger(undefined); if (host.api !== this) { 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, plugin: Plugin, public pluginHost: PluginHost, public mediaManager: MediaManager) { super(); this.pluginId = plugin._id; } // do we care about mixin validation here? // maybe to prevent/notify errant dangling events? async onMixinEvent(id: string, nativeIdOrMixinDevice: ScryptedNativeId|any, eventInterface: any, 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') { // todo: deprecate this code path const mixinProvider = this.scrypted.findPluginDevice(this.pluginId, nativeIdOrMixinDevice); 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, nativeIdOrMixinDevice); 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}`); } else { if (!await this.scrypted.devices[device._id]?.handler?.isMixin(id, nativeIdOrMixinDevice)) { const mixinProvider = this.scrypted.findPluginDevice(this.pluginId, nativeIdOrMixinDevice); throw new Error(`${mixinProvider?._id} does not mixin ${eventInterface} for ${id}`); } } this.scrypted.stateManager.notifyInterfaceEvent(device, eventInterface, eventData); } async getMediaManager(): Promise { return this.mediaManager; } async deliverPush(endpoint: string, httpRequest: HttpRequest) { return this.scrypted.deliverPush(endpoint, httpRequest); } async getLogger(nativeId: ScryptedNativeId): Promise { const device = this.scrypted.findPluginDevice(this.pluginId, nativeId); return this.scrypted.getDeviceLogger(device); } getComponent(id: string): Promise { return this.scrypted.getComponent(id); } setDeviceProperty(id: string, property: ScryptedInterfaceProperty, value: any): Promise { 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 ioClose(id: string) { this.pluginHost.io.clients[id]?.close(); this.pluginHost.ws[id]?.close(); } async ioSend(id: string, message: string) { this.pluginHost.io.clients[id]?.send(message); this.pluginHost.ws[id]?.send(message); } async setState(nativeId: ScryptedNativeId, key: string, value: any) { this.scrypted.stateManager.setPluginState(this.pluginId, nativeId, key, value); } 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 existing = this.scrypted.findPluginDevices(this.pluginId).filter(p => p.nativeId == deviceManifest.providerNativeId); 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) { await this.pluginHost.upsertDevice(upsert); } } 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.notifyInterfaceEvent(plugin, eventInterface, eventData); } async getDeviceById(id: string): Promise { return this.scrypted.getDevice(id); } async listen(EventListener: (id: string, eventDetails: EventDetails, eventData: object) => void): Promise { return this.manageListener(this.scrypted.stateManager.listen(EventListener)); } async listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: object) => void): Promise { 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(); } }