@scrypted/server 0.4.8 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @scrypted/server might be problematic. Click here for more details.

Files changed (41) hide show
  1. package/dist/event-registry.js +24 -10
  2. package/dist/event-registry.js.map +1 -1
  3. package/dist/media-helpers.js +4 -0
  4. package/dist/media-helpers.js.map +1 -1
  5. package/dist/mixin/mixin-cycle.js +2 -1
  6. package/dist/mixin/mixin-cycle.js.map +1 -1
  7. package/dist/plugin/plugin-device.js +6 -6
  8. package/dist/plugin/plugin-device.js.map +1 -1
  9. package/dist/plugin/plugin-host-api.js +18 -10
  10. package/dist/plugin/plugin-host-api.js.map +1 -1
  11. package/dist/plugin/plugin-lazy-remote.js +4 -2
  12. package/dist/plugin/plugin-lazy-remote.js.map +1 -1
  13. package/dist/plugin/plugin-remote.js +36 -16
  14. package/dist/plugin/plugin-remote.js.map +1 -1
  15. package/dist/plugin/system.js +2 -6
  16. package/dist/plugin/system.js.map +1 -1
  17. package/dist/rpc.js +3 -0
  18. package/dist/rpc.js.map +1 -1
  19. package/dist/runtime.js +14 -0
  20. package/dist/runtime.js.map +1 -1
  21. package/dist/scrypted-server-main.js +0 -6
  22. package/dist/scrypted-server-main.js.map +1 -1
  23. package/dist/services/plugin.js +2 -0
  24. package/dist/services/plugin.js.map +1 -1
  25. package/dist/state.js +57 -16
  26. package/dist/state.js.map +1 -1
  27. package/package.json +2 -2
  28. package/src/event-registry.ts +29 -10
  29. package/src/media-helpers.ts +5 -0
  30. package/src/mixin/mixin-cycle.ts +1 -1
  31. package/src/plugin/plugin-api.ts +4 -0
  32. package/src/plugin/plugin-device.ts +6 -6
  33. package/src/plugin/plugin-host-api.ts +20 -10
  34. package/src/plugin/plugin-lazy-remote.ts +5 -3
  35. package/src/plugin/plugin-remote.ts +38 -18
  36. package/src/plugin/system.ts +2 -6
  37. package/src/rpc.ts +3 -0
  38. package/src/runtime.ts +16 -1
  39. package/src/scrypted-server-main.ts +0 -7
  40. package/src/services/plugin.ts +2 -0
  41. package/src/state.ts +68 -20
@@ -1,4 +1,4 @@
1
- import { EventDetails, EventListenerOptions, EventListenerRegister, ScryptedInterface, SystemDeviceState } from "@scrypted/types";
1
+ import { EventDetails, EventListenerOptions, EventListenerRegister, ScryptedInterface } from "@scrypted/types";
2
2
 
3
3
  export class EventListenerRegisterImpl implements EventListenerRegister {
4
4
  removeListener: () => void;
@@ -8,6 +8,18 @@ export class EventListenerRegisterImpl implements EventListenerRegister {
8
8
  }
9
9
  }
10
10
 
11
+ export function getMixinEventName(options: string | EventListenerOptions) {
12
+ let { event, mixinId } = (options || {}) as EventListenerOptions;
13
+ if (!event && typeof options === 'string')
14
+ event = options as string;
15
+ if (!event)
16
+ event = undefined;
17
+ if (!mixinId)
18
+ return event;
19
+ let ret = `${event}-mixin-${mixinId}`;
20
+ return ret;
21
+ }
22
+
11
23
  // todo: storage should only go to host plugin
12
24
  const allowedEventInterfaces = new Set<string>([ScryptedInterface.ScryptedDevice, 'Logger', 'Storage'])
13
25
 
@@ -25,11 +37,7 @@ export class EventRegistry {
25
37
  }
26
38
 
27
39
  listenDevice(id: string, options: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: any) => void): EventListenerRegister {
28
- let { event } = (options || {}) as EventListenerOptions;
29
- if (!event && typeof options === 'string')
30
- event = options as string;
31
- if (!event)
32
- event = undefined;
40
+ let event = getMixinEventName(options)
33
41
  const token = `${id}#${event}`;
34
42
  let events = this.listeners[token];
35
43
  if (!events) {
@@ -44,23 +52,34 @@ export class EventRegistry {
44
52
  });
45
53
  }
46
54
 
47
- notify(id: string|undefined, eventTime: number, eventInterface: string, property: string|undefined, value: any, changed?: boolean): boolean {
55
+ notify(id: string | undefined, eventTime: number, eventInterface: string, property: string | undefined, value: any, options?: {
56
+ changed?: boolean;
57
+ mixinId?: string;
58
+ }): boolean {
59
+ const { changed, mixinId } = options || {};
48
60
  // prevent property event noise
49
61
  if (property && !changed)
50
62
  return false;
51
63
 
52
64
  const eventDetails: EventDetails = {
53
- eventId: Math.random().toString(),
54
- changed,
65
+ eventId: undefined,
55
66
  eventInterface,
56
67
  eventTime,
57
68
  property,
69
+ mixinId,
58
70
  };
59
71
 
72
+ return this.notifyEventDetails(id, eventDetails, value);
73
+ }
74
+
75
+ notifyEventDetails(id: string | undefined, eventDetails: EventDetails, value: any, eventInterface?: string) {
76
+ eventDetails.eventId ||= Math.random().toString(36).substring(2);
77
+ eventInterface ||= eventDetails.eventInterface;
78
+
60
79
  // system listeners only get state changes.
61
80
  // there are many potentially noisy stateless events, like
62
81
  // object detection and settings changes.
63
- if (property || allowedEventInterfaces.has(eventInterface)) {
82
+ if ((eventDetails.property && !eventDetails.mixinId) || allowedEventInterfaces.has(eventInterface)) {
64
83
  for (const event of this.systemListeners) {
65
84
  event(id, eventDetails, value);
66
85
  }
@@ -25,6 +25,9 @@ export function safeKillFFmpeg(cp: ChildProcess) {
25
25
  }
26
26
 
27
27
  export function ffmpegLogInitialOutput(console: Console, cp: ChildProcess, forever?: boolean, storage?: Storage) {
28
+ if (!console)
29
+ return;
30
+
28
31
  const SCRYPTED_FFMPEG_NOISY = !!process.env.SCRYPTED_FFMPEG_NOISY || !!storage?.getItem('SCRYPTED_FFMPEG_NOISY');
29
32
 
30
33
  function logger(log: (str: string) => void): (buffer: Buffer) => void {
@@ -55,6 +58,8 @@ export function ffmpegLogInitialOutput(console: Console, cp: ChildProcess, forev
55
58
  }
56
59
 
57
60
  export function safePrintFFmpegArguments(console: Console, args: string[]) {
61
+ if (!console)
62
+ return;
58
63
  const ret = [];
59
64
  let redactNext = false;
60
65
  for (const arg of args) {
@@ -2,7 +2,7 @@ import { ScryptedInterfaceProperty } from "@scrypted/types";
2
2
  import { ScryptedRuntime } from "../runtime";
3
3
  import { getState } from "../state";
4
4
 
5
- function getMixins(scrypted: ScryptedRuntime, id: string) {
5
+ export function getMixins(scrypted: ScryptedRuntime, id: string) {
6
6
  const pluginDevice = scrypted.findPluginDeviceById(id);
7
7
  if (!pluginDevice)
8
8
  return [];
@@ -171,7 +171,11 @@ export interface PluginRemote {
171
171
  setSystemState(state: { [id: string]: { [property: string]: SystemDeviceState } }): Promise<void>;
172
172
  setNativeId(nativeId: ScryptedNativeId, id: string, storage: { [key: string]: any }): Promise<void>;
173
173
  updateDeviceState(id: string, state: { [property: string]: SystemDeviceState }): Promise<void>;
174
+ /**
175
+ * @deprecated
176
+ */
174
177
  notify(id: string, eventTime: number, eventInterface: string, property: string | undefined, value: SystemDeviceState | any, changed?: boolean): Promise<void>;
178
+ notify(id: string, eventDetails: EventDetails, eventData: SystemDeviceState | any): Promise<void>;
175
179
 
176
180
  ioEvent(id: string, event: string, message?: any): Promise<void>;
177
181
 
@@ -56,23 +56,23 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler<any>, Scr
56
56
  // allow mixins in the process of being released to manage final
57
57
  // events, etc, before teardown.
58
58
  this.releasing.add(proxy);
59
- mixinProvider?.releaseMixin(this.id, proxy);
59
+ mixinProvider?.releaseMixin(this.id, proxy).catch(() => { });
60
60
  await sleep(1000);
61
61
  this.releasing.delete(proxy);
62
62
  })().catch(() => { });
63
63
  }
64
64
 
65
- async isMixin(id: string, mixinDevice: any) {
65
+ async getMixinProviderId(id: string, mixinDevice: any) {
66
66
  if (this.releasing.has(mixinDevice))
67
67
  return true;
68
68
  await this.scrypted.devices[id].handler.ensureProxy();
69
69
  for (const mixin of this.scrypted.devices[id].handler.mixinTable) {
70
70
  const { proxy } = await mixin.entry;
71
71
  if (proxy === mixinDevice) {
72
- return true;
72
+ return mixin.mixinProviderId || id;
73
73
  }
74
74
  }
75
- return false;
75
+ return undefined;
76
76
  }
77
77
 
78
78
  // should this be async?
@@ -260,7 +260,7 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler<any>, Scr
260
260
  const propertyInterfaces = getPropertyInterfaces(host.api.descriptors || ScryptedInterfaceDescriptors);
261
261
  // todo: remove this and pass the setter directly.
262
262
  const deviceState = await host.remote.createDeviceState(this.id,
263
- async (property, value) => this.scrypted.stateManager.setPluginDeviceState(pluginDevice, property, value, propertyInterfaces[property]));
263
+ async (property, value) => this.scrypted.stateManager.setPluginDeviceStateFromMixin(pluginDevice, property, value, propertyInterfaces[property], mixinId));
264
264
  const mixinProxy = await mixinProvider.getMixin(wrappedProxy, previousInterfaces as ScryptedInterface[], deviceState);
265
265
  if (!mixinProxy)
266
266
  throw new PluginError(`mixin provider ${mixinId} did not return mixin for ${this.id}`);
@@ -370,7 +370,7 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler<any>, Scr
370
370
  const pluginDevice = this.scrypted.findPluginDeviceById(mixin.mixinProviderId || this.id);
371
371
  const plugin = this.scrypted.plugins[pluginDevice.pluginId];
372
372
  let methods = new Set<string>(getInterfaceMethods(ScryptedInterfaceDescriptors, entry.interfaces))
373
- if (plugin.api.descriptors)
373
+ if (plugin?.api.descriptors)
374
374
  methods = new Set<string>([...methods, ...getInterfaceMethods(plugin.api.descriptors, entry.interfaces)]);
375
375
  entry.methods = methods;
376
376
  }
@@ -61,14 +61,20 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
61
61
  const { interfaces } = await tableEntry.entry;
62
62
  if (!interfaces.has(eventInterface))
63
63
  throw new Error(`${mixinProvider._id} does not mixin ${eventInterface} for ${id}`);
64
+
65
+ this.scrypted.stateManager.notifyInterfaceEvent(device, eventInterface, eventData);
64
66
  }
65
67
  else {
66
68
  const mixin: object = nativeIdOrMixinDevice;
67
- if (!await this.scrypted.devices[device._id]?.handler?.isMixin(id, mixin)) {
69
+ let mixinProviderId = await this.scrypted.devices[device._id]?.handler?.getMixinProviderId(id, mixin);
70
+ if (!mixinProviderId)
68
71
  throw new Error(`${mixin} does not mixin ${eventInterface} for ${id}`);
69
- }
72
+
73
+ if (mixinProviderId === true)
74
+ mixinProviderId = undefined;
75
+ // this.scrypted.stateManager.notifyInterfaceEvent(device, eventInterface, eventData);
76
+ this.scrypted.stateManager.notifyInterfaceEventFromMixin(device, eventInterface, eventData, mixinProviderId as string);
70
77
  }
71
- this.scrypted.stateManager.notifyInterfaceEvent(device, eventInterface, eventData);
72
78
  }
73
79
 
74
80
  async getMediaManager(): Promise<MediaManager> {
@@ -100,7 +106,11 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
100
106
 
101
107
  async setState(nativeId: ScryptedNativeId, key: string, value: any) {
102
108
  checkProperty(key, value);
103
- this.scrypted.stateManager.setPluginState(this.pluginId, nativeId, this.propertyInterfaces?.[key], key, value);
109
+ const { pluginId } = this;
110
+ const device = this.scrypted.findPluginDevice(pluginId, nativeId);
111
+ if (!device)
112
+ throw new Error(`device not found for plugin id ${pluginId} native id ${nativeId}`);
113
+ await this.scrypted.stateManager.setPluginDeviceStateFromMixin(device, key, value, this.propertyInterfaces?.[key], device._id);
104
114
  }
105
115
 
106
116
  async setStorage(nativeId: ScryptedNativeId, storage: { [key: string]: string }) {
@@ -136,7 +146,7 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
136
146
 
137
147
  async onDeviceEvent(nativeId: any, eventInterface: any, eventData?: any) {
138
148
  const plugin = this.scrypted.findPluginDevice(this.pluginId, nativeId);
139
- this.scrypted.stateManager.notifyInterfaceEvent(plugin, eventInterface, eventData);
149
+ this.scrypted.stateManager.notifyInterfaceEventFromMixin(plugin, eventInterface, eventData, plugin._id);
140
150
  }
141
151
 
142
152
  async getDeviceById<T>(id: string): Promise<T & ScryptedDevice> {
@@ -146,11 +156,11 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
146
156
  return this.manageListener(this.scrypted.stateManager.listen(callback));
147
157
  }
148
158
  async listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: any) => void): Promise<EventListenerRegister> {
149
- const device = this.scrypted.findPluginDeviceById(id);
150
- if (device) {
151
- const self = this.scrypted.findPluginDevice(this.pluginId);
152
- this.scrypted.getDeviceLogger(self).log('i', `requested listen ${getState(device, ScryptedInterfaceProperty.name)} ${JSON.stringify(event)}`);
153
- }
159
+ // const device = this.scrypted.findPluginDeviceById(id);
160
+ // if (device) {
161
+ // const self = this.scrypted.findPluginDevice(this.pluginId);
162
+ // this.scrypted.getDeviceLogger(self).log('i', `requested listen ${getState(device, ScryptedInterfaceProperty.name)} ${JSON.stringify(event)}`);
163
+ // }
154
164
  return this.manageListener(this.scrypted.stateManager.listenDevice(id, event, callback));
155
165
  }
156
166
 
@@ -1,4 +1,4 @@
1
- import { ScryptedNativeId, SystemDeviceState } from '@scrypted/types'
1
+ import { EventDetails, ScryptedNativeId, SystemDeviceState } from '@scrypted/types'
2
2
  import { PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
3
3
 
4
4
  /**
@@ -42,7 +42,9 @@ import { PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
42
42
  }
43
43
  return this.remote.updateDeviceState(id, state);
44
44
  }
45
- async notify(id: string, eventTime: number, eventInterface: string, property: string, propertyState: SystemDeviceState, changed?: boolean): Promise<void> {
45
+ // TODO: deprecate/clean up this signature
46
+ // 12/30/2022
47
+ async notify(id: string, eventTimeOrDetails: number| EventDetails, eventInterfaceOrData: string | SystemDeviceState | any, property?: string, value?: SystemDeviceState | any, changed?: boolean) {
46
48
  try {
47
49
  if (!this.remote)
48
50
  await this.remoteReadyPromise;
@@ -50,7 +52,7 @@ import { PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
50
52
  catch (e) {
51
53
  return;
52
54
  }
53
- return this.remote.notify(id, eventTime, eventInterface, property, propertyState, changed);
55
+ return this.remote.notify(id, eventTimeOrDetails as any, eventInterfaceOrData, property, value, changed);
54
56
  }
55
57
  async ioEvent(id: string, event: string, message?: any): Promise<void> {
56
58
  if (!this.remote)
@@ -1,4 +1,4 @@
1
- import { Device, DeviceManager, DeviceManifest, DeviceState, EndpointManager, Logger, MediaManager, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, ScryptedStatic, SystemDeviceState, SystemManager } from '@scrypted/types';
1
+ import { Device, DeviceManager, DeviceManifest, DeviceState, EndpointManager, EventDetails, Logger, MediaManager, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, ScryptedStatic, SystemDeviceState, SystemManager } from '@scrypted/types';
2
2
  import { RpcPeer, RPCResultError } from '../rpc';
3
3
  import { AccessControls } from './acl';
4
4
  import { BufferSerializer } from './buffer-serializer';
@@ -175,8 +175,6 @@ class DeviceStateProxyHandler implements ProxyHandler<any> {
175
175
  checkProperty(p.toString(), value);
176
176
  const now = Date.now();
177
177
  this.deviceManager.systemManager.state[this.id][p as string] = {
178
- lastEventTime: now,
179
- stateTime: now,
180
178
  value,
181
179
  };
182
180
  this.setState(p.toString(), value);
@@ -367,7 +365,7 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId:
367
365
 
368
366
  const accessControls: AccessControls = peer.tags.acl;
369
367
 
370
- const getAccessControlDeviceState = (id: string, state?: { [property: string]: SystemDeviceState } ) => {
368
+ const getAccessControlDeviceState = (id: string, state?: { [property: string]: SystemDeviceState }) => {
371
369
  state = state || getSystemState()[id];
372
370
  if (accessControls && state) {
373
371
  state = Object.assign({}, state);
@@ -420,11 +418,11 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId:
420
418
  return;
421
419
  }
422
420
 
423
- if (eventDetails.property) {
424
- remote.notify(id, eventDetails.eventTime, eventDetails.eventInterface, eventDetails.property, getSystemState()[id]?.[eventDetails.property], eventDetails.changed).catch(() => { });
421
+ if (eventDetails.property && !eventDetails.mixinId) {
422
+ remote.notify(id, eventDetails, getSystemState()[id]?.[eventDetails.property]).catch(() => { });
425
423
  }
426
424
  else {
427
- remote.notify(id, eventDetails.eventTime, eventDetails.eventInterface, eventDetails.property, eventData, eventDetails.changed).catch(() => { });
425
+ remote.notify(id, eventDetails, eventData).catch(() => { });
428
426
  }
429
427
  });
430
428
 
@@ -567,26 +565,48 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
567
565
  async updateDeviceState(id: string, state: { [property: string]: SystemDeviceState }) {
568
566
  if (!state) {
569
567
  delete systemManager.state[id];
570
- systemManager.events.notify(undefined, undefined, ScryptedInterface.ScryptedDevice, ScryptedInterfaceProperty.id, id, true);
568
+ systemManager.events.notify(undefined, undefined, ScryptedInterface.ScryptedDevice, ScryptedInterfaceProperty.id, id, { changed: true });
571
569
  }
572
570
  else {
573
571
  systemManager.state[id] = state;
574
- systemManager.events.notify(id, undefined, ScryptedInterface.ScryptedDevice, undefined, state, true);
572
+ systemManager.events.notify(id, undefined, ScryptedInterface.ScryptedDevice, undefined, state, { changed: true });
575
573
  }
576
574
  },
577
575
 
578
- async notify(id: string, eventTime: number, eventInterface: string, property: string, value: SystemDeviceState | any, changed?: boolean) {
579
- if (property) {
580
- const state = systemManager.state?.[id];
581
- if (!state) {
582
- log.w(`state not found for ${id}`);
583
- return;
576
+ async notify(id: string, eventTimeOrDetails: number| EventDetails, eventInterfaceOrData: string | SystemDeviceState | any, property?: string, value?: SystemDeviceState | any, changed?: boolean) {
577
+ if (typeof eventTimeOrDetails === 'number') {
578
+ // TODO: remove legacy code path
579
+ // 12/30/2022
580
+ const eventTime = eventTimeOrDetails as number;
581
+ const eventInterface = eventInterfaceOrData as string;
582
+ if (property) {
583
+ const state = systemManager.state?.[id];
584
+ if (!state) {
585
+ log.w(`state not found for ${id}`);
586
+ return;
587
+ }
588
+ state[property] = value;
589
+ systemManager.events.notify(id, eventTime, eventInterface, property, value.value, { changed });
590
+ }
591
+ else {
592
+ systemManager.events.notify(id, eventTime, eventInterface, property, value, { changed });
584
593
  }
585
- state[property] = value;
586
- systemManager.events.notify(id, eventTime, eventInterface, property, value.value, changed);
587
594
  }
588
595
  else {
589
- systemManager.events.notify(id, eventTime, eventInterface, property, value, changed);
596
+ const eventDetails = eventTimeOrDetails as EventDetails;
597
+ const eventData = eventInterfaceOrData as any;
598
+ if (eventDetails.property && !eventDetails.mixinId) {
599
+ const state = systemManager.state?.[id];
600
+ if (!state) {
601
+ log.w(`state not found for ${id}`);
602
+ return;
603
+ }
604
+ state[eventDetails.property] = eventData;
605
+ systemManager.events.notifyEventDetails(id, eventDetails, eventData.value);
606
+ }
607
+ else {
608
+ systemManager.events.notifyEventDetails(id, eventDetails, eventData);
609
+ }
590
610
  }
591
611
  },
592
612
 
@@ -166,15 +166,11 @@ export class SystemManagerImpl implements SystemManager {
166
166
  return this.events.listen(makeOneWayCallback((id, eventDetails, eventData) => callback(this.getDeviceById(id), eventDetails, eventData)));
167
167
  }
168
168
  listenDevice(id: string, options: string | EventListenerOptions, callback: EventListener): EventListenerRegister {
169
- let { event, watch } = (options || {}) as EventListenerOptions;
170
- if (!event && typeof options === 'string')
171
- event = options as string;
172
- if (!event)
173
- event = undefined;
169
+ let { watch } = (options || {}) as EventListenerOptions;
174
170
 
175
171
  // passive watching can be fast pathed to observe local state
176
172
  if (watch)
177
- return this.events.listenDevice(id, event, (eventDetails, eventData) => callback(this.getDeviceById(id), eventDetails, eventData));
173
+ return this.events.listenDevice(id, options, (eventDetails, eventData) => callback(this.getDeviceById(id), eventDetails, eventData));
178
174
 
179
175
  return new EventListenerRegisterImpl(this.api.listenDevice(id, options, makeOneWayCallback((eventDetails, eventData) => callback(this.getDeviceById(id), eventDetails, eventData))));
180
176
  }
package/src/rpc.ts CHANGED
@@ -154,6 +154,9 @@ class RpcProxy implements PrimitiveProxyHandler<any> {
154
154
 
155
155
  if (this.proxyOneWayMethods?.includes?.(method)) {
156
156
  rpcApply.oneway = true;
157
+ // a oneway callable object doesn't need to be in the JSON payload.
158
+ if (method === null)
159
+ delete rpcApply.method;
157
160
  this.peer.send(rpcApply, undefined, serializationContext);
158
161
  return Promise.resolve();
159
162
  }
package/src/runtime.ts CHANGED
@@ -20,7 +20,7 @@ import { getDisplayName, getDisplayRoom, getDisplayType, getProvidedNameOrDefaul
20
20
  import { IOServer } from './io';
21
21
  import { Level } from './level';
22
22
  import { LogEntry, Logger, makeAlertId } from './logger';
23
- import { hasMixinCycle } from './mixin/mixin-cycle';
23
+ import { getMixins, hasMixinCycle } from './mixin/mixin-cycle';
24
24
  import { AccessControls } from './plugin/acl';
25
25
  import { PluginDebug } from './plugin/plugin-debug';
26
26
  import { PluginDeviceProxyHandler } from './plugin/plugin-device';
@@ -652,10 +652,25 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
652
652
  this.setupPluginHostAutoRestart(pluginHost);
653
653
  this.plugins[pluginId] = pluginHost;
654
654
 
655
+ const pluginDeviceSet = new Set<string>();
655
656
  for (const pluginDevice of pluginDevices) {
657
+ if (pluginDeviceSet.has(pluginDevice._id))
658
+ continue;
659
+ pluginDeviceSet.add(pluginDevice._id);
656
660
  this.getDevice(pluginDevice._id)?.probe().catch(() => { });
657
661
  }
658
662
 
663
+ for (const pluginDevice of Object.values(this.pluginDevices)) {
664
+ const { _id } = pluginDevice;
665
+ if (pluginDeviceSet.has(_id))
666
+ continue;
667
+ for (const mixinId of getMixins(this, _id)) {
668
+ if (pluginDeviceSet.has(mixinId)) {
669
+ this.getDevice(_id)?.probe().catch(() => { });
670
+ }
671
+ }
672
+ }
673
+
659
674
  return pluginHost;
660
675
  }
661
676
 
@@ -161,13 +161,6 @@ async function start() {
161
161
  next();
162
162
  })
163
163
 
164
- app.options('*', (req, res) => {
165
- // add more?
166
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
167
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With');
168
- res.send(200);
169
- });
170
-
171
164
  const authSalt = crypto.randomBytes(16);
172
165
  const createAuthorizationToken = (login_user_token: string) => {
173
166
  const salted = login_user_token + authSalt;
@@ -70,6 +70,8 @@ export class PluginComponent {
70
70
  async kill(pluginId: string) {
71
71
  return this.scrypted.plugins[pluginId]?.kill();
72
72
  }
73
+ // TODO: Remove this, ScryptedPlugin exists now.
74
+ // 12/29/2022
73
75
  async getPackageJson(pluginId: string) {
74
76
  return this.scrypted.getPackageJson(pluginId);
75
77
  }
package/src/state.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { EventDetails, EventListenerOptions, EventListenerRegister, Refresh, ScryptedInterface, ScryptedInterfaceProperty, ScryptedNativeId, SystemDeviceState } from "@scrypted/types";
2
2
  import throttle from 'lodash/throttle';
3
3
  import { PluginDevice } from "./db-types";
4
- import { EventListenerRegisterImpl, EventRegistry } from "./event-registry";
5
- import { allInterfaceProperties, propertyInterfaces } from "./plugin/descriptor";
6
- import { RefreshSymbol } from "./plugin/plugin-device";
4
+ import { EventListenerRegisterImpl, EventRegistry, getMixinEventName } from "./event-registry";
5
+ import { propertyInterfaces } from "./plugin/descriptor";
6
+ import { QueryInterfaceSymbol, RefreshSymbol } from "./plugin/plugin-device";
7
7
  import { ScryptedRuntime } from "./runtime";
8
8
  import { sleep } from "./sleep";
9
9
 
@@ -34,25 +34,74 @@ export class ScryptedStateManager extends EventRegistry {
34
34
  this.scrypted = scrypted;
35
35
  }
36
36
 
37
- setPluginState(pluginId: string, nativeId: ScryptedNativeId, eventInterface: ScryptedInterface, property: string, value: any) {
38
- const device = this.scrypted.findPluginDevice(pluginId, nativeId);
37
+ async getImplementerId(pluginDevice: PluginDevice, eventInterface: ScryptedInterface | string) {
38
+ if (!eventInterface)
39
+ throw new Error(`ScryptedInterface is required`);
40
+
41
+ const device = this.scrypted.getDevice(pluginDevice._id);
39
42
  if (!device)
40
- throw new Error(`device not found for plugin id ${pluginId} native id ${nativeId}`);
41
- this.setPluginDeviceState(device, property, value, eventInterface);
43
+ throw new Error(`device ${pluginDevice._id} not found?`);
44
+
45
+ const implementerId: string = await (device as any)[QueryInterfaceSymbol](eventInterface);
46
+ return implementerId;
47
+ }
48
+
49
+ async notifyInterfaceEventFromMixin(pluginDevice: PluginDevice, eventInterface: ScryptedInterface | string, value: any, mixinId: string) {
50
+ // TODO: figure out how to clean this up this hack. For now,
51
+ // Settings interface is allowed to bubble from mixin devices..
52
+ if (eventInterface !== ScryptedInterface.Settings) {
53
+ const implementerId = await this.getImplementerId(pluginDevice, eventInterface);
54
+ if (implementerId !== mixinId) {
55
+ const event = getMixinEventName({
56
+ event: eventInterface,
57
+ mixinId,
58
+ });
59
+
60
+ this.notifyEventDetails(pluginDevice._id, {
61
+ eventId: undefined,
62
+ eventInterface,
63
+ eventTime: Date.now(),
64
+ mixinId,
65
+ }, value, event);
66
+
67
+ return;
68
+ }
69
+ }
70
+
71
+ this.notify(pluginDevice?._id, Date.now(), eventInterface, undefined, value);
72
+ }
73
+
74
+ async setPluginDeviceStateFromMixin(pluginDevice: PluginDevice, property: string, value: any, eventInterface: ScryptedInterface, mixinId: string) {
75
+ const implementerId = await this.getImplementerId(pluginDevice, eventInterface);
76
+ if (implementerId !== mixinId) {
77
+ const event = getMixinEventName({
78
+ event: eventInterface,
79
+ mixinId,
80
+ });
81
+ this.scrypted.getDeviceLogger(pluginDevice).log('i', `${property}: ${value} (mixin)`);
82
+ this.notifyEventDetails(pluginDevice._id, {
83
+ eventId: undefined,
84
+ eventInterface,
85
+ eventTime: Date.now(),
86
+ mixinId,
87
+ property,
88
+ }, value, event);
89
+ return false;
90
+ }
91
+
92
+ return this.setPluginDeviceState(pluginDevice, property, value, eventInterface);
42
93
  }
43
94
 
44
95
  setPluginDeviceState(device: PluginDevice, property: string, value: any, eventInterface?: ScryptedInterface) {
45
96
  eventInterface = eventInterface || propertyInterfaces[property];
46
97
  if (!eventInterface)
47
- throw new Error(`${property} is not a valid property`);
98
+ throw new Error(`eventInterface must be provided`);
48
99
 
49
100
  const changed = setState(device, property, value);
50
101
 
51
- const eventTime = device?.state?.[property]?.lastEventTime;
52
-
53
102
  if (eventInterface !== ScryptedInterface.ScryptedDevice) {
54
- if (this.notify(device?._id, eventTime, eventInterface, property, value, changed) && device) {
55
- this.scrypted.getDeviceLogger(device).log('i', `state change: ${property} ${value}`);
103
+ if (this.notify(device?._id, Date.now(), eventInterface, property, value, { changed }) && device) {
104
+ this.scrypted.getDeviceLogger(device).log('i', `${property}: ${value}`);
56
105
  }
57
106
  }
58
107
 
@@ -63,15 +112,18 @@ export class ScryptedStateManager extends EventRegistry {
63
112
  }
64
113
 
65
114
  updateDescriptor(device: PluginDevice) {
66
- this.notify(device._id, undefined, ScryptedInterface.ScryptedDevice, undefined, device.state, true);
115
+ this.notify(device._id, undefined, ScryptedInterface.ScryptedDevice, undefined, device.state, { changed: true });
67
116
  }
68
117
 
69
118
  removeDevice(id: string) {
70
- this.notify(undefined, undefined, ScryptedInterface.ScryptedDevice, ScryptedInterfaceProperty.id, id, true);
119
+ this.notify(undefined, undefined, ScryptedInterface.ScryptedDevice, ScryptedInterfaceProperty.id, id, { changed: true });
71
120
  }
72
121
 
73
- notifyInterfaceEvent(device: PluginDevice, eventInterface: ScryptedInterface | string, value: any) {
74
- this.notify(device?._id, Date.now(), eventInterface, undefined, value, true);
122
+ notifyInterfaceEvent(device: PluginDevice, eventInterface: ScryptedInterface | string, value: any, mixinId?: string) {
123
+ this.notify(device?._id, Date.now(), eventInterface, undefined, value, {
124
+ changed: true,
125
+ mixinId,
126
+ });
75
127
  }
76
128
 
77
129
  setState(id: string, property: string, value: any) {
@@ -214,12 +266,8 @@ export function setState(pluginDevice: PluginDevice, property: string, value: an
214
266
  if (!pluginDevice.state[property])
215
267
  pluginDevice.state[property] = {};
216
268
  const state = pluginDevice.state[property];
217
- const now = Date.now();
218
269
  const changed = !isSameValue(value, state.value);
219
- if (changed)
220
- state.stateTime = now;
221
270
  state.value = value;
222
- state.lastEventTime = now;
223
271
  return changed;
224
272
  }
225
273