iobroker.loxone 2.2.3 → 3.0.1

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.
Files changed (73) hide show
  1. package/LICENSE +183 -183
  2. package/README.md +23 -1
  3. package/admin/words.js +18 -18
  4. package/build/controls/AalEmergency.js +2 -2
  5. package/build/controls/AalEmergency.js.map +1 -1
  6. package/build/controls/AalSmartAlarm.js +2 -2
  7. package/build/controls/AalSmartAlarm.js.map +1 -1
  8. package/build/controls/Alarm.js +2 -2
  9. package/build/controls/Alarm.js.map +1 -1
  10. package/build/controls/AlarmClock.js +2 -2
  11. package/build/controls/AlarmClock.js.map +1 -1
  12. package/build/controls/AudioZone.js +2 -2
  13. package/build/controls/AudioZone.js.map +1 -1
  14. package/build/controls/CentralAlarm.js +3 -3
  15. package/build/controls/CentralAlarm.js.map +1 -1
  16. package/build/controls/CentralAudioZone.js +1 -1
  17. package/build/controls/CentralAudioZone.js.map +1 -1
  18. package/build/controls/CentralGate.js +3 -3
  19. package/build/controls/CentralGate.js.map +1 -1
  20. package/build/controls/CentralJalousie.js +4 -4
  21. package/build/controls/CentralJalousie.js.map +1 -1
  22. package/build/controls/CentralLightController.js +1 -1
  23. package/build/controls/CentralLightController.js.map +1 -1
  24. package/build/controls/Daytimer.js +1 -1
  25. package/build/controls/Daytimer.js.map +1 -1
  26. package/build/controls/Dimmer.js +2 -2
  27. package/build/controls/Dimmer.js.map +1 -1
  28. package/build/controls/EIBDimmer.js +2 -2
  29. package/build/controls/EIBDimmer.js.map +1 -1
  30. package/build/controls/Gate.js +9 -14
  31. package/build/controls/Gate.js.map +1 -1
  32. package/build/controls/Hourcounter.js +2 -2
  33. package/build/controls/Hourcounter.js.map +1 -1
  34. package/build/controls/IRCV2Daytimer.js +9 -0
  35. package/build/controls/IRCV2Daytimer.js.map +1 -0
  36. package/build/controls/IRoomControllerV2.js +105 -6
  37. package/build/controls/IRoomControllerV2.js.map +1 -1
  38. package/build/controls/Intercom.js +1 -1
  39. package/build/controls/Intercom.js.map +1 -1
  40. package/build/controls/Jalousie.js +10 -13
  41. package/build/controls/Jalousie.js.map +1 -1
  42. package/build/controls/LightController.js +3 -3
  43. package/build/controls/LightController.js.map +1 -1
  44. package/build/controls/LightControllerV2.js +2 -2
  45. package/build/controls/LightControllerV2.js.map +1 -1
  46. package/build/controls/MailBox.js +2 -2
  47. package/build/controls/MailBox.js.map +1 -1
  48. package/build/controls/Meter.js +1 -1
  49. package/build/controls/Meter.js.map +1 -1
  50. package/build/controls/Pushbutton.js +3 -6
  51. package/build/controls/Pushbutton.js.map +1 -1
  52. package/build/controls/Radio.js +1 -4
  53. package/build/controls/Radio.js.map +1 -1
  54. package/build/controls/Remote.js +36 -0
  55. package/build/controls/Remote.js.map +1 -0
  56. package/build/controls/SmokeAlarm.js +2 -2
  57. package/build/controls/SmokeAlarm.js.map +1 -1
  58. package/build/controls/Switch.js +2 -5
  59. package/build/controls/Switch.js.map +1 -1
  60. package/build/controls/TimedSwitch.js +3 -3
  61. package/build/controls/TimedSwitch.js.map +1 -1
  62. package/build/controls/Unknown.js +1 -2
  63. package/build/controls/Unknown.js.map +1 -1
  64. package/build/loxone-handler-base.js +4 -4
  65. package/build/loxone-handler-base.js.map +1 -1
  66. package/build/main.js +410 -96
  67. package/build/main.js.map +1 -1
  68. package/build/weather-server-handler.js +4 -4
  69. package/build/weather-server-handler.js.map +1 -1
  70. package/io-package.json +298 -332
  71. package/package.json +40 -30
  72. package/build/lib/tools.js +0 -96
  73. package/build/lib/tools.js.map +0 -1
package/build/main.js CHANGED
@@ -5,13 +5,19 @@
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Loxone = void 0;
7
7
  const utils = require("@iobroker/adapter-core");
8
- const SentryNode = require("@sentry/node");
9
8
  const axios_1 = require("axios");
10
- const loxoneWsApi = require("node-lox-ws-api");
9
+ const LxCommunicator = require("lxcommunicator");
10
+ const uuid_1 = require("uuid");
11
11
  const Unknown_1 = require("./controls/Unknown");
12
12
  const weather_server_handler_1 = require("./weather-server-handler");
13
13
  const FormData = require("form-data");
14
14
  const Queue = require("queue-fifo");
15
+ const WebSocketConfig = LxCommunicator.WebSocketConfig;
16
+ // Log warnings if no ack event from Loxone in this time
17
+ // TODO: should this be configurable?
18
+ const ackTimeoutMs = 500;
19
+ // Period between connection attempts
20
+ const reconnectTimeoutMs = 5000;
15
21
  class Loxone extends utils.Adapter {
16
22
  constructor(options = {}) {
17
23
  super({
@@ -19,6 +25,7 @@ class Loxone extends utils.Adapter {
19
25
  ...options,
20
26
  name: 'loxone',
21
27
  });
28
+ this.uuid = '';
22
29
  this.existingObjects = {};
23
30
  this.currentStateValues = {};
24
31
  this.operatingModes = {};
@@ -31,14 +38,19 @@ class Loxone extends utils.Adapter {
31
38
  this.queueRunning = false;
32
39
  this.reportedMissingControls = new Set();
33
40
  this.reportedUnsupportedStateChanges = new Set();
41
+ this.connectionInProgress = false;
42
+ this.lxConnected = false;
34
43
  this.on('ready', this.onReady.bind(this));
35
44
  this.on('stateChange', this.onStateChange.bind(this));
36
45
  this.on('unload', this.onUnload.bind(this));
46
+ this.info = new Map();
37
47
  }
38
48
  /**
39
49
  * Is called when databases are connected and adapter received configuration.
40
50
  */
41
51
  async onReady() {
52
+ // Init info
53
+ await this.initInfoStates();
42
54
  // store all current (acknowledged) state values
43
55
  const allStates = await this.getStatesAsync('*');
44
56
  for (const id in allStates) {
@@ -49,69 +61,10 @@ class Loxone extends utils.Adapter {
49
61
  // store all existing objects for later use
50
62
  this.existingObjects = await this.getAdapterObjectsAsync();
51
63
  // Reset the connection indicator during startup
52
- this.setState('info.connection', false, true);
64
+ this.setConnectionState(false);
65
+ this.uuid = (0, uuid_1.v4)();
53
66
  // connect to Loxone Miniserver
54
- this.client = new loxoneWsApi(this.config.host + ':' + this.config.port, this.config.username, this.config.password, true, 'AES-256-CBC');
55
- this.client.on('connect', () => {
56
- this.log.info('Miniserver connected');
57
- });
58
- this.client.on('authorized', () => {
59
- this.log.debug('authorized');
60
- });
61
- this.client.on('auth_failed', () => {
62
- this.log.error('Miniserver auth failed');
63
- });
64
- this.client.on('connect_failed', () => {
65
- this.log.error('Miniserver connect failed');
66
- });
67
- this.client.on('connection_error', (error) => {
68
- this.log.error('Miniserver connection error: ' + error);
69
- });
70
- this.client.on('close', () => {
71
- this.log.info('connection closed');
72
- // Stop queue and clear it. Issue a warning if it isn't empty.
73
- this.runQueue = false;
74
- if (this.eventsQueue.size() > 0) {
75
- this.log.warn('Event queue is not empty. Discarding ' + this.eventsQueue.size() + ' items');
76
- }
77
- // Yes - I know this could go in the 'if' above but here 'just in case' ;)
78
- this.eventsQueue.clear();
79
- this.setState('info.connection', false, true);
80
- });
81
- this.client.on('send', (message) => {
82
- this.log.debug('sent message: ' + message);
83
- });
84
- this.client.on('message_text', (message) => {
85
- this.log.debug('message_text ' + JSON.stringify(message));
86
- });
87
- this.client.on('message_file', (message) => {
88
- this.log.debug('message_file ' + JSON.stringify(message));
89
- });
90
- this.client.on('message_invalid', (message) => {
91
- this.log.debug('message_invalid ' + JSON.stringify(message));
92
- });
93
- this.client.on('keepalive', (time) => {
94
- this.log.silly('keepalive (' + time + 'ms)');
95
- });
96
- this.client.on('get_structure_file', async (data) => {
97
- this.log.silly(`get_structure_file ${JSON.stringify(data)}`);
98
- this.log.info(`got structure file; last modified on ${data.lastModified}`);
99
- const sentry = this.getSentry();
100
- if (sentry) {
101
- // add a global event processor to upload the structure file (only once)
102
- sentry.addGlobalEventProcessor(this.createSentryEventProcessor(data));
103
- }
104
- try {
105
- await this.loadStructureFileAsync(data);
106
- this.log.debug('structure file successfully loaded');
107
- // we are ready, let's set the connection indicator
108
- this.setState('info.connection', true, true);
109
- }
110
- catch (error) {
111
- this.log.error(`Couldn't load structure file: ${error}`);
112
- sentry === null || sentry === void 0 ? void 0 : sentry.captureException(error, { extra: { data } });
113
- }
114
- });
67
+ const webSocketConfig = new WebSocketConfig(WebSocketConfig.protocol.WS, this.uuid, 'iobroker', WebSocketConfig.permission.APP, false);
115
68
  const handleAnyEvent = (uuid, evt) => {
116
69
  this.log.silly(`received update event: ${JSON.stringify(evt)}: ${uuid}`);
117
70
  this.eventsQueue.enqueue({ uuid, evt });
@@ -121,51 +74,266 @@ class Loxone extends utils.Adapter {
121
74
  (_a = this.getSentry()) === null || _a === void 0 ? void 0 : _a.captureException(e, { extra: { uuid, evt } });
122
75
  });
123
76
  };
124
- this.client.on('update_event_value', handleAnyEvent);
125
- this.client.on('update_event_text', handleAnyEvent);
126
- this.client.on('update_event_daytimer', handleAnyEvent);
127
- this.client.on('update_event_weather', handleAnyEvent);
128
- this.client.connect();
77
+ webSocketConfig.delegate = {
78
+ socketOnDataProgress: (socket, progress) => {
79
+ this.log.debug('data progress ' + progress);
80
+ },
81
+ socketOnTokenConfirmed: (_socket, _response) => {
82
+ this.log.debug('token confirmed');
83
+ },
84
+ socketOnTokenReceived: (_socket, _result) => {
85
+ this.log.debug('token received');
86
+ },
87
+ socketOnConnectionClosed: (socket, code) => {
88
+ this.log.info('Socket closed ' + code);
89
+ this.setConnectionState(false);
90
+ // Stop queue and clear it. Issue a warning if it isn't empty.
91
+ this.runQueue = false;
92
+ if (this.eventsQueue.size() > 0) {
93
+ this.log.warn('Event queue is not empty. Discarding ' + this.eventsQueue.size() + ' items');
94
+ }
95
+ // Yes - I know this could go in the 'if' above but here 'just in case' ;)
96
+ this.eventsQueue.clear();
97
+ if (code != LxCommunicator.SupportCode.WEBSOCKET_MANUAL_CLOSE) {
98
+ this.reconnect();
99
+ }
100
+ },
101
+ socketOnEventReceived: (socket, events, type) => {
102
+ this.log.silly(`socket event received ${type} ${JSON.stringify(events)}`);
103
+ this.incInfoState('info.messagesReceived');
104
+ for (const evt of events) {
105
+ switch (type) {
106
+ case LxCommunicator.BinaryEvent.Type.EVENT:
107
+ handleAnyEvent(evt.uuid, evt.value);
108
+ break;
109
+ case LxCommunicator.BinaryEvent.Type.EVENTTEXT:
110
+ handleAnyEvent(evt.uuid, evt.text);
111
+ break;
112
+ case LxCommunicator.BinaryEvent.Type.EVENT:
113
+ handleAnyEvent(evt.uuid, evt);
114
+ break;
115
+ case LxCommunicator.BinaryEvent.Type.WEATHER:
116
+ handleAnyEvent(evt.uuid, evt);
117
+ break;
118
+ default:
119
+ break;
120
+ }
121
+ }
122
+ },
123
+ };
124
+ this.socket = new LxCommunicator.WebSocket(webSocketConfig);
125
+ await this.connect();
129
126
  this.subscribeStates('*');
130
127
  }
128
+ async loadStructureFile() {
129
+ let file;
130
+ try {
131
+ const fileString = await this.socket.send('data/LoxAPP3.json');
132
+ file = JSON.parse(fileString);
133
+ }
134
+ catch (error) {
135
+ // do not stringify error, it can contain circular references
136
+ this.log.error(`Couldn't get structure file`);
137
+ return false;
138
+ }
139
+ this.log.silly(`get_structure_file ${JSON.stringify(file)}`);
140
+ this.log.info(`got structure file; last modified on ${file.lastModified}`);
141
+ const sentry = this.getSentry();
142
+ if (sentry) {
143
+ // add a global event processor to upload the structure file (only once)
144
+ sentry.addGlobalEventProcessor(this.createSentryEventProcessor(file));
145
+ }
146
+ try {
147
+ await this.loadStructureFileAsync(file);
148
+ this.log.debug('structure file successfully loaded');
149
+ }
150
+ catch (error) {
151
+ // do not stringify error, it can contain circular references
152
+ this.log.error(`Couldn't load structure file`);
153
+ sentry === null || sentry === void 0 ? void 0 : sentry.captureException(error, { extra: { file } });
154
+ return false;
155
+ }
156
+ return true; // Success
157
+ }
158
+ async connect() {
159
+ if (this.connectionInProgress) {
160
+ this.log.warn('Connection already in progress');
161
+ }
162
+ else {
163
+ this.log.info('Trying to connect');
164
+ this.connectionInProgress = true;
165
+ let success = true; // Assume success
166
+ try {
167
+ await this.socket.open(this.config.host + ':' + this.config.port, this.config.username, this.config.password);
168
+ }
169
+ catch (error) {
170
+ // do not stringify error, it can contain circular references
171
+ this.log.error(`Couldn't open socket`);
172
+ success = false;
173
+ }
174
+ if (success) {
175
+ success = await this.loadStructureFile();
176
+ }
177
+ if (success) {
178
+ try {
179
+ await this.socket.send('jdev/sps/enablebinstatusupdate');
180
+ }
181
+ catch (error) {
182
+ // do not stringify error, it can contain circular references
183
+ this.log.error(`Couldn't enable status updates`);
184
+ success = false;
185
+ }
186
+ }
187
+ this.connectionInProgress = false;
188
+ if (!success) {
189
+ this.log.debug('Connection failed - will retry after delay');
190
+ this.socket.close();
191
+ this.reconnect();
192
+ }
193
+ else {
194
+ // We are ready, let's set the connection indicator
195
+ this.setConnectionState(true);
196
+ }
197
+ }
198
+ }
199
+ reconnect() {
200
+ if (this.reconnectTimer) {
201
+ this.log.debug('Reconnect called while timer already running');
202
+ }
203
+ else if (this.connectionInProgress) {
204
+ this.log.debug('Reconnect called while connection in progress');
205
+ }
206
+ else {
207
+ this.reconnectTimer = this.setTimeout(() => {
208
+ delete this.reconnectTimer;
209
+ this.connect().catch((e) => {
210
+ this.log.error(`Couldn't reconnect: ${e}`);
211
+ this.reconnect();
212
+ });
213
+ }, reconnectTimeoutMs);
214
+ }
215
+ }
216
+ setConnectionState(connected) {
217
+ this.lxConnected = connected;
218
+ this.setState('info.connection', this.lxConnected, true);
219
+ }
131
220
  /**
132
221
  * Is called when adapter shuts down - callback has to be called under any circumstances!
133
222
  */
134
223
  onUnload(callback) {
135
224
  try {
136
- if (this.client) {
137
- this.client.close();
138
- delete this.client;
225
+ if (this.socket) {
226
+ this.socket.close();
227
+ delete this.socket;
139
228
  }
140
229
  callback();
141
230
  }
142
231
  catch (e) {
143
232
  callback();
144
233
  }
234
+ this.flushInfoStates();
235
+ // TODO: clear queued state change timers
145
236
  }
146
237
  /**
147
238
  * Is called if a subscribed state changes
148
239
  */
149
- onStateChange(id, state) {
240
+ async onStateChange(id, state) {
150
241
  // Warning: state can be null if it was deleted!
151
242
  if (!id || !state || state.ack) {
152
- return;
243
+ // Do nothing
153
244
  }
154
- this.log.silly(`stateChange ${id} ${JSON.stringify(state)}`);
155
- if (!this.stateChangeListeners.hasOwnProperty(id)) {
156
- const msg = 'Unsupported state change: ' + id;
157
- this.log.error(msg);
158
- if (!this.reportedUnsupportedStateChanges.has(id)) {
159
- this.reportedUnsupportedStateChanges.add(id);
160
- const sentry = this.getSentry();
161
- sentry === null || sentry === void 0 ? void 0 : sentry.withScope((scope) => {
162
- scope.setExtra('state', state);
163
- sentry.captureMessage(msg, SentryNode.Severity.Warning);
164
- });
245
+ else if (id.includes('.info.')) {
246
+ // Ignore info changes
247
+ // TODO: can this be done better by ignoring '.info.' in subscribeStates?
248
+ }
249
+ else {
250
+ this.log.debug(`stateChange ${id} ${JSON.stringify(state)}`);
251
+ if (!this.stateChangeListeners.hasOwnProperty(id)) {
252
+ const msg = 'Unsupported state change: ' + id;
253
+ this.log.error(msg);
254
+ if (!this.reportedUnsupportedStateChanges.has(id)) {
255
+ this.reportedUnsupportedStateChanges.add(id);
256
+ const sentry = this.getSentry();
257
+ sentry === null || sentry === void 0 ? void 0 : sentry.withScope((scope) => {
258
+ scope.setExtra('state', state);
259
+ sentry.captureMessage(msg, 'warning');
260
+ });
261
+ }
262
+ }
263
+ else if (!this.lxConnected) {
264
+ this.log.warn(`stateChange ${id} while disconnected, discarding`);
265
+ this.incInfoState('info.stateChangesDiscarded');
266
+ }
267
+ else {
268
+ const stateChangeListener = this.stateChangeListeners[id];
269
+ if (stateChangeListener.ackTimer) {
270
+ // Ack timer is running: we didn't get a reply from the previous command yet
271
+ if (stateChangeListener.queuedVal !== null) {
272
+ // Already a queued state change: we're going to have to discard that and replace with latest
273
+ this.log.warn(`State change in progress for ${id}, discarding ${stateChangeListener.queuedVal}`);
274
+ this.incInfoState('info.stateChangesDiscarded');
275
+ }
276
+ else {
277
+ // Nothing queued, so this will only be delayed (at least for now)
278
+ this.log.warn(`State change in progress for ${id}, delaying ${state.val}`);
279
+ this.incInfoState('info.stateChangesDelayed');
280
+ }
281
+ stateChangeListener.queuedVal = state.val;
282
+ }
283
+ else {
284
+ // Ack timer is not running, so we're all good to handle this
285
+ await this.handleStateChange(id, stateChangeListener, state.val);
286
+ }
165
287
  }
166
- return;
167
288
  }
168
- this.stateChangeListeners[id](this.currentStateValues[id], state.val);
289
+ }
290
+ convertStateToInt(value) {
291
+ return !value ? 0 : parseInt(value.toString());
292
+ }
293
+ async handleStateChange(id, stateChangeListener, val) {
294
+ var _a, _b, _c, _d, _e, _f, _g, _h;
295
+ if ((_a = stateChangeListener.opts) === null || _a === void 0 ? void 0 : _a.convertToInt) {
296
+ // Convert any values to ints within range if necessary.
297
+ val = this.convertStateToInt(val);
298
+ if (((_b = stateChangeListener.opts) === null || _b === void 0 ? void 0 : _b.minInt) !== undefined && val < stateChangeListener.opts.minInt) {
299
+ val = stateChangeListener.opts.minInt;
300
+ }
301
+ if (((_c = stateChangeListener.opts) === null || _c === void 0 ? void 0 : _c.maxInt) !== undefined && val > stateChangeListener.opts.maxInt) {
302
+ val = stateChangeListener.opts.maxInt;
303
+ }
304
+ }
305
+ if (((_d = stateChangeListener.opts) === null || _d === void 0 ? void 0 : _d.notIfEqual) && this.currentStateValues[id] === val) {
306
+ // new/old values are the same so don't send update.
307
+ // However, ack the state change as we have 'handled' this (by doing nothing)
308
+ this.log.debug(`State value is unchanged, no listener+self-ack: ${id} ${val}`);
309
+ await this.setStateAck(id, val);
310
+ }
311
+ else {
312
+ if (!((_e = stateChangeListener.opts) === null || _e === void 0 ? void 0 : _e.selfAck)) {
313
+ // Set ack timer before calling listener
314
+ stateChangeListener.ackTimer = this.setTimeout(async (id, stateChangeListener) => {
315
+ this.log.warn(`Timeout for ack ${id}`);
316
+ this.incInfoState('info.ackTimeouts', id);
317
+ stateChangeListener.ackTimer = null;
318
+ // Even though this is a timeout, handle any change that may have been delayed waiting for this
319
+ await this.handleDelayedStateChange(id, stateChangeListener);
320
+ }, ((_f = stateChangeListener.opts) === null || _f === void 0 ? void 0 : _f.ackTimeoutMs) ? (_g = stateChangeListener.opts) === null || _g === void 0 ? void 0 : _g.ackTimeoutMs : ackTimeoutMs, id, stateChangeListener);
321
+ }
322
+ // Change will be handled by listener
323
+ stateChangeListener.listener(this.currentStateValues[id], val);
324
+ if ((_h = stateChangeListener.opts) === null || _h === void 0 ? void 0 : _h.selfAck) {
325
+ // Loxone is not expected to send an event to acknowledge this so just do it ourself
326
+ this.log.debug(`Self-ack: ${id} ${val}`);
327
+ await this.setStateAck(id, val);
328
+ }
329
+ }
330
+ }
331
+ async handleDelayedStateChange(id, stateChangeListener) {
332
+ if (stateChangeListener.queuedVal !== null) {
333
+ this.log.debug(`Handling delayed state: ${id} ${stateChangeListener.queuedVal}`);
334
+ await this.handleStateChange(id, stateChangeListener, stateChangeListener.queuedVal);
335
+ stateChangeListener.queuedVal = null;
336
+ }
169
337
  }
170
338
  createSentryEventProcessor(data) {
171
339
  const sentry = this.getSentry();
@@ -180,7 +348,7 @@ class Loxone extends utils.Adapter {
180
348
  type: 'debug',
181
349
  category: 'started',
182
350
  message: `Structure file added to event ${attachmentEventId}`,
183
- level: SentryNode.Severity.Info,
351
+ level: 'info',
184
352
  });
185
353
  }
186
354
  return event;
@@ -190,8 +358,8 @@ class Loxone extends utils.Adapter {
190
358
  return event;
191
359
  }
192
360
  attachmentEventId = event.event_id;
193
- const { host, path, projectId, port, protocol, user } = dsn;
194
- const endpoint = `${protocol}://${host}${port !== '' ? `:${port}` : ''}${path !== '' ? `/${path}` : ''}/api/${projectId}/events/${attachmentEventId}/attachments/?sentry_key=${user}&sentry_version=7&sentry_client=custom-javascript`;
361
+ const { host, path, projectId, port, protocol, publicKey } = dsn;
362
+ const endpoint = `${protocol}://${host}${port !== '' ? `:${port}` : ''}${path !== '' ? `/${path}` : ''}/api/${projectId}/events/${attachmentEventId}/attachments/?sentry_key=${publicKey}&sentry_version=7&sentry_client=custom-javascript`;
195
363
  const form = new FormData();
196
364
  form.append('att', JSON.stringify(data, null, 2), {
197
365
  contentType: 'application/json',
@@ -355,13 +523,14 @@ class Loxone extends utils.Adapter {
355
523
  }
356
524
  }
357
525
  async loadControlAsync(controlType, uuid, control) {
526
+ var _a;
358
527
  const type = control.type || 'None';
359
528
  if (type.match(/[^a-z0-9]/i)) {
360
529
  throw new Error(`Bad control type: ${type}`);
361
530
  }
362
531
  let controlObject;
363
532
  try {
364
- const module = await Promise.resolve().then(() => require(`./controls/${type}`));
533
+ const module = await (_a = `./controls/${type}`, Promise.resolve().then(() => require(_a)));
365
534
  controlObject = new module[type](this);
366
535
  }
367
536
  catch (error) {
@@ -464,6 +633,7 @@ class Loxone extends utils.Adapter {
464
633
  const stateEventHandlerList = this.stateEventHandlers[evt.uuid];
465
634
  if (stateEventHandlerList === undefined) {
466
635
  this.log.debug(`Unknown event ${evt.uuid}: ${JSON.stringify(evt.evt)}`);
636
+ this.incInfoState('info.unknownEvents', evt.uuid, evt.evt);
467
637
  return;
468
638
  }
469
639
  for (const item of stateEventHandlerList) {
@@ -476,9 +646,126 @@ class Loxone extends utils.Adapter {
476
646
  }
477
647
  }
478
648
  }
649
+ async initInfoStates() {
650
+ // Wait for states to load because if we don't, although the chances
651
+ // of processing starting before this actually completes is small, we
652
+ // should cater for that.
653
+ await this.initInfoState('info.ackTimeouts', true);
654
+ await this.initInfoState('info.messagesReceived');
655
+ await this.initInfoState('info.messagesSent');
656
+ await this.initInfoState('info.stateChangesDelayed');
657
+ await this.initInfoState('info.stateChangesDiscarded');
658
+ await this.initInfoState('info.unknownEvents', true);
659
+ }
660
+ async initInfoState(id, hasDetails = false) {
661
+ const state = await this.getStateAsync(id);
662
+ const initValue = state ? state.val : null;
663
+ const entry = {
664
+ value: initValue,
665
+ lastSet: initValue,
666
+ timer: null,
667
+ };
668
+ if (hasDetails) {
669
+ // TODO: Maybe read these in so they persist across restarts?
670
+ entry.detailsMap = new Map();
671
+ }
672
+ this.info.set(id, entry);
673
+ }
674
+ flushInfoStates() {
675
+ // Called on shutdown
676
+ this.info.forEach((infoEntry, key) => {
677
+ if (infoEntry.timer) {
678
+ // Timer running, so cancel it and update state value if changed since last written
679
+ this.clearTimeout(infoEntry.timer);
680
+ this.setInfoStateIfChanged(key, infoEntry, true);
681
+ }
682
+ });
683
+ }
684
+ getInfoEntry(id) {
685
+ const infoEntry = this.info.get(id);
686
+ if (!infoEntry) {
687
+ // This should never happen!
688
+ this.log.error('No info entry for ' + id);
689
+ }
690
+ return infoEntry;
691
+ }
692
+ addInfoDetailsEntry(details, id, value) {
693
+ /// ... and add details of this event to the map.
694
+ const eventEntry = details.get(id);
695
+ if (eventEntry) {
696
+ // Add to existing
697
+ eventEntry.count++;
698
+ if (value !== undefined) {
699
+ eventEntry.lastValue = value;
700
+ }
701
+ }
702
+ else {
703
+ // New entry
704
+ if (value !== undefined) {
705
+ details.set(id, { count: 1, lastValue: value });
706
+ }
707
+ else {
708
+ details.set(id, { count: 1 });
709
+ }
710
+ }
711
+ }
712
+ incInfoState(id, detailId, detailValue) {
713
+ // Increment the given ID
714
+ const infoEntry = this.getInfoEntry(id);
715
+ if (infoEntry) {
716
+ // Can't use ++ here because ioBroker.StateValue isn't necessarily a number
717
+ infoEntry.value = Number(infoEntry.value) + 1;
718
+ // If value given and this entry has details record that
719
+ if (infoEntry.detailsMap && detailId) {
720
+ this.addInfoDetailsEntry(infoEntry.detailsMap, detailId, detailValue);
721
+ }
722
+ if (!infoEntry.timer) {
723
+ this.setInfoStateIfChanged(id, infoEntry);
724
+ }
725
+ }
726
+ }
727
+ buildInfoDetails(src) {
728
+ // TODO: shouldn't this use JSON.stringify?
729
+ const out = [];
730
+ src.forEach((value, key) => {
731
+ if (value.lastValue !== undefined) {
732
+ out.push({ id: key, count: value.count, lastValue: value.lastValue });
733
+ }
734
+ else {
735
+ out.push({ id: key, count: value.count });
736
+ }
737
+ });
738
+ return JSON.stringify(out);
739
+ }
740
+ setInfoStateIfChanged(id, infoEntry, shutdown = false) {
741
+ if (infoEntry.value != infoEntry.lastSet) {
742
+ this.log.silly('value of ' + id + ' changed to ' + infoEntry.value);
743
+ // Store counter
744
+ this.setState(id, infoEntry.value, true);
745
+ infoEntry.lastSet = infoEntry.value;
746
+ // Store any details
747
+ if (infoEntry.detailsMap) {
748
+ this.setState(id + 'Detail', this.buildInfoDetails(infoEntry.detailsMap), true);
749
+ }
750
+ if (!shutdown) {
751
+ // Start a timer which will set the current value from the info ID map on completion
752
+ // Obviously don't do this if called from shutdown
753
+ this.log.silly('Starting timer for ' + id);
754
+ infoEntry.timer = this.setTimeout((cbId, cbInfoEntry) => {
755
+ this.log.silly('Timeout for ' + id);
756
+ // Remove from timer from map as we have just finished
757
+ cbInfoEntry.timer = null;
758
+ // Update the state, but only if the value in the info ID map has changed
759
+ this.setInfoStateIfChanged(cbId, cbInfoEntry);
760
+ }, 30000, // Update every 30s max TODO: make this a config parameter?
761
+ id, infoEntry);
762
+ }
763
+ }
764
+ }
479
765
  sendCommand(uuid, action) {
480
766
  this.log.debug(`Sending command ${uuid} ${action}`);
481
- this.client.send_cmd(uuid, action);
767
+ this.incInfoState('info.messagesSent');
768
+ this.socket.send(`jdev/sps/io/${uuid}/${action}`, 2);
482
769
  }
483
770
  getExistingObject(id) {
484
771
  const fullId = this.namespace + '.' + id;
@@ -549,11 +836,38 @@ class Loxone extends utils.Adapter {
549
836
  }
550
837
  return found;
551
838
  }
552
- addStateChangeListener(id, listener) {
553
- this.stateChangeListeners[this.namespace + '.' + id] = listener;
839
+ addStateChangeListener(id, listener, opts) {
840
+ this.stateChangeListeners[this.namespace + '.' + id] = {
841
+ listener,
842
+ opts,
843
+ queuedVal: null,
844
+ ackTimer: null,
845
+ };
846
+ }
847
+ async checkStateForAck(id) {
848
+ const stateChangeListener = this.stateChangeListeners[id];
849
+ if (stateChangeListener) {
850
+ // This state change could be a result of a command we sent being ack'd
851
+ if (stateChangeListener.ackTimer) {
852
+ // Timer is running so clear it
853
+ this.log.debug(`Clearing ackTimer for ${id}`);
854
+ this.clearTimeout(stateChangeListener.ackTimer);
855
+ stateChangeListener.ackTimer = null;
856
+ // Send any command that may have been delayed waiting for this ack
857
+ await this.handleDelayedStateChange(id, stateChangeListener);
858
+ }
859
+ else {
860
+ this.log.debug(`No ackTimer for ${id}`);
861
+ }
862
+ }
863
+ else {
864
+ this.log.silly(`${id} has no stateChangeListener`);
865
+ }
554
866
  }
555
867
  async setStateAck(id, value) {
556
- this.currentStateValues[this.namespace + '.' + id] = value;
868
+ const keyId = this.namespace + '.' + id;
869
+ this.currentStateValues[keyId] = value;
870
+ await this.checkStateForAck(keyId);
557
871
  await this.setStateAsync(id, { val: value, ack: true });
558
872
  }
559
873
  getCachedStateValue(id) {
@@ -574,7 +888,7 @@ class Loxone extends utils.Adapter {
574
888
  reportError(message) {
575
889
  var _a;
576
890
  this.log.error(message);
577
- (_a = this.getSentry()) === null || _a === void 0 ? void 0 : _a.captureMessage(message, SentryNode.Severity.Error);
891
+ (_a = this.getSentry()) === null || _a === void 0 ? void 0 : _a.captureMessage(message, 'error');
578
892
  }
579
893
  }
580
894
  exports.Loxone = Loxone;