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.
- package/LICENSE +183 -183
- package/README.md +23 -1
- package/admin/words.js +18 -18
- package/build/controls/AalEmergency.js +2 -2
- package/build/controls/AalEmergency.js.map +1 -1
- package/build/controls/AalSmartAlarm.js +2 -2
- package/build/controls/AalSmartAlarm.js.map +1 -1
- package/build/controls/Alarm.js +2 -2
- package/build/controls/Alarm.js.map +1 -1
- package/build/controls/AlarmClock.js +2 -2
- package/build/controls/AlarmClock.js.map +1 -1
- package/build/controls/AudioZone.js +2 -2
- package/build/controls/AudioZone.js.map +1 -1
- package/build/controls/CentralAlarm.js +3 -3
- package/build/controls/CentralAlarm.js.map +1 -1
- package/build/controls/CentralAudioZone.js +1 -1
- package/build/controls/CentralAudioZone.js.map +1 -1
- package/build/controls/CentralGate.js +3 -3
- package/build/controls/CentralGate.js.map +1 -1
- package/build/controls/CentralJalousie.js +4 -4
- package/build/controls/CentralJalousie.js.map +1 -1
- package/build/controls/CentralLightController.js +1 -1
- package/build/controls/CentralLightController.js.map +1 -1
- package/build/controls/Daytimer.js +1 -1
- package/build/controls/Daytimer.js.map +1 -1
- package/build/controls/Dimmer.js +2 -2
- package/build/controls/Dimmer.js.map +1 -1
- package/build/controls/EIBDimmer.js +2 -2
- package/build/controls/EIBDimmer.js.map +1 -1
- package/build/controls/Gate.js +9 -14
- package/build/controls/Gate.js.map +1 -1
- package/build/controls/Hourcounter.js +2 -2
- package/build/controls/Hourcounter.js.map +1 -1
- package/build/controls/IRCV2Daytimer.js +9 -0
- package/build/controls/IRCV2Daytimer.js.map +1 -0
- package/build/controls/IRoomControllerV2.js +105 -6
- package/build/controls/IRoomControllerV2.js.map +1 -1
- package/build/controls/Intercom.js +1 -1
- package/build/controls/Intercom.js.map +1 -1
- package/build/controls/Jalousie.js +10 -13
- package/build/controls/Jalousie.js.map +1 -1
- package/build/controls/LightController.js +3 -3
- package/build/controls/LightController.js.map +1 -1
- package/build/controls/LightControllerV2.js +2 -2
- package/build/controls/LightControllerV2.js.map +1 -1
- package/build/controls/MailBox.js +2 -2
- package/build/controls/MailBox.js.map +1 -1
- package/build/controls/Meter.js +1 -1
- package/build/controls/Meter.js.map +1 -1
- package/build/controls/Pushbutton.js +3 -6
- package/build/controls/Pushbutton.js.map +1 -1
- package/build/controls/Radio.js +1 -4
- package/build/controls/Radio.js.map +1 -1
- package/build/controls/Remote.js +36 -0
- package/build/controls/Remote.js.map +1 -0
- package/build/controls/SmokeAlarm.js +2 -2
- package/build/controls/SmokeAlarm.js.map +1 -1
- package/build/controls/Switch.js +2 -5
- package/build/controls/Switch.js.map +1 -1
- package/build/controls/TimedSwitch.js +3 -3
- package/build/controls/TimedSwitch.js.map +1 -1
- package/build/controls/Unknown.js +1 -2
- package/build/controls/Unknown.js.map +1 -1
- package/build/loxone-handler-base.js +4 -4
- package/build/loxone-handler-base.js.map +1 -1
- package/build/main.js +410 -96
- package/build/main.js.map +1 -1
- package/build/weather-server-handler.js +4 -4
- package/build/weather-server-handler.js.map +1 -1
- package/io-package.json +298 -332
- package/package.json +40 -30
- package/build/lib/tools.js +0 -96
- 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
|
|
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.
|
|
64
|
+
this.setConnectionState(false);
|
|
65
|
+
this.uuid = (0, uuid_1.v4)();
|
|
53
66
|
// connect to Loxone Miniserver
|
|
54
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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.
|
|
137
|
-
this.
|
|
138
|
-
delete this.
|
|
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
|
-
|
|
243
|
+
// Do nothing
|
|
153
244
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
194
|
-
const endpoint = `${protocol}://${host}${port !== '' ? `:${port}` : ''}${path !== '' ? `/${path}` : ''}/api/${projectId}/events/${attachmentEventId}/attachments/?sentry_key=${
|
|
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(
|
|
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.
|
|
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] =
|
|
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.
|
|
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,
|
|
891
|
+
(_a = this.getSentry()) === null || _a === void 0 ? void 0 : _a.captureMessage(message, 'error');
|
|
578
892
|
}
|
|
579
893
|
}
|
|
580
894
|
exports.Loxone = Loxone;
|