iobroker.al-ko 0.3.1 → 0.3.3

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,28 +53,12 @@ Do **not** contact AL-KO customer service regarding this project.
53
53
 
54
54
  ## Changelog
55
55
 
56
- ### 0.3.1 (2026-03-09)
56
+ ### 0.3.3 (2026-03-13)
57
57
 
58
- - Documentation improvements
59
- - Corrected LICENSE information
58
+ - Improved WebSocket reconnect handling
59
+ - Fixed processing of AL-KO WebSocket reportedState messages
60
+ - Updated Dependabot and workflow configuration
60
61
  - Updated development dependencies
61
- - Minor CI / workflow cleanup
62
- - No functional changes
63
-
64
- ### 0.3.0 (2026-03-09)
65
-
66
- - Major maintenance release
67
- - Updated ESLint 9, Prettier 3 and TypeScript tooling
68
- - Updated development dependencies
69
- - Improved CI pipeline and adapter structure
70
- - No functional changes
71
-
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
62
 
79
63
  ➡ Full changelog here:
80
64
  [CHANGELOG.md](./CHANGELOG.md)
package/docs/de/README.md CHANGED
@@ -48,6 +48,21 @@ AL-KO bietet **keinen offiziellen Support** hierfür.
48
48
 
49
49
  ## Änderungen (Auszug)
50
50
 
51
+ ### 0.3.3 (2026-03-13)
52
+
53
+ - WebSocket-Verarbeitung verbessert
54
+ - Verarbeitung der AL-KO `reportedState` WebSocket-Nachrichten korrigiert
55
+ - Stabileres Wiederverbinden der WebSocket-Verbindung
56
+ - GitHub-Workflows aktualisiert (Dependabot / Automerge)
57
+ - Entwicklungsabhängigkeiten aktualisiert
58
+
59
+ ### 0.3.2 (2026-03-12)
60
+
61
+ - WebSocket-Reconnect nach Token-Aktualisierung verbessert
62
+ - Reconnect-Schleifen bei absichtlich geschlossenen WebSocket-Verbindungen verhindert
63
+ - API-Fehlerlogging für Push-Requests verbessert
64
+ - Logging für WebSocket-Close-Code und Reason ergänzt
65
+
51
66
  ### 0.3.1 (2026-03-09)
52
67
 
53
68
  - Verbesserte Dokumentation
package/docs/en/README.md CHANGED
@@ -48,12 +48,12 @@ It is a **community-developed project**.
48
48
 
49
49
  ## Changes (Summary)
50
50
 
51
- ### 0.3.1 (2026-03-09)
51
+ ### 0.3.3 (2026-03-13)
52
52
 
53
- - Documentation improvements
54
- - Corrected LICENSE information
53
+ - Improved WebSocket reconnect handling
54
+ - Fixed processing of AL-KO WebSocket reportedState messages
55
+ - Updated Dependabot and workflow configuration
55
56
  - Updated development dependencies
56
- - No functional changes
57
57
 
58
58
  See full changelog here:
59
59
  ➡ [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.1",
4
+ "version": "0.3.3",
5
5
  "tier": 3,
6
6
  "titleLang": {
7
7
  "en": "AL-KO",
@@ -40,6 +40,14 @@
40
40
  "de": "docs/de/README.md"
41
41
  },
42
42
  "news": {
43
+ "0.3.3": {
44
+ "en": "Improved WebSocket handling and message processing, updated repository workflows, and updated development dependencies.",
45
+ "de": "WebSocket-Handling und Nachrichtenverarbeitung verbessert, Repository-Workflows aktualisiert und Entwicklungsabhängigkeiten aktualisiert."
46
+ },
47
+ "0.3.2": {
48
+ "en": "Improved WebSocket reconnect handling, prevented reconnect loops on intentional closes, and improved API error logging for push requests.",
49
+ "de": "WebSocket-Reconnect-Verhalten verbessert, Reconnect-Schleifen bei absichtlich geschlossenen Verbindungen verhindert und API-Fehlerlogging für Push-Requests verbessert."
50
+ },
43
51
  "0.3.1": {
44
52
  "en": "Documentation improvements, corrected license information and updated development dependencies. No functional changes.",
45
53
  "de": "Dokumentation verbessert, Lizenzangaben korrigiert und Entwicklungsabhängigkeiten aktualisiert. Keine funktionalen Änderungen."
@@ -47,18 +55,6 @@
47
55
  "0.3.0": {
48
56
  "en": "Major maintenance release. Updated ESLint 9, Prettier 3, TypeScript tooling and development dependencies. Improved adapter structure and CI pipeline. No functional changes.",
49
57
  "de": "Größeres Wartungsrelease. Aktualisierung von ESLint 9, Prettier 3, TypeScript-Tooling und Entwicklungsabhängigkeiten. Verbesserte Adapterstruktur und CI-Pipeline. Keine funktionalen Änderungen."
50
- },
51
- "0.2.15": {
52
- "en": "Admin config cleanup for adapter-check: removed $schema, corrected structure, added missing size attributes.",
53
- "de": "Admin-Konfiguration für adapter-check bereinigt: $schema entfernt, Struktur korrigiert, fehlende size-Attribute ergänzt."
54
- },
55
- "0.2.14": {
56
- "en": "Updated development tooling: release-script v5, plugins v4, ESLint 9, TypeScript 5.9, Prettier 3.",
57
- "de": "Entwicklungswerkzeuge aktualisiert: release-script v5, Plugins v4, ESLint 9, TypeScript 5.9, Prettier 3."
58
- },
59
- "0.2.13": {
60
- "en": "Fixed JSON syntax error in io-package.json. No functional changes.",
61
- "de": "JSON-Syntaxfehler in io-package.json behoben. Keine funktionalen Änderungen."
62
58
  }
63
59
  },
64
60
  "keywords": ["al-ko", "Robolinho", "mower", "garden", "smart-garden"],
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,29 +403,51 @@ 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) => {
267
413
  if (this.config.wsDebug) {
268
414
  this.log.debug(`WebSocket message (${deviceId}): ${msg}`);
269
415
  }
416
+
270
417
  try {
271
418
  const data = JSON.parse(msg.toString());
272
- if (data && data.state) {
273
- const newState = data.state.reported || data.state;
274
419
 
275
- this.deviceStates[deviceId] = this.deepMerge(
276
- this.deviceStates[deviceId] || {},
277
- newState,
278
- );
420
+ let newState = null;
421
+
422
+ if (data?.reportedState?.state?.reported) {
423
+ newState = data.reportedState.state.reported;
424
+ } else if (
425
+ data?.reportedState &&
426
+ typeof data.reportedState === "object" &&
427
+ !data.reportedState.state &&
428
+ !data.reportedState.previous &&
429
+ !data.reportedState.current &&
430
+ !data.reportedState.metadata
431
+ ) {
432
+ newState = data.reportedState;
433
+ } else if (data?.state?.reported) {
434
+ newState = data.state.reported;
435
+ }
279
436
 
280
- await this.createStatesRecursive(
281
- `${this.namespace}.${deviceId}.state`,
282
- this.deviceStates[deviceId],
283
- "",
284
- );
437
+ if (!newState || typeof newState !== "object") {
438
+ return;
285
439
  }
440
+
441
+ this.deviceStates[deviceId] = this.deepMerge(
442
+ this.deviceStates[deviceId] || {},
443
+ newState,
444
+ );
445
+
446
+ await this.createStatesRecursive(
447
+ `${this.namespace}.${deviceId}.state`,
448
+ this.deviceStates[deviceId],
449
+ "",
450
+ );
286
451
  } catch (e) {
287
452
  this.log.error(
288
453
  `Error processing WebSocket message for device ${deviceId}: ${e.message}`,
@@ -290,17 +455,36 @@ class AlKoAdapter extends utils.Adapter {
290
455
  }
291
456
  });
292
457
 
293
- ws.on("close", () => {
458
+ ws.on("close", (code, reason) => {
459
+ if (this.pingIntervals[deviceId]) {
460
+ this.clearInterval(this.pingIntervals[deviceId]);
461
+ delete this.pingIntervals[deviceId];
462
+ }
463
+ if (this.pongTimeouts[deviceId]) {
464
+ this.clearTimeout(this.pongTimeouts[deviceId]);
465
+ delete this.pongTimeouts[deviceId];
466
+ }
467
+
468
+ if (this.webSockets[deviceId] === ws) {
469
+ delete this.webSockets[deviceId];
470
+ }
471
+
472
+ if (this._stopRequested || ws._intentionalClose) {
473
+ return;
474
+ }
475
+
294
476
  this.log.warn(
295
- `WebSocket closed for device ${deviceId}. Retrying in 10 seconds.`,
477
+ `WebSocket closed for device ${deviceId}. Code: ${code}, Reason: ${reason?.toString() || "none"}. Retrying in 10 seconds.`,
296
478
  );
297
- this.clearInterval(this.pingIntervals[deviceId]);
298
- this.clearTimeout(this.pongTimeouts[deviceId]);
299
479
 
300
- this.reconnectTimeouts[deviceId] = this.setTimeout(
301
- () => this.connectWebSocket(deviceId),
302
- 10000,
303
- );
480
+ if (this.reconnectTimeouts[deviceId]) {
481
+ this.clearTimeout(this.reconnectTimeouts[deviceId]);
482
+ }
483
+
484
+ this.reconnectTimeouts[deviceId] = this.setTimeout(() => {
485
+ delete this.reconnectTimeouts[deviceId];
486
+ this.connectWebSocket(deviceId);
487
+ }, 10000);
304
488
  });
305
489
 
306
490
  ws.on("error", (err) => {
@@ -536,8 +720,14 @@ class AlKoAdapter extends utils.Adapter {
536
720
  this.log.info(`Push successful: ${id}`);
537
721
  this.updateDeviceStateCache(deviceId, relPathArr, state.val);
538
722
  } catch (err) {
723
+ const status = err.response?.status;
724
+ const data =
725
+ typeof err.response?.data === "object"
726
+ ? JSON.stringify(err.response.data, null, 2)
727
+ : err.response?.data;
728
+
539
729
  this.log.error(
540
- `Error pushing state ${id}: ${err.response?.status} ${err.response?.data || err.message}`,
730
+ `Error pushing state ${id}: ${status || ""} ${data || err.message}`,
541
731
  );
542
732
  } finally {
543
733
  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.3",
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",
@@ -64,9 +64,9 @@
64
64
  "@iobroker/eslint-config": "^2.2.0",
65
65
  "@iobroker/testing": "^5.2.2",
66
66
  "@tsconfig/node20": "^20.1.8",
67
- "@types/node": "^25.1.0",
68
- "@typescript-eslint/eslint-plugin": "^8.54.0",
69
- "@typescript-eslint/parser": "^8.54.0",
67
+ "@types/node": "^25.5.0",
68
+ "@typescript-eslint/eslint-plugin": "^8.57.0",
69
+ "@typescript-eslint/parser": "^8.57.0",
70
70
  "prettier": "^3.8.1",
71
71
  "proxyquire": "^2.1.3"
72
72
  },