iobroker.al-ko 0.3.1 → 0.3.2

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/README.md CHANGED
@@ -53,6 +53,13 @@ Do **not** contact AL-KO customer service regarding this project.
53
53
 
54
54
  ## Changelog
55
55
 
56
+ ### 0.3.2 (2026-03-12)
57
+
58
+ - Improved WebSocket reconnect handling after token refresh
59
+ - Prevented reconnect loops on intentional WebSocket closes
60
+ - Improved API error logging for push requests
61
+ - Added WebSocket close code and reason logging
62
+
56
63
  ### 0.3.1 (2026-03-09)
57
64
 
58
65
  - Documentation improvements
@@ -69,13 +76,6 @@ Do **not** contact AL-KO customer service regarding this project.
69
76
  - Improved CI pipeline and adapter structure
70
77
  - No functional changes
71
78
 
72
- ### 0.2.15 (2025-11-02)
73
-
74
- - Cleaned up admin/jsonConfig structure for adapter-check
75
- - Added missing `size` attributes
76
- - Added `.commitinfo` to `.gitignore`
77
- - No functional changes
78
-
79
79
  ➡ Full changelog here:
80
80
  [CHANGELOG.md](./CHANGELOG.md)
81
81
 
package/docs/de/README.md CHANGED
@@ -48,6 +48,13 @@ AL-KO bietet **keinen offiziellen Support** hierfür.
48
48
 
49
49
  ## Änderungen (Auszug)
50
50
 
51
+ ### 0.3.2 (2026-03-12)
52
+
53
+ - WebSocket-Reconnect nach Token-Aktualisierung verbessert
54
+ - Reconnect-Schleifen bei absichtlich geschlossenen WebSocket-Verbindungen verhindert
55
+ - API-Fehlerlogging für Push-Requests verbessert
56
+ - Logging für WebSocket-Close-Code und Reason ergänzt
57
+
51
58
  ### 0.3.1 (2026-03-09)
52
59
 
53
60
  - Verbesserte Dokumentation
package/docs/en/README.md CHANGED
@@ -48,6 +48,13 @@ It is a **community-developed project**.
48
48
 
49
49
  ## Changes (Summary)
50
50
 
51
+ ### 0.3.2 (2026-03-12)
52
+
53
+ - Improved WebSocket reconnect handling after token refresh
54
+ - Prevented reconnect loops on intentional WebSocket closes
55
+ - Improved API error logging for push requests
56
+ - Added WebSocket close code and reason logging
57
+
51
58
  ### 0.3.1 (2026-03-09)
52
59
 
53
60
  - Documentation improvements
package/io-package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "al-ko",
4
- "version": "0.3.1",
4
+ "version": "0.3.2",
5
5
  "tier": 3,
6
6
  "titleLang": {
7
7
  "en": "AL-KO",
@@ -40,6 +40,10 @@
40
40
  "de": "docs/de/README.md"
41
41
  },
42
42
  "news": {
43
+ "0.3.2": {
44
+ "en": "Improved WebSocket reconnect handling, prevented reconnect loops on intentional closes, and improved API error logging for push requests.",
45
+ "de": "WebSocket-Reconnect-Verhalten verbessert, Reconnect-Schleifen bei absichtlich geschlossenen Verbindungen verhindert und API-Fehlerlogging für Push-Requests verbessert."
46
+ },
43
47
  "0.3.1": {
44
48
  "en": "Documentation improvements, corrected license information and updated development dependencies. No functional changes.",
45
49
  "de": "Dokumentation verbessert, Lizenzangaben korrigiert und Entwicklungsabhängigkeiten aktualisiert. Keine funktionalen Änderungen."
package/main.js CHANGED
@@ -83,7 +83,9 @@ class AlKoAdapter extends utils.Adapter {
83
83
  this.log.info("Adapter is ready.");
84
84
  } catch (err) {
85
85
  this.log.error(
86
- `Startup error: ${err.response?.data || err.message || err}`,
86
+ `Startup error: ${err.response?.status || ""} ${
87
+ JSON.stringify(err.response?.data, null, 2) || err.message
88
+ }`,
87
89
  );
88
90
  }
89
91
  }
@@ -139,6 +141,34 @@ class AlKoAdapter extends utils.Adapter {
139
141
  this.tokenExpiresAt = Date.now() + res.data.expires_in * 1000;
140
142
 
141
143
  this.log.debug("Access token refreshed successfully.");
144
+
145
+ for (const deviceId of Object.keys(this.webSockets)) {
146
+ try {
147
+ this.log.debug(
148
+ `Reconnecting WebSocket for ${deviceId} after token refresh`,
149
+ );
150
+
151
+ // const ws = this.webSockets[deviceId];
152
+ // if (ws) {
153
+ // ws.terminate();
154
+ //}
155
+
156
+ const ws = this.webSockets[deviceId];
157
+ if (ws) {
158
+ ws._intentionalClose = true;
159
+ ws.terminate();
160
+ }
161
+
162
+ this.clearInterval(this.pingIntervals[deviceId]);
163
+ this.clearTimeout(this.pongTimeouts[deviceId]);
164
+
165
+ this.connectWebSocket(deviceId);
166
+ } catch (err) {
167
+ this.log.warn(
168
+ `Failed to reconnect WebSocket for ${deviceId}: ${err.message}`,
169
+ );
170
+ }
171
+ }
142
172
  }
143
173
  }
144
174
 
@@ -227,24 +257,137 @@ class AlKoAdapter extends utils.Adapter {
227
257
  return res.data;
228
258
  }
229
259
 
260
+ // ---------------- WebSocket Handling ----------------
261
+ // alte Version
262
+
263
+ //connectWebSocket(deviceId) {
264
+ // if (!this.accessToken) {
265
+ // return;
266
+ // }
267
+ //
268
+ // const url = `wss://socket.al-ko.com/v1?Authorization=${this.accessToken}&thingName=${deviceId}`;
269
+ // const ws = new WebSocket(url);
270
+ //
271
+ // ws.isAlive = true;
272
+ //
273
+ // ws.on("open", () => {
274
+ // this.log.debug(`WebSocket connected for device ${deviceId}`);
275
+ //
276
+ // this.pingIntervals[deviceId] = this.setInterval(() => {
277
+ // if (ws.readyState === WebSocket.OPEN) {
278
+ // ws.ping();
279
+ // ws.isAlive = false;
280
+ //
281
+ // this.pongTimeouts[deviceId] = this.setTimeout(() => {
282
+ // if (!ws.isAlive) {
283
+ // this.log.warn(
284
+ // `WebSocket ping timeout for ${deviceId}, closing connection.`,
285
+ // );
286
+ // ws.terminate();
287
+ // }
288
+ // }, 30000);
289
+ // }
290
+ // }, 120000);
291
+ // });
292
+ //
293
+ // ws.on("pong", () => {
294
+ // ws.isAlive = true;
295
+ // this.clearTimeout(this.pongTimeouts[deviceId]);
296
+ // });
297
+ //
298
+ // ws.on("message", async (msg) => {
299
+ // if (this.config.wsDebug) {
300
+ // this.log.debug(`WebSocket message (${deviceId}): ${msg}`);
301
+ // }
302
+ // try {
303
+ // const data = JSON.parse(msg.toString());
304
+ // if (data && data.state) {
305
+ // const newState = data.state.reported || data.state;
306
+ //
307
+ // this.deviceStates[deviceId] = this.deepMerge(
308
+ // this.deviceStates[deviceId] || {},
309
+ // newState,
310
+ // );
311
+ //
312
+ // await this.createStatesRecursive(
313
+ // `${this.namespace}.${deviceId}.state`,
314
+ // this.deviceStates[deviceId],
315
+ // "",
316
+ // );
317
+ // }
318
+ // } catch (e) {
319
+ // this.log.error(
320
+ // `Error processing WebSocket message for device ${deviceId}: ${e.message}`,
321
+ // );
322
+ // }
323
+ // });
324
+ //
325
+ // ws.on("close", () => {
326
+ // this.log.warn(
327
+ // `WebSocket closed for device ${deviceId}. Retrying in 10 seconds.`,
328
+ // );
329
+ // this.clearInterval(this.pingIntervals[deviceId]);
330
+ // this.clearTimeout(this.pongTimeouts[deviceId]);
331
+ //
332
+ // this.reconnectTimeouts[deviceId] = this.setTimeout(
333
+ // () => this.connectWebSocket(deviceId),
334
+ // 10000,
335
+ // );
336
+ // });
337
+ //
338
+ // ws.on("error", (err) => {
339
+ // this.log.error(`WebSocket error for ${deviceId}: ${err.message}`);
340
+ // try {
341
+ // ws.terminate();
342
+ // } catch {}
343
+ // });
344
+ //
345
+ // this.webSockets[deviceId] = ws;
346
+ //}
347
+
230
348
  // ---------------- WebSocket Handling ----------------
231
349
  connectWebSocket(deviceId) {
232
- if (!this.accessToken) {
350
+ if (!this.accessToken || this._stopRequested) {
233
351
  return;
234
352
  }
235
353
 
354
+ if (this.reconnectTimeouts[deviceId]) {
355
+ this.clearTimeout(this.reconnectTimeouts[deviceId]);
356
+ delete this.reconnectTimeouts[deviceId];
357
+ }
358
+
359
+ const existingWs = this.webSockets[deviceId];
360
+ if (existingWs) {
361
+ try {
362
+ existingWs._intentionalClose = true;
363
+ existingWs.terminate();
364
+ } catch {}
365
+ }
366
+
236
367
  const url = `wss://socket.al-ko.com/v1?Authorization=${this.accessToken}&thingName=${deviceId}`;
237
368
  const ws = new WebSocket(url);
238
369
 
239
370
  ws.isAlive = true;
371
+ ws._intentionalClose = false;
240
372
 
241
373
  ws.on("open", () => {
242
374
  this.log.debug(`WebSocket connected for device ${deviceId}`);
243
375
 
376
+ if (this.pingIntervals[deviceId]) {
377
+ this.clearInterval(this.pingIntervals[deviceId]);
378
+ }
379
+ if (this.pongTimeouts[deviceId]) {
380
+ this.clearTimeout(this.pongTimeouts[deviceId]);
381
+ }
382
+
244
383
  this.pingIntervals[deviceId] = this.setInterval(() => {
245
384
  if (ws.readyState === WebSocket.OPEN) {
246
- ws.ping();
247
385
  ws.isAlive = false;
386
+ ws.ping();
387
+
388
+ if (this.pongTimeouts[deviceId]) {
389
+ this.clearTimeout(this.pongTimeouts[deviceId]);
390
+ }
248
391
 
249
392
  this.pongTimeouts[deviceId] = this.setTimeout(() => {
250
393
  if (!ws.isAlive) {
@@ -260,7 +403,10 @@ class AlKoAdapter extends utils.Adapter {
260
403
 
261
404
  ws.on("pong", () => {
262
405
  ws.isAlive = true;
263
- this.clearTimeout(this.pongTimeouts[deviceId]);
406
+ if (this.pongTimeouts[deviceId]) {
407
+ this.clearTimeout(this.pongTimeouts[deviceId]);
408
+ delete this.pongTimeouts[deviceId];
409
+ }
264
410
  });
265
411
 
266
412
  ws.on("message", async (msg) => {
@@ -290,17 +436,36 @@ class AlKoAdapter extends utils.Adapter {
290
436
  }
291
437
  });
292
438
 
293
- ws.on("close", () => {
439
+ ws.on("close", (code, reason) => {
440
+ if (this.pingIntervals[deviceId]) {
441
+ this.clearInterval(this.pingIntervals[deviceId]);
442
+ delete this.pingIntervals[deviceId];
443
+ }
444
+ if (this.pongTimeouts[deviceId]) {
445
+ this.clearTimeout(this.pongTimeouts[deviceId]);
446
+ delete this.pongTimeouts[deviceId];
447
+ }
448
+
449
+ if (this.webSockets[deviceId] === ws) {
450
+ delete this.webSockets[deviceId];
451
+ }
452
+
453
+ if (this._stopRequested || ws._intentionalClose) {
454
+ return;
455
+ }
456
+
294
457
  this.log.warn(
295
- `WebSocket closed for device ${deviceId}. Retrying in 10 seconds.`,
458
+ `WebSocket closed for device ${deviceId}. Code: ${code}, Reason: ${reason?.toString() || "none"}. Retrying in 10 seconds.`,
296
459
  );
297
- this.clearInterval(this.pingIntervals[deviceId]);
298
- this.clearTimeout(this.pongTimeouts[deviceId]);
299
460
 
300
- this.reconnectTimeouts[deviceId] = this.setTimeout(
301
- () => this.connectWebSocket(deviceId),
302
- 10000,
303
- );
461
+ if (this.reconnectTimeouts[deviceId]) {
462
+ this.clearTimeout(this.reconnectTimeouts[deviceId]);
463
+ }
464
+
465
+ this.reconnectTimeouts[deviceId] = this.setTimeout(() => {
466
+ delete this.reconnectTimeouts[deviceId];
467
+ this.connectWebSocket(deviceId);
468
+ }, 10000);
304
469
  });
305
470
 
306
471
  ws.on("error", (err) => {
@@ -536,8 +701,14 @@ class AlKoAdapter extends utils.Adapter {
536
701
  this.log.info(`Push successful: ${id}`);
537
702
  this.updateDeviceStateCache(deviceId, relPathArr, state.val);
538
703
  } catch (err) {
704
+ const status = err.response?.status;
705
+ const data =
706
+ typeof err.response?.data === "object"
707
+ ? JSON.stringify(err.response.data, null, 2)
708
+ : err.response?.data;
709
+
539
710
  this.log.error(
540
- `Error pushing state ${id}: ${err.response?.status} ${err.response?.data || err.message}`,
711
+ `Error pushing state ${id}: ${status || ""} ${data || err.message}`,
541
712
  );
542
713
  } finally {
543
714
  this.pendingPushes.delete(id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.al-ko",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Adapter for communication with AL-KO smart garden devices (Robolinho, mowing windows, operationState, etc.)",
5
5
  "author": {
6
6
  "name": "Hubert Zechner",