iobroker.al-ko 0.3.0 → 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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2019
3
+ Copyright (c) 2026 Hubert Zechner <hubertiob@posteo.at>
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -51,29 +51,41 @@ Do **not** contact AL-KO customer service regarding this project.
51
51
 
52
52
  ---
53
53
 
54
- ## Latest Changes
54
+ ## Changelog
55
55
 
56
- ### **0.2.16 (unreleased / development)**
56
+ ### 0.3.2 (2026-03-12)
57
57
 
58
- - Improved instance handling and object hierarchy
59
- - Better separation of writable and non-writable states
60
- - Added axios global timeout
61
- - Introduced adapter-safe timers (`this.setTimeout`, `this.setInterval`)
62
- - Many internal improvements and preparations for future patches
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
63
62
 
64
- ### **0.2.15 (2025-11-02)**
63
+ ### 0.3.1 (2026-03-09)
65
64
 
66
- - Cleaned up admin/jsonConfig structure for adapter-check
67
- - Added missing `size` attributes
68
- - Added `.commitinfo` to `.gitignore`
65
+ - Documentation improvements
66
+ - Corrected LICENSE information
67
+ - Updated development dependencies
68
+ - Minor CI / workflow cleanup
69
69
  - No functional changes
70
70
 
71
- **Full changelog here:**
72
- **[CHANGELOG.md](./CHANGELOG.md)**
71
+ ### 0.3.0 (2026-03-09)
72
+
73
+ - Major maintenance release
74
+ - Updated ESLint 9, Prettier 3 and TypeScript tooling
75
+ - Updated development dependencies
76
+ - Improved CI pipeline and adapter structure
77
+ - No functional changes
78
+
79
+ ➡ Full changelog here:
80
+ [CHANGELOG.md](./CHANGELOG.md)
73
81
 
74
82
  ---
75
83
 
76
84
  ## License
77
85
 
86
+ MIT License
87
+
88
+ Copyright (c) 2026 Hubert Zechner <hubertiob@posteo.at>
89
+
78
90
  This project is released under the **MIT License**.
79
91
  See the included **LICENSE** file for full details.
package/docs/de/README.md CHANGED
@@ -48,7 +48,19 @@ AL-KO bietet **keinen offiziellen Support** hierfür.
48
48
 
49
49
  ## Änderungen (Auszug)
50
50
 
51
- ### **0.3.0**
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
+
58
+ ### 0.3.1 (2026-03-09)
59
+
60
+ - Verbesserte Dokumentation
61
+ - Lizenzangaben korrigiert
62
+ - Entwicklungsabhängigkeiten aktualisiert
63
+ - Keine funktionalen Änderungen
52
64
 
53
65
  Alle Änderungen siehe vollständigen Changelog:
54
66
  ➡ [CHANGELOG.md](../../CHANGELOG.md)
package/docs/en/README.md CHANGED
@@ -48,7 +48,19 @@ It is a **community-developed project**.
48
48
 
49
49
  ## Changes (Summary)
50
50
 
51
- ### **0.3.0**
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
+
58
+ ### 0.3.1 (2026-03-09)
59
+
60
+ - Documentation improvements
61
+ - Corrected LICENSE information
62
+ - Updated development dependencies
63
+ - No functional changes
52
64
 
53
65
  See full changelog here:
54
66
  ➡ [CHANGELOG.md](../../CHANGELOG.md)
package/io-package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "al-ko",
4
- "version": "0.3.0",
4
+ "version": "0.3.2",
5
5
  "tier": 3,
6
6
  "titleLang": {
7
7
  "en": "AL-KO",
@@ -40,6 +40,18 @@
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
+ },
47
+ "0.3.1": {
48
+ "en": "Documentation improvements, corrected license information and updated development dependencies. No functional changes.",
49
+ "de": "Dokumentation verbessert, Lizenzangaben korrigiert und Entwicklungsabhängigkeiten aktualisiert. Keine funktionalen Änderungen."
50
+ },
51
+ "0.3.0": {
52
+ "en": "Major maintenance release. Updated ESLint 9, Prettier 3, TypeScript tooling and development dependencies. Improved adapter structure and CI pipeline. No functional changes.",
53
+ "de": "Größeres Wartungsrelease. Aktualisierung von ESLint 9, Prettier 3, TypeScript-Tooling und Entwicklungsabhängigkeiten. Verbesserte Adapterstruktur und CI-Pipeline. Keine funktionalen Änderungen."
54
+ },
43
55
  "0.2.15": {
44
56
  "en": "Admin config cleanup for adapter-check: removed $schema, corrected structure, added missing size attributes.",
45
57
  "de": "Admin-Konfiguration für adapter-check bereinigt: $schema entfernt, Struktur korrigiert, fehlende size-Attribute ergänzt."
@@ -51,14 +63,6 @@
51
63
  "0.2.13": {
52
64
  "en": "Fixed JSON syntax error in io-package.json. No functional changes.",
53
65
  "de": "JSON-Syntaxfehler in io-package.json behoben. Keine funktionalen Änderungen."
54
- },
55
- "0.2.12": {
56
- "en": "Fixed jsonConfig schema URL, added missing size attributes, increased minimum requirements.",
57
- "de": "Schema-URL korrigiert, fehlende Size-Attribute ergänzt, Mindestanforderungen erhöht."
58
- },
59
- "0.2.11": {
60
- "en": "Minor code corrections.",
61
- "de": "Kleine Code-Korrekturen."
62
66
  }
63
67
  },
64
68
  "keywords": ["al-ko", "Robolinho", "mower", "garden", "smart-garden"],
@@ -81,7 +85,7 @@
81
85
  ],
82
86
  "globalDependencies": [
83
87
  {
84
- "admin": ">=7.6.17"
88
+ "admin": ">=7.6.20"
85
89
  }
86
90
  ]
87
91
  },
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.0",
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",
@@ -56,13 +56,13 @@
56
56
  "ws": "^8.19.0"
57
57
  },
58
58
  "devDependencies": {
59
- "@alcalzone/release-script": "^5.0.0",
60
- "@alcalzone/release-script-plugin-iobroker": "^4.0.0",
61
- "@alcalzone/release-script-plugin-license": "^4.0.0",
62
- "@alcalzone/release-script-plugin-manual-review": "^4.0.0",
63
- "@iobroker/adapter-dev": "^1.0.0",
59
+ "@alcalzone/release-script": "^5.1.1",
60
+ "@alcalzone/release-script-plugin-iobroker": "^5.1.2",
61
+ "@alcalzone/release-script-plugin-license": "^5.1.1",
62
+ "@alcalzone/release-script-plugin-manual-review": "^5.1.1",
63
+ "@iobroker/adapter-dev": "^1.5.0",
64
64
  "@iobroker/eslint-config": "^2.2.0",
65
- "@iobroker/testing": "^5.1.1",
65
+ "@iobroker/testing": "^5.2.2",
66
66
  "@tsconfig/node20": "^20.1.8",
67
67
  "@types/node": "^25.1.0",
68
68
  "@typescript-eslint/eslint-plugin": "^8.54.0",