aes70 1.3.6 → 1.3.8

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.
@@ -33,6 +33,16 @@ export declare class OcaPowerSupply extends OcaAgent {
33
33
  */
34
34
  OnChargingChanged: PropertyEvent<boolean>;
35
35
 
36
+ /**
37
+ * This event is emitted whenever LoadFractionAvailable changes.
38
+ */
39
+ OnLoadFractionAvailableChanged: PropertyEvent<number>;
40
+
41
+ /**
42
+ * This event is emitted whenever StorageFractionAvailable changes.
43
+ */
44
+ OnStorageFractionAvailableChanged: PropertyEvent<number>;
45
+
36
46
  constructor(objectNumber: number, device: RemoteDevice);
37
47
 
38
48
  /**
@@ -33,8 +33,8 @@ export const OcaPowerSupply = make_control_class(
33
33
  ['ModelInfo', [OcaString], 3, 2, false, false, null],
34
34
  ['State', [OcaPowerSupplyState], 3, 3, false, false, null],
35
35
  ['Charging', [OcaBoolean], 3, 4, false, false, null],
36
- ['LoadFractionAvailable', [OcaFloat32], 3, 5, true, false, null],
37
- ['StorageFractionAvailable', [OcaFloat32], 3, 6, true, false, null],
36
+ ['LoadFractionAvailable', [OcaFloat32], 3, 5, false, false, null],
37
+ ['StorageFractionAvailable', [OcaFloat32], 3, 6, false, false, null],
38
38
  ['Location', [OcaPowerSupplyLocation], 3, 7, true, false, null],
39
39
  ],
40
40
  []
@@ -126,3 +126,21 @@ export const OcaPowerSupply = make_control_class(
126
126
  *
127
127
  * @member {PropertyEvent<boolean>} OcaPowerSupply#OnChargingChanged
128
128
  */
129
+ /**
130
+ * This event is emitted when the property LoadFractionAvailable changes in the remote object.
131
+ * The property ``LoadFractionAvailable`` is described in the AES70 standard as follows.
132
+ * Fraction of power supply's load capacity that is currently not being
133
+ * used. Readonly. Normal value range 0...1. A negative value indicates
134
+ * this data is not available.
135
+ *
136
+ * @member {PropertyEvent<number>} OcaPowerSupply#OnLoadFractionAvailableChanged
137
+ */
138
+ /**
139
+ * This event is emitted when the property StorageFractionAvailable changes in the remote object.
140
+ * The property ``StorageFractionAvailable`` is described in the AES70 standard as follows.
141
+ * Fraction of power supply's energy storage that remains available. For
142
+ * battery supplies. Readonly. Normal value range 0...1. A negative value
143
+ * indicates this data is not available.
144
+ *
145
+ * @member {PropertyEvent<number>} OcaPowerSupply#OnStorageFractionAvailableChanged
146
+ */
@@ -10,6 +10,11 @@ import { OcaSensorReadingState } from '../../types/OcaSensorReadingState';
10
10
  * @class OcaSensor
11
11
  */
12
12
  export declare class OcaSensor extends OcaWorker {
13
+ /**
14
+ * This event is emitted whenever ReadingState changes.
15
+ */
16
+ OnReadingStateChanged: PropertyEvent<OcaSensorReadingState>;
17
+
13
18
  constructor(objectNumber: number, device: RemoteDevice);
14
19
 
15
20
  /**
@@ -14,7 +14,7 @@ export const OcaSensor = make_control_class(
14
14
  2,
15
15
  OcaWorker,
16
16
  [['GetReadingState', 3, 1, [], [OcaSensorReadingState]]],
17
- [['ReadingState', [OcaSensorReadingState], 3, 1, false, true, null]],
17
+ [['ReadingState', [OcaSensorReadingState], 3, 1, false, false, null]],
18
18
  []
19
19
  );
20
20
 
@@ -25,3 +25,11 @@ export const OcaSensor = make_control_class(
25
25
  * @returns {Promise<OcaSensorReadingState>}
26
26
  * A promise which resolves to a single value of type :class:`OcaSensorReadingState`.
27
27
  */
28
+ /**
29
+ * This event is emitted when the property ReadingState changes in the remote object.
30
+ * The property ``ReadingState`` is described in the AES70 standard as follows.
31
+ * Enum that describes whether current reading value is valid and if not,
32
+ * why not. Readonly.
33
+ *
34
+ * @member {PropertyEvent<OcaSensorReadingState>} OcaSensor#OnReadingStateChanged
35
+ */
@@ -4,7 +4,6 @@
4
4
  * AES70-2018.
5
5
  */
6
6
 
7
- import { ObjectBase } from './object_base';
8
7
  import { OcaRoot } from './ControlClasses/OcaRoot';
9
8
  import { OcaWorker } from './ControlClasses/OcaWorker';
10
9
  import { OcaActuator } from './ControlClasses/OcaActuator';
@@ -0,0 +1,211 @@
1
+ /* eslint-env node */
2
+
3
+ import { encodeMessage } from '../OCP1/encode_message.js';
4
+ import { decodeMessage } from '../OCP1/decode_message.js';
5
+ import { KeepAlive } from '../OCP1/keepalive.js';
6
+ import { ClientConnection } from './client_connection.js';
7
+
8
+ function delay(n) {
9
+ return new Promise((resolve) => setTimeout(resolve, n));
10
+ }
11
+
12
+ async function waitForKeepalive(socket, options) {
13
+ const waiter = socket.receiveMessage().then((buffer) => {
14
+ const pdus = [];
15
+ let pos = 0;
16
+
17
+ pos = decodeMessage(new DataView(buffer), 0, pdus);
18
+
19
+ if (pdus.length !== 1) throw new Error('Expected keepalive response.');
20
+
21
+ return true;
22
+ });
23
+
24
+ const msg = encodeMessage(new KeepAlive(1000));
25
+ const t = 5 * (options.retry_interval || 250);
26
+
27
+ for (let i = 0; i < 3; i++) {
28
+ socket.send(msg);
29
+
30
+ if (await Promise.race([waiter, delay(t)])) return;
31
+ }
32
+
33
+ throw new Error('Failed to connect.');
34
+ }
35
+
36
+ /**
37
+ * :class:`ClientConnection` subclass which implements OCP.1 with UDP
38
+ * transport.
39
+ */
40
+ export class AbstractUDPConnection extends ClientConnection {
41
+ constructor(socket, options) {
42
+ // allow us to batch 128 bytes max
43
+ // Set this to a higher value, e.g. close to MTU
44
+ // if you are sure that the device can handle it.
45
+ if (!(options.batch >= 0)) options.batch = 128;
46
+ super(options);
47
+ this.socket = socket;
48
+ this.delay = options.delay >= 0 ? options.delay : 5;
49
+ this.retry_interval =
50
+ options.retry_interval >= 0 ? options.retry_interval : 250;
51
+ this.retry_count = options.retry_count >= 0 ? options.retry_count : 3;
52
+ this._write_out_id = -1;
53
+ this._write_out_callback = () => {
54
+ this._write_out_id = -1;
55
+ this._write_out();
56
+ };
57
+ this._retry_id =
58
+ this.retry_interval > 0
59
+ ? setInterval(() => this._retryCommands(), this.retry_interval)
60
+ : -1;
61
+ this.q = [];
62
+ socket.onmessage = (buffer) => {
63
+ try {
64
+ this.read(buffer);
65
+ } catch (err) {
66
+ console.warn('Failed to parse incoming AES70 packet: %o', err);
67
+ }
68
+ if (this.inbuf !== null) this.close();
69
+ };
70
+ socket.onerror = (err) => this.error(err);
71
+ this.set_keepalive_interval(1);
72
+ }
73
+
74
+ get is_reliable() {
75
+ return false;
76
+ }
77
+
78
+ /**
79
+ * Connect to the given endpoint.
80
+ *
81
+ * @param {String} options.host - hostname or ip
82
+ * @param {number} options.port - port number
83
+ * @param {number} [options.delay=10] - Delay in ms between individual packets.
84
+ * This can be a useful strategy when communicating with devices which
85
+ * cannot handle high packet rates.
86
+ * @param {number} [options.retry_interval=250] - Delay in ms after which a
87
+ * command should be automatically re-sent if no response has been
88
+ * received, yet.
89
+ * @param {number} [options.retry_count=3] - Number of times to retry sending
90
+ * commands. If no response has been received after all retries, the
91
+ * command will fail with an error.
92
+ * @param {number} [options.batch=128] - Maximum number of bytes to send
93
+ * in an individual UDP packet. Note that AES70 messages which are larger
94
+ * than this limit are sent anyway. This only limits how many seperate
95
+ * messages are batched into a single packet.
96
+ * @returns {Promise<UDPConnection>}
97
+ * The connection.
98
+ */
99
+ static async connect(udpApi, options) {
100
+ const socket = await udpApi.connect(options.host, options.port, options.type);
101
+
102
+ await waitForKeepalive(socket, options);
103
+
104
+ return new this(socket, options);
105
+ }
106
+
107
+ write(buf) {
108
+ this.q.push(buf);
109
+
110
+ if (this.tx_idle_time() >= this.delay) this._write_out();
111
+ else this._schedule_write_out();
112
+ }
113
+
114
+ flush() {
115
+ super.flush();
116
+ if (this.tx_idle_time() > this.delay) this._write_out();
117
+ }
118
+
119
+ cleanup() {
120
+ super.cleanup();
121
+ if (this.socket) {
122
+ this.socket.close();
123
+ this.socket = null;
124
+ }
125
+ if (this._write_out_id !== -1) {
126
+ clearTimeout(this._write_out_id);
127
+ this._write_out_id = -1;
128
+ }
129
+ if (this._retry_id !== -1) {
130
+ clearInterval(this._retry_id);
131
+ this._retry_id = -1;
132
+ }
133
+ }
134
+
135
+ _estimate_next_tx_time() {
136
+ return this._now() + (this.delay + 2) * this.q.length;
137
+ }
138
+
139
+ _write_out() {
140
+ if (!this.socket) return;
141
+ const q = this.q;
142
+
143
+ if (!q.length) return;
144
+
145
+ const buf = q.shift();
146
+
147
+ this.socket.send(buf);
148
+ super.write(buf);
149
+
150
+ if (q.length) this._schedule_write_out();
151
+ }
152
+
153
+ _schedule_write_out() {
154
+ const tx_idle_time = this.tx_idle_time();
155
+ const delay = this.delay;
156
+
157
+ if (tx_idle_time >= delay) {
158
+ this._write_out();
159
+ return;
160
+ }
161
+
162
+ // Already scheduled.
163
+ if (this._write_out_id !== -1) return;
164
+
165
+ this._write_out_id = setTimeout(
166
+ this._write_out_callback,
167
+ delay - tx_idle_time
168
+ );
169
+ }
170
+
171
+ _retryCommands() {
172
+ const now = this._now();
173
+ const retryTime = now - this.retry_interval;
174
+ // This is an estimate for how many commands we would manage to send
175
+ // off.
176
+ const max = 5 * (this.retry_interval / this.delay) - this.q.length;
177
+ const pendingCommands = this._pendingCommands;
178
+
179
+ const retries = [];
180
+ const failed = [];
181
+
182
+ for (const entry of pendingCommands) {
183
+ const [, pendingCommand] = entry;
184
+
185
+ // All later commands are newer than the cutoff.
186
+ if (pendingCommand.lastSent > retryTime) break;
187
+ if (pendingCommand.retries >= this.retry_count) {
188
+ failed.push(entry);
189
+ } else if (retries.length < max) {
190
+ retries.push(entry);
191
+ }
192
+ }
193
+
194
+ if (failed.length) {
195
+ const timeoutError = new Error('Timeout.');
196
+
197
+ failed.forEach(([handle, pendingCommand]) => {
198
+ pendingCommands.delete(handle);
199
+ pendingCommand.reject(timeoutError);
200
+ });
201
+ }
202
+
203
+ retries.forEach(([handle, pendingCommand]) => {
204
+ pendingCommands.delete(handle);
205
+ pendingCommands.set(handle, pendingCommand);
206
+ this.send(pendingCommand.command);
207
+ pendingCommand.lastSent = now;
208
+ pendingCommand.retries++;
209
+ });
210
+ }
211
+ }
@@ -1,6 +1,6 @@
1
1
  import { IConnectionOptions, Connection } from '../connection';
2
2
 
3
- export interface IClientConnectionOptions extends IConnectionOptions {}
3
+ export type IClientConnectionOptions = IConnectionOptions
4
4
 
5
5
  /**
6
6
  * A generic base class for all client connections.
@@ -1,7 +1,6 @@
1
1
  import { RemoteError } from './remote_error.js';
2
2
  import { Connection } from '../connection.js';
3
3
  import { Response } from '../OCP1/response.js';
4
- import { encodeMessage } from '../OCP1/encode_message.js';
5
4
  import { KeepAlive } from '../OCP1/keepalive.js';
6
5
  import { Notification } from '../OCP1/notification.js';
7
6
  import { Arguments } from './arguments.js';
@@ -119,8 +118,12 @@ export class ClientConnection extends Connection {
119
118
  return handle;
120
119
  }
121
120
 
122
- send_command(command, returnTypes) {
123
- return new Promise((resolve, reject) => {
121
+ _estimate_next_tx_time() {
122
+ return this._now();
123
+ }
124
+
125
+ send_command(command, returnTypes, callback) {
126
+ const executor = (resolve, reject) => {
124
127
  const handle = this._getNextCommandHandle();
125
128
 
126
129
  command.handle = handle;
@@ -134,9 +137,18 @@ export class ClientConnection extends Connection {
134
137
 
135
138
  this._pendingCommands.set(handle, pendingCommand);
136
139
 
137
- pendingCommand.lastSent = this._now();
140
+ pendingCommand.lastSent = this._estimate_next_tx_time();
138
141
  this.send(command);
139
- });
142
+ };
143
+
144
+ if (callback) {
145
+ executor(
146
+ (result) => callback(true, result),
147
+ (error) => callback(false, error)
148
+ );
149
+ } else {
150
+ return new Promise(executor);
151
+ }
140
152
  }
141
153
 
142
154
  _removePendingCommand(handle) {
@@ -58,15 +58,35 @@ function implement_method(cls, method) {
58
58
 
59
59
  const [name, level, index, argumentTypes, returnTypes] = method;
60
60
 
61
- cls.prototype[name] = function () {
61
+ cls.prototype[name] = function (...args) {
62
+ const argumentCount = argumentTypes.length;
63
+ let callback = null;
64
+
65
+ // If there are too few arguments, this might mean
66
+ //
67
+ // - that some of them use the default encoding (e.g. 0)
68
+ // - that the method signature has change in the AES70 version
69
+ // used
70
+ //
71
+ // this is why we do not error here, yet.
72
+ if (argumentCount < args.length) {
73
+ if (
74
+ argumentCount + 1 === args.length &&
75
+ typeof args[argumentCount] === 'function'
76
+ ) {
77
+ callback = args[argumentCount];
78
+ args.length = argumentCount;
79
+ }
80
+ }
81
+
62
82
  const cmd = new CommandRrq(
63
83
  this.ono,
64
84
  level,
65
85
  index,
66
- argumentTypes.length,
67
- new EncodedArguments(argumentTypes, Array.from(arguments))
86
+ argumentCount,
87
+ new EncodedArguments(argumentTypes, args)
68
88
  );
69
- return this.device.send_command(cmd, returnTypes);
89
+ return this.device.send_command(cmd, returnTypes, callback);
70
90
  };
71
91
  }
72
92
 
@@ -0,0 +1,141 @@
1
+ /* eslint-env node */
2
+
3
+ import { createSocket } from 'dgram';
4
+ import { isIPv4, isIPv6 } from 'net';
5
+ import { lookup } from 'dns';
6
+
7
+ function lookup_address(host) {
8
+ if (isIPv4(host))
9
+ return Promise.resolve(host);
10
+
11
+ return new Promise((resolve, reject) => {
12
+ lookup(host, { family: 4 }, (err, address) => {
13
+ if (err) reject(err);
14
+ else resolve(address);
15
+ });
16
+ });
17
+ }
18
+
19
+ export class NodeUDP {
20
+ get onmessage() {
21
+ return this._onmessage;
22
+ }
23
+
24
+ set onmessage(cb) {
25
+ if (this._onmessage && cb)
26
+ throw new Error('Cannot install more than one message callback.');
27
+
28
+ this._onmessage = cb;
29
+ this._notifyMessage();
30
+ }
31
+
32
+ get onerror() {
33
+ return this._onerror;
34
+ }
35
+
36
+ set onerror(cb) {
37
+ if (this._onerror && cb)
38
+ throw new Error('Cannot install more than one error callback.');
39
+
40
+ this._onerror = cb;
41
+ this._notifyError();
42
+ }
43
+
44
+ _notifyMessage() {
45
+ const onmessage = this.onmessage;
46
+
47
+ if (!onmessage) return;
48
+
49
+ const inbuf = this._inbuf;
50
+
51
+ if (!inbuf.length) return;
52
+
53
+ this._inbuf = [];
54
+
55
+ for (let i = 0; i < inbuf.length; i++) {
56
+ onmessage(inbuf[i].buffer);
57
+ }
58
+ }
59
+
60
+ _notifyError() {
61
+ const onerror = this.onerror;
62
+
63
+ if (!onerror) return;
64
+
65
+ const error = this._error;
66
+
67
+ if (!error) return;
68
+ this._error = null;
69
+
70
+ onerror(error);
71
+ }
72
+
73
+ send(buf) {
74
+ this.socket.send(Buffer.from(buf));
75
+ }
76
+
77
+ receiveMessage(timeout) {
78
+ return new Promise((resolve, reject) => {
79
+ if (this._error) {
80
+ reject(this._error);
81
+ } else if (this._inbuf.length) {
82
+ resolve(this._inbuf.shift());
83
+ } else {
84
+ this.onmessage = (msg) => {
85
+ this.onmessage = null;
86
+ this.onerror = null;
87
+ resolve(msg);
88
+ };
89
+ this.onerror = (err) => {
90
+ this.onmessage = null;
91
+ this.onerror = null;
92
+ reject(err);
93
+ };
94
+ }
95
+ });
96
+ }
97
+
98
+ close() {
99
+ this.socket.close();
100
+ this.socket = null;
101
+ }
102
+
103
+ constructor(socket) {
104
+ this._inbuf = [];
105
+ this._onmessage = null;
106
+ this._onerror = null;
107
+ this.socket = socket;
108
+ socket.on('message', (msg, rinfo) => {
109
+ this._inbuf.push(msg);
110
+ this._notifyMessage();
111
+ });
112
+ socket.on('error', (err) => {
113
+ this._error = err;
114
+ this._notifyError(err);
115
+ });
116
+ }
117
+
118
+ static connect(host, port, type) {
119
+ return lookup_address(host).then((ip) => {
120
+ return new Promise((resolve, reject) => {
121
+ const socket = createSocket(type || 'udp4');
122
+ const onerror = function (ev) {
123
+ reject(ev);
124
+ };
125
+ socket.on('error', onerror);
126
+ socket.bind(
127
+ {
128
+ exclusive: true,
129
+ },
130
+ () => {
131
+ socket.on('connect', () => {
132
+ resolve(new this(socket));
133
+ socket.removeListener('error', onerror);
134
+ });
135
+ socket.connect(port, host);
136
+ }
137
+ );
138
+ });
139
+ });
140
+ }
141
+ }
@@ -21,7 +21,6 @@ import * as RemoteControlClasses from './ControlClasses.js';
21
21
 
22
22
  import { OcaManagerDefaultObjectNumbers } from '../types/OcaManagerDefaultObjectNumbers.js';
23
23
  import { OcaNotificationDeliveryMode } from '../types/OcaNotificationDeliveryMode.js';
24
- import { RemoteError } from './remote_error.js';
25
24
 
26
25
  function eventToKey(event) {
27
26
  const ono = event.EmitterONo;
@@ -248,8 +247,8 @@ export class RemoteDevice extends Events {
248
247
  this.connection.close();
249
248
  }
250
249
 
251
- send_command(cmd, returnType) {
252
- return this.connection.send_command(cmd, returnType);
250
+ send_command(cmd, returnType, callback) {
251
+ return this.connection.send_command(cmd, returnType, callback);
253
252
  }
254
253
 
255
254
  _doSubscribe(event) {