@scrypted/server 0.0.108 → 0.0.112

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.

package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrypted/server",
3
- "version": "0.0.108",
3
+ "version": "0.0.112",
4
4
  "description": "",
5
5
  "dependencies": {
6
6
  "@scrypted/sdk": "^0.0.131",
@@ -21,7 +21,7 @@ from asyncio.futures import Future
21
21
  from asyncio.streams import StreamReader, StreamWriter
22
22
  import os
23
23
  from os import sys
24
- from sys import stderr, stdout
24
+ import platform
25
25
 
26
26
  class SystemDeviceState(TypedDict):
27
27
  lastEventTime: int
@@ -164,8 +164,8 @@ class PluginRemote:
164
164
 
165
165
  zip = zipfile.ZipFile(zipPath)
166
166
 
167
- python_prefix = os.path.join(os.environ.get(
168
- 'SCRYPTED_PLUGIN_VOLUME'), 'python')
167
+ plugin_volume = os.environ.get('SCRYPTED_PLUGIN_VOLUME')
168
+ python_prefix = os.path.join(plugin_volume, 'python-%s-%s' % (platform.system(), platform.machine()))
169
169
  if not os.path.exists(python_prefix):
170
170
  os.makedirs(python_prefix)
171
171
 
@@ -179,7 +179,7 @@ class PluginRemote:
179
179
 
180
180
  requirementstxt = os.path.join(python_prefix, 'requirements.txt')
181
181
  installed_requirementstxt = os.path.join(
182
- python_prefix, 'installed-requirements.txt')
182
+ python_prefix, 'requirements.installed.txt')
183
183
 
184
184
  need_pip = True
185
185
  try:
package/python/rpc.py CHANGED
@@ -1,8 +1,10 @@
1
1
  from asyncio.futures import Future
2
- from typing import Callable, Mapping, List
2
+ from typing import Any, Callable, Mapping, List
3
3
  import traceback
4
4
  import inspect
5
+ from typing_extensions import TypedDict
5
6
  import weakref
7
+ import sys
6
8
 
7
9
  jsonSerializable = set()
8
10
  jsonSerializable.add(float)
@@ -45,21 +47,35 @@ class RpcProxyMethod:
45
47
  return self.__proxy.__apply__(self.__proxy_method_name, args)
46
48
 
47
49
 
50
+ class LocalProxiedEntry(TypedDict):
51
+ id: str
52
+ finalizerId: str
53
+
54
+
48
55
  class RpcProxy(object):
49
- def __init__(self, peer, proxyId: str, proxyConstructorName: str, proxyProps: any, proxyOneWayMethods: List[str]):
50
- self.__dict__['__proxy_id'] = proxyId
56
+ def __init__(self, peer, entry: LocalProxiedEntry, proxyConstructorName: str, proxyProps: any, proxyOneWayMethods: List[str]):
57
+ self.__dict__['__proxy_id'] = entry['id']
58
+ self.__dict__['__proxy_entry'] = entry
51
59
  self.__dict__['__proxy_constructor'] = proxyConstructorName
52
60
  self.__dict__['__proxy_peer'] = peer
53
61
  self.__dict__['__proxy_props'] = proxyProps
54
62
  self.__dict__['__proxy_oneway_methods'] = proxyOneWayMethods
55
63
 
56
64
  def __getattr__(self, name):
65
+ if name == '__proxy_finalizer_id':
66
+ return self.dict['__proxy_entry']['finalizerId']
57
67
  if name in self.__dict__:
58
68
  return self.__dict__[name]
59
69
  if self.__dict__['__proxy_props'] and name in self.__dict__['__proxy_props']:
60
70
  return self.__dict__['__proxy_props'][name]
61
71
  return RpcProxyMethod(self, name)
62
72
 
73
+ def __setattr__(self, name: str, value: Any) -> None:
74
+ if name == '__proxy_finalizer_id':
75
+ self.dict['__proxy_entry']['finalizerId'] = value
76
+
77
+ return super().__setattr__(name, value)
78
+
63
79
  def __call__(self, *args, **kwargs):
64
80
  print('call')
65
81
  pass
@@ -69,20 +85,18 @@ class RpcProxy(object):
69
85
 
70
86
 
71
87
  class RpcPeer:
72
- # todo: these are all class statics lol, fix this.
73
- idCounter = 1
74
- peerName = 'Unnamed Peer'
75
- params: Mapping[str, any] = {}
76
- localProxied: Mapping[any, str] = {}
77
- localProxyMap: Mapping[str, any] = {}
78
- constructorSerializerMap = {}
79
- proxyCounter = 1
80
- pendingResults: Mapping[str, Future] = {}
81
- remoteWeakProxies: Mapping[str, any] = {}
82
- nameDeserializerMap: Mapping[str, RpcSerializer] = {}
83
-
84
88
  def __init__(self, send: Callable[[object, Callable[[Exception], None]], None]) -> None:
85
89
  self.send = send
90
+ self.idCounter = 1
91
+ self.peerName = 'Unnamed Peer'
92
+ self.params: Mapping[str, any] = {}
93
+ self.localProxied: Mapping[any, LocalProxiedEntry] = {}
94
+ self.localProxyMap: Mapping[str, any] = {}
95
+ self.constructorSerializerMap = {}
96
+ self.proxyCounter = 1
97
+ self.pendingResults: Mapping[str, Future] = {}
98
+ self.remoteWeakProxies: Mapping[str, any] = {}
99
+ self.nameDeserializerMap: Mapping[str, RpcSerializer] = {}
86
100
 
87
101
  def __apply__(self, proxyId: str, oneWayMethods: List[str], method: str, args: list):
88
102
  serializedArgs = []
@@ -120,12 +134,17 @@ class RpcPeer:
120
134
  def serialize(self, value, requireProxy):
121
135
  if (not value or (not requireProxy and type(value) in jsonSerializable)):
122
136
  return value
137
+
123
138
  __remote_constructor_name = 'Function' if callable(value) else value.__proxy_constructor if hasattr(
124
139
  value, '__proxy_constructor') else type(value).__name__
125
- proxyId = self.localProxied.get(value, None)
126
- if proxyId:
140
+
141
+ proxiedEntry = self.localProxied.get(value, None)
142
+ if proxiedEntry:
143
+ proxiedEntry['finalizerId'] = str(self.proxyCounter)
144
+ self.proxyCounter = self.proxyCounter + 1
127
145
  ret = {
128
- '__remote_proxy_id': proxyId,
146
+ '__remote_proxy_id': proxiedEntry['id'],
147
+ '__remote_proxy_finalizer_id': proxiedEntry['finalizerId'],
129
148
  '__remote_constructor_name': __remote_constructor_name,
130
149
  '__remote_proxy_props': getattr(value, '__proxy_props', None),
131
150
  '__remote_proxy_oneway_methods': getattr(value, '__proxy_oneway_methods', None),
@@ -140,13 +159,15 @@ class RpcPeer:
140
159
  }
141
160
  return ret
142
161
 
143
- serializerMapName = self.constructorSerializerMap.get(type(value), None)
162
+ serializerMapName = self.constructorSerializerMap.get(
163
+ type(value), None)
144
164
  if serializerMapName:
145
165
  __remote_constructor_name = serializerMapName
146
166
  serializer = self.nameDeserializerMap.get(serializerMapName, None)
147
167
  serialized = serializer.serialize(value)
148
168
  ret = {
149
169
  '__remote_proxy_id': None,
170
+ '__remote_proxy_finalizer_id': None,
150
171
  '__remote_constructor_name': __remote_constructor_name,
151
172
  '__remote_proxy_props': getattr(value, '__proxy_props', None),
152
173
  '__remote_proxy_oneway_methods': getattr(value, '__proxy_oneway_methods', None),
@@ -156,11 +177,16 @@ class RpcPeer:
156
177
 
157
178
  proxyId = str(self.proxyCounter)
158
179
  self.proxyCounter = self.proxyCounter + 1
159
- self.localProxied[value] = proxyId
180
+ proxiedEntry = {
181
+ 'id': proxyId,
182
+ 'finalizerId': proxyId,
183
+ }
184
+ self.localProxied[value] = proxiedEntry
160
185
  self.localProxyMap[proxyId] = value
161
186
 
162
187
  ret = {
163
188
  '__remote_proxy_id': proxyId,
189
+ '__remote_proxy_finalizer_id': proxyId,
164
190
  '__remote_constructor_name': __remote_constructor_name,
165
191
  '__remote_proxy_props': getattr(value, '__proxy_props', None),
166
192
  '__remote_proxy_oneway_methods': getattr(value, '__proxy_oneway_methods', None),
@@ -168,20 +194,26 @@ class RpcPeer:
168
194
 
169
195
  return ret
170
196
 
171
- def finalize(self, id: str):
197
+ def finalize(self, localProxiedEntry: LocalProxiedEntry):
198
+ id = localProxiedEntry['id']
172
199
  self.remoteWeakProxies.pop(id, None)
173
200
  rpcFinalize = {
174
201
  '__local_proxy_id': id,
202
+ '__local_proxy_finalizer_id': localProxiedEntry['finalizerId'],
175
203
  'type': 'finalize',
176
204
  }
177
205
  self.send(rpcFinalize)
178
206
 
179
207
  def newProxy(self, proxyId: str, proxyConstructorName: str, proxyProps: any, proxyOneWayMethods: List[str]):
180
- proxy = RpcProxy(self, proxyId, proxyConstructorName,
208
+ localProxiedEntry: LocalProxiedEntry = {
209
+ 'id': proxyId,
210
+ 'finalizerId': None,
211
+ }
212
+ proxy = RpcProxy(self, localProxiedEntry, proxyConstructorName,
181
213
  proxyProps, proxyOneWayMethods)
182
214
  wr = weakref.ref(proxy)
183
215
  self.remoteWeakProxies[proxyId] = wr
184
- weakref.finalize(proxy, lambda: self.finalize(proxyId))
216
+ weakref.finalize(proxy, lambda: self.finalize(localProxiedEntry))
185
217
  return proxy
186
218
 
187
219
  def deserialize(self, value):
@@ -192,6 +224,8 @@ class RpcPeer:
192
224
  return value
193
225
 
194
226
  __remote_proxy_id = value.get('__remote_proxy_id', None)
227
+ __remote_proxy_finalizer_id = value.get(
228
+ '__remote_proxy_finalizer_id', None)
195
229
  __local_proxy_id = value.get('__local_proxy_id', None)
196
230
  __remote_constructor_name = value.get(
197
231
  '__remote_constructor_name', None)
@@ -206,6 +240,7 @@ class RpcPeer:
206
240
  if not proxy:
207
241
  proxy = self.newProxy(__remote_proxy_id, __remote_constructor_name,
208
242
  __remote_proxy_props, __remote_proxy_oneway_methods)
243
+ proxy.__proxy_finalizer_id = __remote_proxy_finalizer_id
209
244
  return proxy
210
245
 
211
246
  if __local_proxy_id:
@@ -297,9 +332,16 @@ class RpcPeer:
297
332
  future.set_result(self.deserialize(
298
333
  message.get('result', None)))
299
334
  elif messageType == 'finalize':
300
- local = self.localProxyMap.pop(
301
- message['__local_proxy_id'], None)
302
- self.localProxied.pop(local, None)
335
+ finalizerId = message.get('__local_proxy_finalizer_id', None)
336
+ proxyId = message['__local_proxy_id']
337
+ local = self.localProxyMap.get(proxyId, None)
338
+ if local:
339
+ localProxiedEntry = self.localProxied.get(local)
340
+ if localProxiedEntry and finalizerId and localProxiedEntry['finalizerId'] != finalizerId:
341
+ print('mismatch finalizer id', file=sys.stderr)
342
+ return
343
+ self.localProxied.pop(local, None)
344
+ local = self.localProxyMap.pop(proxyId, None)
303
345
  else:
304
346
  raise RpcResultException(
305
347
  None, 'unknown rpc message type %s' % type)
@@ -12,12 +12,12 @@ export class EventListenerRegisterImpl implements EventListenerRegister {
12
12
  const allowedEventInterfaces = new Set<string>([ScryptedInterface.ScryptedDevice, 'Logger', 'Storage'])
13
13
 
14
14
  export class EventRegistry {
15
- systemListeners = new Set<(id: string, eventDetails: EventDetails, eventData: object) => void>();
16
- listeners: { [token: string]: Set<(eventDetails: EventDetails, eventData: object) => void> } = {};
15
+ systemListeners = new Set<(id: string, eventDetails: EventDetails, eventData: any) => void>();
16
+ listeners: { [token: string]: Set<(eventDetails: EventDetails, eventData: any) => void> } = {};
17
17
 
18
- listen(EventListener: (id: string, eventDetails: EventDetails, eventData: object) => void): EventListenerRegister {
18
+ listen(callback: (id: string, eventDetails: EventDetails, eventData: any) => void): EventListenerRegister {
19
19
  const events = this.systemListeners;
20
- let cb = EventListener;
20
+ let cb = callback;
21
21
  events.add(cb);
22
22
  return new EventListenerRegisterImpl(() => {
23
23
  events.delete(cb);
@@ -25,7 +25,7 @@ export class EventRegistry {
25
25
  });
26
26
  }
27
27
 
28
- listenDevice(id: string, options: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: object) => void): EventListenerRegister {
28
+ listenDevice(id: string, options: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: any) => void): EventListenerRegister {
29
29
  let { event } = (options || {}) as EventListenerOptions;
30
30
  if (!event && typeof options === 'string')
31
31
  event = options as string;
@@ -20,7 +20,7 @@ export interface PluginAPI {
20
20
  setDeviceProperty(id: string, property: ScryptedInterfaceProperty, value: any): Promise<void>;
21
21
  removeDevice(id: string): Promise<void>;
22
22
  listen(EventListener: (id: string, eventDetails: EventDetails, eventData: any) => void): Promise<EventListenerRegister>;
23
- listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: object) => void): Promise<EventListenerRegister>;
23
+ listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: any) => void): Promise<EventListenerRegister>;
24
24
 
25
25
  ioClose(id: string): Promise<void>;
26
26
  ioSend(id: string, message: string): Promise<void>;
@@ -101,10 +101,10 @@ export class PluginAPIProxy extends PluginAPIManagedListeners implements PluginA
101
101
  removeDevice(id: string): Promise<void> {
102
102
  return this.api.removeDevice(id);
103
103
  }
104
- async listen(EventListener: (id: string, eventDetails: EventDetails, eventData: any) => void): Promise<EventListenerRegister> {
105
- return this.manageListener(await this.api.listen(EventListener));
104
+ async listen(callback: (id: string, eventDetails: EventDetails, eventData: any) => void): Promise<EventListenerRegister> {
105
+ return this.manageListener(await this.api.listen(callback));
106
106
  }
107
- async listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: object) => void): Promise<EventListenerRegister> {
107
+ async listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: any) => void): Promise<EventListenerRegister> {
108
108
  return this.manageListener(await this.api.listenDevice(id, event, callback));
109
109
  }
110
110
  ioClose(id: string): Promise<void> {
@@ -54,6 +54,7 @@ export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevi
54
54
  async isMixin(id: string, mixinDevice: any) {
55
55
  if (this.releasing.has(mixinDevice))
56
56
  return true;
57
+ await this.scrypted.devices[id].handler.ensureProxy();
57
58
  for (const mixin of this.scrypted.devices[id].handler.mixinTable) {
58
59
  const { proxy } = await mixin.entry;
59
60
  if (proxy === mixinDevice) {
@@ -298,7 +299,7 @@ export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevi
298
299
  return new Proxy(() => prop, this);
299
300
  }
300
301
 
301
- listen(event: string | EventListenerOptions, callback: (eventSource: ScryptedDevice, eventDetails: EventDetails, eventData: object) => void): EventListenerRegister {
302
+ listen(event: string | EventListenerOptions, callback: (eventSource: ScryptedDevice, eventDetails: EventDetails, eventData: any) => void): EventListenerRegister {
302
303
  return this.scrypted.stateManager.listenDevice(this.id, event, (eventDetails, eventData) => callback(this.scrypted.getDevice(this.id), eventDetails, eventData));
303
304
  }
304
305
  async setName(name: string): Promise<void> {
@@ -47,22 +47,23 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
47
47
  const device = this.scrypted.findPluginDeviceById(id);
48
48
 
49
49
  if (!nativeIdOrMixinDevice || typeof nativeIdOrMixinDevice === 'string') {
50
+ const nativeId: string = nativeIdOrMixinDevice;
50
51
  // todo: deprecate this code path
51
- const mixinProvider = this.scrypted.findPluginDevice(this.pluginId, nativeIdOrMixinDevice);
52
+ const mixinProvider = this.scrypted.findPluginDevice(this.pluginId, nativeId);
52
53
  const mixins: string[] = getState(device, ScryptedInterfaceProperty.mixins) || [];
53
54
  if (!mixins.includes(mixinProvider._id))
54
55
  throw new Error(`${mixinProvider._id} is not a mixin provider for ${id}`);
55
56
 
56
- this.scrypted.findPluginDevice(this.pluginId, nativeIdOrMixinDevice);
57
+ this.scrypted.findPluginDevice(this.pluginId, nativeId);
57
58
  const tableEntry = this.scrypted.devices[device._id].handler.mixinTable.find(entry => entry.mixinProviderId === mixinProvider._id);
58
59
  const { interfaces } = await tableEntry.entry;
59
60
  if (!interfaces.has(eventInterface))
60
61
  throw new Error(`${mixinProvider._id} does not mixin ${eventInterface} for ${id}`);
61
62
  }
62
63
  else {
63
- if (!await this.scrypted.devices[device._id]?.handler?.isMixin(id, nativeIdOrMixinDevice)) {
64
- const mixinProvider = this.scrypted.findPluginDevice(this.pluginId, nativeIdOrMixinDevice);
65
- throw new Error(`${mixinProvider?._id} does not mixin ${eventInterface} for ${id}`);
64
+ const mixin: object = nativeIdOrMixinDevice;
65
+ if (!await this.scrypted.devices[device._id]?.handler?.isMixin(id, mixin)) {
66
+ throw new Error(`${mixin} does not mixin ${eventInterface} for ${id}`);
66
67
  }
67
68
  }
68
69
  this.scrypted.stateManager.notifyInterfaceEvent(device, eventInterface, eventData);
@@ -121,7 +122,8 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
121
122
  }
122
123
 
123
124
  async onDevicesChanged(deviceManifest: DeviceManifest) {
124
- const existing = this.scrypted.findPluginDevices(this.pluginId).filter(p => p.nativeId == deviceManifest.providerNativeId);
125
+ const provider = this.scrypted.findPluginDevice(this.pluginId, deviceManifest.providerNativeId);
126
+ const existing = this.scrypted.findPluginDevices(this.pluginId).filter(p => p.state[ScryptedInterfaceProperty.providerId].value === provider._id);
125
127
  const newIds = deviceManifest.devices.map(device => device.nativeId);
126
128
  const toRemove = existing.filter(e => e.nativeId && !newIds.includes(e.nativeId));
127
129
 
@@ -150,10 +152,10 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
150
152
  async getDeviceById<T>(id: string): Promise<T & ScryptedDevice> {
151
153
  return this.scrypted.getDevice(id);
152
154
  }
153
- async listen(listener: (id: string, eventDetails: EventDetails, eventData: any) => void): Promise<EventListenerRegister> {
154
- return this.manageListener(this.scrypted.stateManager.listen(listener));
155
+ async listen(callback: (id: string, eventDetails: EventDetails, eventData: any) => void): Promise<EventListenerRegister> {
156
+ return this.manageListener(this.scrypted.stateManager.listen(callback));
155
157
  }
156
- async listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: object) => void): Promise<EventListenerRegister> {
158
+ async listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: any) => void): Promise<EventListenerRegister> {
157
159
  const device = this.scrypted.findPluginDeviceById(id);
158
160
  if (device) {
159
161
  const self = this.scrypted.findPluginDevice(this.pluginId);
@@ -3,14 +3,18 @@ import fs from 'fs';
3
3
  import child_process from 'child_process';
4
4
  import path from 'path';
5
5
  import { once } from 'events';
6
+ import process from 'process';
7
+ import mkdirp from "mkdirp";
6
8
 
7
9
  export async function installOptionalDependencies(console: Console, packageJson: any) {
8
10
  const pluginVolume = ensurePluginVolume(packageJson.name);
9
- const optPj = path.join(pluginVolume, 'package.json');
11
+ const nodePrefix = path.join(pluginVolume, `${process.platform}-${process.arch}`);
12
+ const packageJsonPath = path.join(nodePrefix, 'package.json');
13
+ const currentInstalledPackageJsonPath = path.join(nodePrefix, 'package.installed.json');
10
14
 
11
15
  let currentPackageJson: any;
12
16
  try {
13
- currentPackageJson = JSON.parse(fs.readFileSync(optPj).toString());
17
+ currentPackageJson = JSON.parse(fs.readFileSync(currentInstalledPackageJsonPath).toString());
14
18
  }
15
19
  catch (e) {
16
20
  }
@@ -34,21 +38,18 @@ export async function installOptionalDependencies(console: Console, packageJson:
34
38
  delete reduced.optionalDependencies;
35
39
  delete reduced.devDependencies;
36
40
 
37
- try {
38
- fs.writeFileSync(optPj, JSON.stringify(reduced));
39
-
40
- const cp = child_process.spawn('npm', ['--prefix', pluginVolume, 'install'], {
41
- cwd: pluginVolume,
42
- stdio: 'inherit',
43
- });
44
-
45
- await once(cp, 'exit');
46
- if (cp.exitCode !== 0)
47
- throw new Error('npm installation failed with exit code ' + cp.exitCode);
48
- }
49
- catch (e) {
50
- fs.rmSync(optPj);
51
- throw e;
52
- }
41
+ mkdirp.sync(nodePrefix);
42
+ fs.writeFileSync(packageJsonPath, JSON.stringify(reduced));
43
+
44
+ const cp = child_process.spawn('npm', ['--prefix', nodePrefix, 'install'], {
45
+ cwd: nodePrefix,
46
+ stdio: 'inherit',
47
+ });
48
+
49
+ await once(cp, 'exit');
50
+ if (cp.exitCode !== 0)
51
+ throw new Error('npm installation failed with exit code ' + cp.exitCode);
52
+
53
+ fs.writeFileSync(currentInstalledPackageJsonPath, JSON.stringify(reduced));
53
54
  console.log('native dependencies installed.');
54
55
  }
@@ -1,6 +1,6 @@
1
1
  import { EventListenerOptions, EventDetails, EventListenerRegister, ScryptedDevice, ScryptedInterface, ScryptedInterfaceDescriptors, SystemDeviceState, SystemManager, ScryptedInterfaceProperty, ScryptedDeviceType, Logger } from "@scrypted/sdk/types";
2
2
  import { PluginAPI } from "./plugin-api";
3
- import { handleFunctionInvocations } from '../rpc';
3
+ import { handleFunctionInvocations, PROPERTY_PROXY_ONEWAY_METHODS } from '../rpc';
4
4
  import { EventRegistry } from "../event-registry";
5
5
  import { allInterfaceProperties, isValidInterfaceMethod } from "./descriptor";
6
6
 
@@ -56,7 +56,7 @@ class DeviceProxyHandler implements ProxyHandler<any>, ScryptedDevice {
56
56
  return (this.device as any)[method](...argArray);
57
57
  }
58
58
 
59
- listen(event: string | EventListenerOptions, callback: (eventSource: ScryptedDevice, eventDetails: EventDetails, eventData: object) => void): EventListenerRegister {
59
+ listen(event: string | EventListenerOptions, callback: (eventSource: ScryptedDevice, eventDetails: EventDetails, eventData: any) => void): EventListenerRegister {
60
60
  return this.systemManager.listenDevice(this.id, event, callback);
61
61
  }
62
62
 
@@ -92,6 +92,15 @@ class EventListenerRegisterImpl implements EventListenerRegister {
92
92
  }
93
93
  }
94
94
 
95
+ function makeOneWayCallback<T>(input: T): T {
96
+ const f: any = input;
97
+ const oneways: string[] = f[PROPERTY_PROXY_ONEWAY_METHODS] || [];
98
+ if (!oneways.includes(null))
99
+ oneways.push(null);
100
+ f[PROPERTY_PROXY_ONEWAY_METHODS] = oneways;
101
+ return input;
102
+ }
103
+
95
104
  export class SystemManagerImpl implements SystemManager {
96
105
  api: PluginAPI;
97
106
  state: {[id: string]: {[property: string]: SystemDeviceState}};
@@ -122,10 +131,10 @@ export class SystemManagerImpl implements SystemManager {
122
131
  return this.getDeviceById(id);
123
132
  }
124
133
  }
125
- listen(EventListener: (eventSource: ScryptedDevice, eventDetails: EventDetails, eventData: object) => void): EventListenerRegister {
126
- return this.events.listen((id, eventDetails, eventData) => EventListener(this.getDeviceById(id), eventDetails, eventData));
134
+ listen(callback: (eventSource: ScryptedDevice, eventDetails: EventDetails, eventData: any) => void): EventListenerRegister {
135
+ return this.events.listen(makeOneWayCallback((id, eventDetails, eventData) => callback(this.getDeviceById(id), eventDetails, eventData)));
127
136
  }
128
- listenDevice(id: string, options: string | EventListenerOptions, callback: (eventSource: ScryptedDevice, eventDetails: EventDetails, eventData: object) => void): EventListenerRegister {
137
+ listenDevice(id: string, options: string | EventListenerOptions, callback: (eventSource: ScryptedDevice, eventDetails: EventDetails, eventData: any) => void): EventListenerRegister {
129
138
  let { event, watch } = (options || {}) as EventListenerOptions;
130
139
  if (!event && typeof options === 'string')
131
140
  event = options as string;
@@ -136,7 +145,7 @@ export class SystemManagerImpl implements SystemManager {
136
145
  if (watch)
137
146
  return this.events.listenDevice(id, event, (eventDetails, eventData) => callback(this.getDeviceById(id), eventDetails, eventData));
138
147
 
139
- return new EventListenerRegisterImpl(this.api.listenDevice(id, options, (eventDetails, eventData) => callback(this.getDeviceById(id), eventDetails, eventData)))
148
+ return new EventListenerRegisterImpl(this.api.listenDevice(id, options, makeOneWayCallback((eventDetails, eventData) => callback(this.getDeviceById(id), eventDetails, eventData))));
140
149
  }
141
150
 
142
151
  async removeDevice(id: string) {