iobroker.zwavews 0.1.3 → 0.1.5

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
@@ -35,8 +35,14 @@ Activate WS Server Settings in `zwave-js-ui` we use the Home Assistant Settings
35
35
 
36
36
 
37
37
  ## Changelog
38
+ ### 0.1.5 (2026-04-21)
39
+ * (arteck) upd devicemanager
40
+
41
+ ### 0.1.4 (2026-04-16)
42
+ * (arteck) Dependencies have been updated
43
+ * (arteck) add vscode folder
44
+
38
45
  ### 0.1.3 (2026-04-03)
39
- * (arteck) fix unknown state from scene
40
46
  * (arteck) del last dot from DP
41
47
  * (arteck) fix scene
42
48
 
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "zwavews",
4
- "version": "0.1.3",
4
+ "version": "0.1.5",
5
5
  "news": {
6
+ "0.1.5": {
7
+ "en": "upd devicemanager",
8
+ "de": "gerätemanager und -manager",
9
+ "ru": "улучшенный devicemanager",
10
+ "pt": "gerenciador de dispositivos upd",
11
+ "nl": "upd apparaatbeheerder",
12
+ "fr": "gestionnaire d'appareil",
13
+ "it": "upd devicemanager",
14
+ "es": "upd devicemanager",
15
+ "pl": "upd devicemaniger",
16
+ "uk": "напляскване",
17
+ "zh-cn": "上调设备管理器"
18
+ },
19
+ "0.1.4": {
20
+ "en": "Dependencies have been updated\nadd vscode folder",
21
+ "de": "Abhängigkeiten wurden aktualisiert\nvscode ordner hinzufügen",
22
+ "ru": "Зависимости были обновлены\nдобавить vscode папку",
23
+ "pt": "As dependências foram atualizadas\nadicionar pasta vscode",
24
+ "nl": "Afhankelijkheden zijn bijgewerkt\nvscode map toevoegen",
25
+ "fr": "Les dépendances ont été actualisées\najouter un dossier vscode",
26
+ "it": "Le dipendenze sono state aggiornate\naggiungere cartella vscode",
27
+ "es": "Se han actualizado las dependencias\nañadir carpeta vscode",
28
+ "pl": "Zaktualizowano zależności\ndodaj folder vscode",
29
+ "uk": "Залежність було оновлено\nдодати папку проти коду",
30
+ "zh-cn": "依赖关系已更新\n添加 vscode 文件夹"
31
+ },
6
32
  "0.1.3": {
7
33
  "en": "fix unknown state from scene\ndel last dot from DP\nfix scene",
8
34
  "de": "unbekannter zustand von szene\nder letzte Punkt von DP\nfixe szene",
@@ -67,32 +93,6 @@
67
93
  "pl": "dodaj info.sendMessageDopuszczalny obiekt, aby umożliwić wysyłanie wiadomości do zwave- ui- js\ndodaj nową opcję do ustawienia info.sendMessage Dozwolone bezpośrednio po uruchomieniu adaptera",
68
94
  "uk": "додайте інформацію.sendMessageВсього об'єкту, щоб дозволити надсилати повідомлення на zwave-ui-js\nдодати нову прапорець, щоб встановити інформацію.sendMessage Допускається відразу після запуску адаптера",
69
95
  "zh-cn": "添加信息. sendMessage Allowed 对象允许将消息发送到 zwave- ui- js\n添加新复选框以设置信息. sendMessage 启动适配器后立即允许"
70
- },
71
- "0.0.17": {
72
- "en": "fix adapter start\nDependencies have been updated",
73
- "de": "befestigungsadapter start\nAbhängigkeiten wurden aktualisiert",
74
- "ru": "запуск адаптера\nЗависимости были обновлены",
75
- "pt": "corrigir o início do adaptador\nAs dependências foram atualizadas",
76
- "nl": "fix adapter start\nAfhankelijkheden zijn bijgewerkt",
77
- "fr": "fixer le démarrage de l'adaptateur\nLes dépendances ont été actualisées",
78
- "it": "avvio dell'adattatore\nLe dipendenze sono state aggiornate",
79
- "es": "adaptador de fijación\nSe han actualizado las dependencias",
80
- "pl": "uruchomić adapter\nZaktualizowano zależności",
81
- "uk": "запуск адаптера\nЗалежність було оновлено",
82
- "zh-cn": "固定适配器启动\n依赖关系已更新"
83
- },
84
- "0.0.16": {
85
- "en": "fix warning message",
86
- "de": "warnmeldung aktivieren",
87
- "ru": "исправить предупреждающее сообщение",
88
- "pt": "corrigir a mensagem de aviso",
89
- "nl": "waarschuwingsbericht herstellen",
90
- "fr": "corriger le message d'avertissement",
91
- "it": "correzione del messaggio di avviso",
92
- "es": "mensaje de advertencia",
93
- "pl": "naprawić komunikat ostrzegawczy",
94
- "uk": "фіксувати повідомлення про попередження",
95
- "zh-cn": "修补警告消息"
96
96
  }
97
97
  },
98
98
  "titleLang": {
@@ -180,7 +180,7 @@
180
180
  ],
181
181
  "globalDependencies": [
182
182
  {
183
- "admin": ">=7.6.17"
183
+ "admin": ">=7.6.20"
184
184
  }
185
185
  ],
186
186
  "plugins": {
package/lib/devicemgmt.js CHANGED
@@ -17,21 +17,28 @@ class dmZwave extends dmUtils.DeviceManagement {
17
17
  }
18
18
 
19
19
  /**
20
+ * Loads all ZWave devices and reports them to the device manager context.
21
+ * Called by the dm-utils framework in response to a 'dm:loadDevices' message.
20
22
  *
23
+ * @param {object} context - The DeviceLoadContext (addDevice / setTotalDevices / complete).
21
24
  */
22
- async listDevices() {
25
+ async loadDevices(context) {
23
26
  const devices = await this.adapter.getDevicesAsync();
24
- const arrDevices = [];
27
+ context.setTotalDevices(devices.length);
28
+
25
29
  for (const i in devices) {
26
30
  const status = {};
27
31
 
28
32
  const nodeId = this.stripIobPrefix(devices[i]._id);
29
33
 
30
- const device = this.adapter.nodeCache[nodeId].nodeData;
31
-
32
- if (device.ready) {
33
- status.connection = device.ready ? 'connected' : 'disconnected';
34
+ const cacheEntry = this.adapter.nodeCache[nodeId];
35
+ if (!cacheEntry) {
36
+ this.adapter.log.warn(`listDevices: nodeCache miss for ${nodeId}, skipping.`);
37
+ continue;
34
38
  }
39
+ const device = cacheEntry.nodeData;
40
+
41
+ status.connection = device.ready ? 'connected' : 'disconnected';
35
42
 
36
43
  //const link_quality = await this.adapter.getStateAsync(`${theDevice._id}.status`);
37
44
  //status.rssi = link_quality.val == 'alive' ? '100' : '0';
@@ -60,12 +67,53 @@ class dmZwave extends dmUtils.DeviceManagement {
60
67
  devStatus = 'unknown';
61
68
  }
62
69
 
70
+ // Sensordaten aus Multilevel_Sensor laden
71
+ const sensorCustomInfo = {
72
+ id: nodeId,
73
+ schema: {
74
+ type: 'panel',
75
+ items: {},
76
+ },
77
+ };
78
+
79
+ try {
80
+ const sensorObjects = await this.adapter.getObjectViewAsync('system', 'state', {
81
+ startkey: `${devices[i]._id}.Multilevel_Sensor.`,
82
+ endkey: `${devices[i]._id}.Multilevel_Sensor.\u9999`,
83
+ });
84
+
85
+ if (sensorObjects && sensorObjects.rows && sensorObjects.rows.length > 0) {
86
+ for (const row of sensorObjects.rows) {
87
+ const obj = row.value;
88
+ if (!obj) {
89
+ continue;
90
+ }
91
+ const stateId = obj._id;
92
+ const sensorKey = stateId.replace(/\./g, '_');
93
+ const labelParts = stateId.split('.');
94
+ const sensorLabel = labelParts[labelParts.length - 1];
95
+ const unit = obj.common?.unit ? ` (${obj.common.unit})` : '';
96
+
97
+ sensorCustomInfo.schema.items[sensorKey] = {
98
+ type: 'state',
99
+ oid: stateId,
100
+ foreign: true,
101
+ label: `${sensorLabel}${unit}`,
102
+ newLine: true,
103
+ };
104
+ }
105
+ }
106
+ } catch (e) {
107
+ this.adapter.log.warn(`listDevices: Fehler beim Laden der Multilevel_Sensor-Daten für ${nodeId}: ${e.message}`);
108
+ }
109
+
110
+
63
111
  const res = {
64
112
  id: nodeId,
65
113
  name: device.name || device.label,
66
114
  icon: devStatus,
67
- manufacturer: device.deviceConfig.manufacturer,
68
- model: `${device.deviceConfig.label } ${ device.deviceConfig.description}`,
115
+ manufacturer: device.deviceConfig?.manufacturer ?? '',
116
+ model: `${device.deviceConfig?.label ?? ''} ${device.deviceConfig?.description ?? ''}`.trim(),
69
117
  status: status,
70
118
  hasDetails: true,
71
119
  actions: [
@@ -78,17 +126,122 @@ class dmZwave extends dmUtils.DeviceManagement {
78
126
  ],
79
127
  };
80
128
 
129
+ // Schalter aus Multilevel_Switch laden
130
+ try {
131
+ const switchObjects = await this.adapter.getObjectViewAsync('system', 'state', {
132
+ startkey: `${devices[i]._id}.Multilevel_Switch.`,
133
+ endkey: `${devices[i]._id}.Multilevel_Switch.\u9999`,
134
+ });
135
+
136
+ if (switchObjects && switchObjects.rows && switchObjects.rows.length > 0) {
137
+ // Trennlinie einfügen, wenn bereits Sensordaten vorhanden
138
+ if (Object.keys(sensorCustomInfo.schema.items).length > 0) {
139
+ sensorCustomInfo.schema.items['_divider_switch'] = {
140
+ type: 'divider',
141
+ color: 'primary',
142
+ };
143
+ }
144
+
145
+ // Alle Rows in eine Map sammeln (rawName → {obj, stateId})
146
+ const switchMap = {};
147
+ for (const row of switchObjects.rows) {
148
+ const obj = row.value;
149
+ if (!obj) {
150
+ continue;
151
+ }
152
+ const parts = obj._id.split('.');
153
+ const rawName = parts[parts.length - 1];
154
+ switchMap[rawName] = { obj, stateId: obj._id };
155
+ }
156
+
157
+ // Gewünschte Reihenfolge, unbekannte States werden danach angehängt
158
+ const order = ['open', 'close', 'currentValue', 'targetValue', 'restorePrevious', 'duration'];
159
+ const allKeys = [...order, ...Object.keys(switchMap).filter(k => !order.includes(k))];
160
+
161
+ allKeys.forEach((rawName, index) => {
162
+ if (!switchMap[rawName]) {
163
+ return;
164
+ }
165
+ const { obj, stateId } = switchMap[rawName];
166
+ // Nummerierten Prefix damit Admin-UI die Elemente in der richtigen Reihenfolge anzeigt
167
+ const switchKey = `_sw${String(index + 1).padStart(2, '0')}_${rawName}`;
168
+ const isBoolean = obj.common?.type === 'boolean';
81
169
 
82
- arrDevices.push(res);
170
+ // Anzeigenamen umbenennen
171
+ let switchLabel;
172
+ if (rawName === 'targetValue') {
173
+ switchLabel = 'Target';
174
+ } else if (rawName === 'currentValue') {
175
+ switchLabel = 'Current';
176
+ } else {
177
+ switchLabel = rawName;
178
+ }
179
+
180
+ // currentValue: nur lesebarer numerischer Wert mit %-Einheit
181
+ // duration: nur lesebarer numerischer Wert
182
+ if (rawName === 'currentValue' && !isBoolean) {
183
+ sensorCustomInfo.schema.items[switchKey] = {
184
+ type: 'state',
185
+ oid: stateId,
186
+ foreign: true,
187
+ label: switchLabel,
188
+ readOnly: true,
189
+ unit: '%',
190
+ newLine: true,
191
+ };
192
+ } else if (rawName === 'duration' && !isBoolean) {
193
+ sensorCustomInfo.schema.items[switchKey] = {
194
+ type: 'state',
195
+ oid: stateId,
196
+ foreign: true,
197
+ label: switchLabel,
198
+ readOnly: true,
199
+ newLine: true,
200
+ };
201
+ } else if (isBoolean) {
202
+ sensorCustomInfo.schema.items[switchKey] = {
203
+ type: 'state',
204
+ oid: stateId,
205
+ foreign: true,
206
+ label: switchLabel,
207
+ control: 'switch',
208
+ trueTextStyle: { color: 'green' },
209
+ falseTextStyle: { color: 'red' },
210
+ trueText: 'ON',
211
+ falseText: 'OFF',
212
+ newLine: true,
213
+ };
214
+ } else {
215
+ sensorCustomInfo.schema.items[switchKey] = {
216
+ type: 'state',
217
+ oid: stateId,
218
+ foreign: true,
219
+ label: switchLabel,
220
+ control: 'slider',
221
+ min: obj.common?.min ?? 0,
222
+ max: obj.common?.max ?? 99,
223
+ newLine: true,
224
+ };
225
+ }
226
+ });
227
+ }
228
+ } catch (e) {
229
+ this.adapter.log.warn(`listDevices: Fehler beim Laden der Multilevel_Switch-Daten für ${nodeId}: ${e.message}`);
230
+ }
231
+
232
+
233
+ // Nur customInfo anhängen, wenn Sensor- oder Schalterdaten vorhanden sind
234
+ if (Object.keys(sensorCustomInfo.schema.items).length > 0) {
235
+ res.customInfo = sensorCustomInfo;
236
+ }
237
+
238
+
239
+ context.addDevice(res);
83
240
  }
84
241
 
85
242
  // nach id sortieren (z.B. nodeID_2 vor nodeID_10)
86
- arrDevices.sort((a, b) => String(a?.id ?? '').localeCompare(String(b?.id ?? ''), undefined, {
87
- numeric: true,
88
- sensitivity: 'base',
89
- }));
90
-
91
- return arrDevices;
243
+ // Note: sorting is informational only; context already sent devices
244
+ context.complete();
92
245
  }
93
246
 
94
247
  /**
@@ -145,10 +298,10 @@ class dmZwave extends dmUtils.DeviceManagement {
145
298
  * Returns the detail schema and data for a specific device.
146
299
  *
147
300
  * @param {string} id - The node ID of the device.
148
- * @param {object} action - The action object passed by the device management framework.
149
- * @param {object} context - The device management context.
301
+ * @param {object} _action - The action object passed by the device management framework.
302
+ * @param {object} _context - The device management context.
150
303
  */
151
- async getDeviceDetails(id, action, context) {
304
+ async getDeviceDetails(id, _action, _context) {
152
305
  this.adapter.log.debug('getDeviceDetails');
153
306
 
154
307
  const device = this.adapter.nodeCache[id]?.nodeData;
@@ -269,6 +422,10 @@ class dmZwave extends dmUtils.DeviceManagement {
269
422
  devStatus = 'unknown';
270
423
  }
271
424
 
425
+ // Kalibrierungsknopf prüfen
426
+ const calibStateId = `${this.adapter.namespace}.${id}.Configuration.Forced_Roller_Shutter_Calibration`;
427
+ const calibObj = await this.adapter.getObjectAsync(calibStateId).catch(() => null);
428
+
272
429
  return {
273
430
  id: String(device.nodeId),
274
431
  schema: {
@@ -338,6 +495,34 @@ class dmZwave extends dmUtils.DeviceManagement {
338
495
  label: 'Max Baud Rate',
339
496
  readOnly: true,
340
497
  },
498
+ ...(calibObj ? {
499
+ _divider_calib: {
500
+ type: 'divider',
501
+ color: 'primary',
502
+ },
503
+ _calib_spacer: {
504
+ type: 'staticText',
505
+ text: '',
506
+ newLine: true,
507
+ xs: 8,
508
+ sm: 9,
509
+ md: 10,
510
+ lg: 10,
511
+ xl: 10,
512
+ },
513
+ _calib_button: {
514
+ type: 'state',
515
+ oid: calibStateId,
516
+ foreign: true,
517
+ label: 'Kalibrierung starten',
518
+ control: 'button',
519
+ xs: 4,
520
+ sm: 3,
521
+ md: 2,
522
+ lg: 2,
523
+ xl: 2,
524
+ },
525
+ } : {}),
341
526
  },
342
527
  },
343
528
  _tab_Details: {
@@ -370,18 +555,16 @@ class dmZwave extends dmUtils.DeviceManagement {
370
555
  * @param {number} time - The timestamp in milliseconds (epoch).
371
556
  * @param {'ISO_8601'|'ISO_8601_local'|'epoch'|'relative'} type - The desired output format.
372
557
  */
373
- async formatDate(time, type) { //'ISO_8601' | 'ISO_8601_local' | 'epoch' | 'relative'
558
+ formatDate(time, type) { //'ISO_8601' | 'ISO_8601_local' | 'epoch' | 'relative'
374
559
  if (type === 'ISO_8601') {
375
- return new Date(time).toISOString();
376
- } else if (type === 'ISO_8601_local') {
377
- return this.toLocalISOString(new Date(time));
378
- } else if (type === 'epoch') {
379
- return time;
380
- }
381
- // relative
382
- const ago = `${humanizeDuration(Date.now() - time, {language: 'en', largest: 2, round: true}) } ago`;
383
- return ago;
384
-
560
+ return new Date(time).toISOString();
561
+ } else if (type === 'ISO_8601_local') {
562
+ return this.toLocalISOString(new Date(time));
563
+ } else if (type === 'epoch') {
564
+ return time;
565
+ }
566
+ // relative
567
+ return `${humanizeDuration(Date.now() - time, { language: 'en', largest: 2, round: true })} ago`;
385
568
  }
386
569
 
387
570
  /**
package/lib/helper.js CHANGED
@@ -26,6 +26,33 @@ class Helper {
26
26
 
27
27
  }
28
28
 
29
+ /**
30
+ * Normalises any value to a valid ioBroker common.type string.
31
+ * Valid types: "number" | "string" | "boolean" | "array" | "object" | "mixed" | "file"
32
+ *
33
+ * @param {*} value - The raw value whose type should be determined.
34
+ * @param {string} [hint] - An optional type hint (e.g. from metadata.type).
35
+ * @returns {string} A valid ioBroker type string.
36
+ */
37
+ normalizeType(value, hint) {
38
+ const VALID = new Set(["number", "string", "boolean", "array", "object", "mixed", "file"]);
39
+ if (hint && VALID.has(hint)) {
40
+ return hint;
41
+ }
42
+ if (Array.isArray(value)) {
43
+ return "array";
44
+ }
45
+ const t = typeof value;
46
+ if (t === "number") {
47
+ return "number";
48
+ }
49
+ if (t === "boolean") {
50
+ return "boolean";
51
+ }
52
+ // strings are stored as "mixed" to allow numeric/bool changes later
53
+ return "mixed";
54
+ }
55
+
29
56
  /**
30
57
  * Creates a ZWave node device and all its value states in ioBroker.
31
58
  *
@@ -55,9 +82,9 @@ class Helper {
55
82
  await this.createReadyStatus(nodeId);
56
83
 
57
84
  const valuesOnly = element.values ?? null;
58
- delete element.values;
85
+ const { values: _values, ...elementWithoutValues } = element;
59
86
 
60
- await this.parse(`${nodeId}.info`, element);
87
+ await this.parse(`${nodeId}.info`, elementWithoutValues);
61
88
 
62
89
  if (valuesOnly != null && typeof valuesOnly === "object" && valuesOnly.length > 0) {
63
90
  for (const v of valuesOnly) {
@@ -112,7 +139,8 @@ class Helper {
112
139
  metadata.value = v.value; // add value for resolution
113
140
  const valDp = this.resolveCommandClassValue(metadata) ?? 0;
114
141
 
115
- let typeDp = metadata.type === "timeout" ? "number" : metadata.type;
142
+ const rawType = metadata.type === "timeout" ? "number" : metadata.type;
143
+ let typeDp = this.normalizeType(valDp, rawType);
116
144
 
117
145
  if (constant.mixedType.includes(nam_id)) {
118
146
  typeDp = "mixed";
@@ -177,7 +205,7 @@ class Helper {
177
205
  async parse(path, element, options = { write: false },change = false) {
178
206
  let parsePath = utils.deleteLastDot(utils.formatObject(path));
179
207
 
180
- if (element === undefined) {
208
+ if (element === undefined || element === null) {
181
209
  this.adapter.log.error(`Skip undefined value for ${parsePath}`);
182
210
  return;
183
211
  }
@@ -189,8 +217,19 @@ class Helper {
189
217
 
190
218
  if (!this.alreadyCreatedObjects[parsePath]) {
191
219
  try {
192
- let common = {};
193
- if (typeof element === "string" || typeof element === "number") {
220
+ let common;
221
+ if (typeof element === "boolean") {
222
+ common = {
223
+ id: parsePath,
224
+ name: parsePath,
225
+ role: "switch",
226
+ type: "boolean",
227
+ write: options.write,
228
+ read: true,
229
+ def: false,
230
+ };
231
+ } else {
232
+ // string or number
194
233
  common = {
195
234
  id: parsePath,
196
235
  name: parsePath,
@@ -222,20 +261,19 @@ class Helper {
222
261
  return;
223
262
  }
224
263
 
225
- options.channelName = utils.getLastSegment(parsePath);
264
+ const channelName = utils.getLastSegment(parsePath);
226
265
 
227
266
  if (!this.alreadyCreatedObjects[parsePath]) {
228
267
  try {
229
268
  await this.adapter.setObjectNotExistsAsync(parsePath, {
230
269
  type: "channel",
231
270
  common: {
232
- name: options.channelName || ""
271
+ name: channelName || ""
233
272
  },
234
273
  native: {},
235
274
  });
236
275
 
237
276
  this.alreadyCreatedObjects[parsePath] = { };
238
- delete options.channelName;
239
277
  } catch (error) {
240
278
  this.adapter.log.error(`parse error ${ parsePath}`);
241
279
  this.adapter.log.error(error);
@@ -300,7 +338,7 @@ class Helper {
300
338
 
301
339
  if (!this.alreadyCreatedObjects[fullPath]) {
302
340
  const objectName = options.descriptions?.[key] || key;
303
- let typeDp = typeof valDP === "string" ? "mixed" : (valDP != null ? typeof valDP : "mixed");
341
+ let typeDp = this.normalizeType(valDP);
304
342
 
305
343
  if (constant.mixedType.includes(key)) {
306
344
  typeDp = "mixed";
@@ -335,10 +373,9 @@ class Helper {
335
373
  await this.changeState(fullPath, valDP, change);
336
374
 
337
375
  if (valDP !== undefined) {
338
- if (fullPath.endsWith('ready') ) {
339
- valDP = element['status'];
340
- if (utils.isNumeric(valDP) && valDP === 3) {
341
- fullPath = fullPath.replace(".status", ".ready");
376
+ if (fullPath.endsWith('ready')) {
377
+ const statusVal = element['status'];
378
+ if (utils.isNumeric(statusVal) && statusVal === 3) {
342
379
  await this.changeState(fullPath, false);
343
380
  }
344
381
  }
@@ -375,15 +412,11 @@ class Helper {
375
412
 
376
413
  for (let i = 0; i < array.length; i++) {
377
414
  const arrayElement = array[i];
378
- // const index = (i + 1).toString().padStart(2, "0");
379
415
 
380
416
  if (typeof arrayElement === "string") {
381
- if (key === undefined || key === "") {
382
- key = arrayElement;
383
- }
384
-
417
+ const segKey = (key === undefined || key === "") ? arrayElement : key;
385
418
  await this.parse(
386
- `${path}.${key}.${arrayElement}`,
419
+ `${path}.${segKey}`,
387
420
  arrayElement,
388
421
  options,
389
422
  );
@@ -443,6 +476,10 @@ class Helper {
443
476
  resolveCommandClassValue(element) {
444
477
  const type = element.type;
445
478
 
479
+ if (!type) {
480
+ return element.value ?? 0;
481
+ }
482
+
446
483
  if (type === "any" || type === "color") {
447
484
  element.type = "mixed";
448
485
  return typeof element.value === "object"
@@ -487,10 +524,10 @@ class Helper {
487
524
  }
488
525
 
489
526
  if (type === "number") {
490
- if (element?.value) {
527
+ if (element.value != null) {
491
528
  return utils.isNumeric(element.value) ? element.value : 0;
492
529
  }
493
- return element.value ?? element.min;
530
+ return element.min ?? 0;
494
531
  }
495
532
 
496
533
  return element.readable === false
@@ -545,18 +582,22 @@ class Helper {
545
582
  */
546
583
  async updateDevice(nodeId, element, nameChange = true) {
547
584
  const obj = await this.adapter.getObjectAsync(nodeId);
548
- if (obj) {
549
- if (nameChange) {
550
- const newName = element.name || element.productLabel || element.manufacturer || element.newValue;
551
-
552
- if (obj.common?.name !== newName) {
553
- obj.common = obj.common ?? {};
554
- obj.common.name = newName;
555
- } else {
556
- const newDesc = element.desc;
557
- obj.common = obj.common ?? {};
558
- obj.common.desc = newDesc;
559
- }
585
+ if (!obj) {
586
+ return;
587
+ }
588
+
589
+ obj.common = obj.common ?? {};
590
+
591
+ if (nameChange) {
592
+ const newName = element.name || element.productLabel || element.manufacturer || element.newValue;
593
+ if (newName !== undefined && obj.common.name !== newName) {
594
+ obj.common.name = newName;
595
+ await this.adapter.setObjectAsync(nodeId, obj);
596
+ }
597
+ } else {
598
+ const newDesc = element.desc;
599
+ if (newDesc !== undefined) {
600
+ obj.common.desc = newDesc;
560
601
  await this.adapter.setObjectAsync(nodeId, obj);
561
602
  }
562
603
  }
@@ -571,9 +612,9 @@ class Helper {
571
612
  */
572
613
  async changeState(path, value, change = false) {
573
614
  if (change) {
574
- this.adapter.setState(path, value, true);
615
+ await this.adapter.setStateAsync(path, value, true);
575
616
  } else {
576
- this.adapter.setStateChanged(path, value, true);
617
+ await this.adapter.setStateChangedAsync(path, value, true);
577
618
  }
578
619
  }
579
620
 
package/lib/messages.js CHANGED
@@ -11,7 +11,7 @@ async function adapterInfo(config, log) {
11
11
  log.info(`|| zwaveWS Frontend Server: ${config.webUIServer}`);
12
12
  log.info(`|| zwaveWS Frontend Port: ${config.webUIPort}`);
13
13
  log.info(`|| zwaveWS Connection Type: ${config.connectionType}`);
14
- if (config.connectionType == "ws") {
14
+ if (config.connectionType === "ws") {
15
15
  log.info(`|| zwaveWS Websocket Scheme: ${config.wsScheme}`);
16
16
  log.info(`|| zwaveWS Websocket Server: ${config.wsServerIP}`);
17
17
  log.info(`|| zwaveWS Websocket Port: ${config.wsServerPort}`);
@@ -21,11 +21,11 @@ async function adapterInfo(config, log) {
21
21
  log.info(
22
22
  `|| zwaveWS Websocket Dummy MQTT-Server: ${config.dummyMqtt ? "activated" : "deactivated"}`,
23
23
  );
24
- if (config.dummyMqtt == true) {
24
+ if (config.dummyMqtt === true) {
25
25
  log.info(`|| zwaveWS Dummy MQTT IP-Bind: ${config.mqttServerIPBind}`);
26
26
  log.info(`|| zwaveWS Dummy MQTT Port: ${config.mqttServerPort}`);
27
27
  }
28
- } else if (config.connectionType == "exmqtt") {
28
+ } else if (config.connectionType === "exmqtt") {
29
29
  log.info(
30
30
  `|| zwaveWS Externanl MQTT Server: ${config.externalMqttServerIP}`,
31
31
  );
@@ -35,7 +35,7 @@ async function adapterInfo(config, log) {
35
35
  log.info(
36
36
  `|| zwaveWS Externanl MQTT Credentials: ${config.externalMqttServerCredentials ? "use" : "unused"}`,
37
37
  );
38
- } else if (config.connectionType == "intmqtt") {
38
+ } else if (config.connectionType === "intmqtt") {
39
39
  log.info(`|| zwaveWS Internal MQTT IP-Bind: ${config.mqttServerIPBind}`);
40
40
  log.info(`|| zwaveWS Internal MQTT Port: ${config.mqttServerPort}`);
41
41
  }
@@ -67,7 +67,7 @@ class MqttServerController {
67
67
  *
68
68
  */
69
69
  closeServer() {
70
- if (mqttServer && !mqttServer.closed()) {
70
+ if (mqttServer && mqttServer.listening) {
71
71
  mqttServer.close();
72
72
  }
73
73
  }
@@ -45,9 +45,10 @@ class StatesController {
45
45
  async subscribeAllWritableExistsStates() {
46
46
  const writableStates = {};
47
47
 
48
+ const ns = `${this.adapter.namespace}.`;
48
49
  const res = await this.adapter.getObjectViewAsync("system", "state", {
49
- startkey: "zwaveWS.",
50
- endkey: "zwaveWS.\u9999",
50
+ startkey: ns,
51
+ endkey: `${ns}\u9999`,
51
52
  });
52
53
 
53
54
  for (const row of res.rows) {
package/lib/utils.js CHANGED
@@ -41,12 +41,8 @@ function miredKelvinConversion(t) {
41
41
  * @param {number} decimal - The decimal number to convert.
42
42
  * @param {number} padding - The minimum length of the resulting hex string.
43
43
  */
44
- function decimalToHex(decimal, padding) {
44
+ function decimalToHex(decimal, padding = 2) {
45
45
  let hex = Number(decimal).toString(16);
46
- padding =
47
- typeof padding === "undefined" || padding === null
48
- ? (padding = 2)
49
- : padding;
50
46
 
51
47
  while (hex.length < padding) {
52
48
  hex = `0${hex}`;
@@ -145,6 +141,9 @@ function isNumeric(value) {
145
141
  * @param {string} str - The string to process.
146
142
  */
147
143
  function replaceLastDot(str) {
144
+ if (typeof str !== "string") {
145
+ return "";
146
+ }
148
147
  const idx = str.lastIndexOf(".");
149
148
  return idx >= 0 ? `${str.slice(0, idx)}_${str.slice(idx + 1)}` : str;
150
149
  }
@@ -155,6 +154,9 @@ function replaceLastDot(str) {
155
154
  * @param {string|undefined} str - The string to process.
156
155
  */
157
156
  function deleteLastDot(str) {
157
+ if (typeof str !== "string") {
158
+ return "";
159
+ }
158
160
  return str.endsWith(".") ? str.slice(0, -1) : str;
159
161
  }
160
162
 
@@ -217,8 +219,8 @@ function getStatusText(status) {
217
219
  function formatNodeId(nodeIdOriginal) {
218
220
  let nodeId = nodeIdOriginal;
219
221
 
220
- if (this.isNumeric(nodeIdOriginal)) {
221
- nodeId = this.padNodeId(`nodeID_${nodeIdOriginal}`);
222
+ if (isNumeric(nodeIdOriginal)) {
223
+ nodeId = padNodeId(`nodeID_${nodeIdOriginal}`);
222
224
  }
223
225
  return nodeId;
224
226
  }
@@ -28,7 +28,7 @@ class WebsocketController {
28
28
  try {
29
29
  let wsURL = `${this.adapter.config.wsScheme}://${this.adapter.config.wsServerIP}:${this.adapter.config.wsServerPort}/api`;
30
30
 
31
- if (this.adapter.config.wsTokenEnabled == true) {
31
+ if (this.adapter.config.wsTokenEnabled === true) {
32
32
  wsURL += `?token=${this.adapter.config.wsToken}`;
33
33
  }
34
34
 
@@ -104,7 +104,7 @@ class WebsocketController {
104
104
  /**
105
105
  * Schedules an automatic reconnect attempt after the configured restart timeout.
106
106
  */
107
- async autoRestart() {
107
+ autoRestart() {
108
108
  this.adapter.log.warn(`Start try again in ${restartTimeout / 1000} seconds...`);
109
109
  autoRestartTimeout = setTimeout(() => {
110
110
  this.adapter.startWebsocket();
@@ -123,7 +123,7 @@ class WebsocketController {
123
123
  /**
124
124
  * Clears all active timers (ping, pingTimeout, autoRestartTimeout).
125
125
  */
126
- async allTimerClear() {
126
+ allTimerClear() {
127
127
  clearTimeout(pingTimeout);
128
128
  clearTimeout(ping);
129
129
  clearTimeout(autoRestartTimeout);
package/main.js CHANGED
@@ -65,8 +65,8 @@ class zwavews extends core.Adapter {
65
65
  // MQTT
66
66
  if (["exmqtt", "intmqtt"].includes(this.config.connectionType)) {
67
67
  // External MQTT-Server
68
- if (this.config.connectionType == "exmqtt") {
69
- if (this.config.externalMqttServerIP == "") {
68
+ if (this.config.connectionType === "exmqtt") {
69
+ if (this.config.externalMqttServerIP === "") {
70
70
  this.log.warn(
71
71
  "Please configure the External MQTT-Server connection!",
72
72
  );
@@ -85,7 +85,7 @@ class zwavews extends core.Adapter {
85
85
  };
86
86
 
87
87
  // Set external mqtt credentials
88
- if (this.config.externalMqttServerCredentials == true) {
88
+ if (this.config.externalMqttServerCredentials === true) {
89
89
  mqttClientOptions.username = this.config.externalMqttServerUsername;
90
90
  mqttClientOptions.password = this.config.externalMqttServerPassword;
91
91
  }
@@ -112,25 +112,39 @@ class zwavews extends core.Adapter {
112
112
 
113
113
  // MQTT Client
114
114
  mqttClient.on("connect", () => {
115
- this.log.info(`Connect to zwavews over ${this.config.connectionType == "exmqtt" ? "external mqtt" : "internal mqtt"} connection.`);
115
+ this.log.info(`Connect to zwavews over ${this.config.connectionType === "exmqtt" ? "external mqtt" : "internal mqtt"} connection.`);
116
116
  this.setStateChanged("info.connection", true, true);
117
117
  });
118
118
 
119
- mqttClient.subscribe(`${this.config.baseTopic}/#`);
119
+ mqttClient.subscribe(`${this.config.baseTopic}/#`, (err) => {
120
+ if (err) {
121
+ this.log.error(`<zwavews> MQTT subscribe error: ${err.message}`);
122
+ }
123
+ });
120
124
 
121
125
  mqttClient.on("message", (topic, payload) => {
122
- const newMessage = `{"payload":${payload.toString() == "" ? '"null"' : payload.toString()},"topic":"${topic.slice(topic.search("/") + 1)}"}`;
126
+ const rawPayload = payload.toString();
127
+ let parsedPayload;
128
+ try {
129
+ parsedPayload = rawPayload === "" ? null : JSON.parse(rawPayload);
130
+ } catch {
131
+ parsedPayload = rawPayload;
132
+ }
133
+ const newMessage = JSON.stringify({
134
+ payload: parsedPayload,
135
+ topic: topic.slice(topic.indexOf("/") + 1),
136
+ });
123
137
  this.messageParse(newMessage);
124
138
  });
125
- } else if (this.config.connectionType == 'ws') {
139
+ } else if (this.config.connectionType === 'ws') {
126
140
  // Websocket
127
- if (this.config.wsServerIP == '') {
141
+ if (this.config.wsServerIP === '') {
128
142
  this.log.warn('Please configure the Websoket connection!');
129
143
  return;
130
144
  }
131
145
 
132
146
  // Dummy MQTT-Server
133
- if (this.config.dummyMqtt == true) {
147
+ if (this.config.dummyMqtt === true) {
134
148
  mqttServerController = new MqttServerController(this);
135
149
  await mqttServerController.createDummyMQTTServer();
136
150
  this.setStateChanged("info.connection", true, true);
@@ -145,27 +159,30 @@ class zwavews extends core.Adapter {
145
159
  websocketController = new WebsocketController(this);
146
160
  const wsClient = websocketController.initWsClient();
147
161
 
148
- if (wsClient) {
149
- wsClient.on('open', () => {
150
- this.log.info('Connect to zwave-js-ui over websocket connection.');
151
- startListening = true;
152
- websocketController.send(JSON.stringify({command: "start_listening"}));
153
- });
154
-
155
- wsClient.on('message', (message) => {
156
- this.messageParse(message);
157
- });
158
-
159
- wsClient.on('close', async () => {
160
- this.setStateChanged('info.connection', false, true);
161
- await statesController.setAllAvailableToFalse();
162
- startListening = false;
163
- allNodesCreated = false;
164
- deviceCache = [];
165
- this.nodeCache = [];
166
- this.log.info('Websocket connection closed. Attempting to reconnect...');
167
- });
162
+ if (!wsClient) {
163
+ this.log.error('<zwavews> initWsClient returned null — websocket not started.');
164
+ return;
168
165
  }
166
+
167
+ wsClient.on('open', () => {
168
+ this.log.info('Connect to zwave-js-ui over websocket connection.');
169
+ startListening = true;
170
+ websocketController.send(JSON.stringify({command: "start_listening"}));
171
+ });
172
+
173
+ wsClient.on('message', (message) => {
174
+ this.messageParse(message);
175
+ });
176
+
177
+ wsClient.on('close', async () => {
178
+ this.setStateChanged('info.connection', false, true);
179
+ await statesController.setAllAvailableToFalse();
180
+ startListening = false;
181
+ allNodesCreated = false;
182
+ deviceCache = {};
183
+ this.nodeCache = {};
184
+ this.log.info('Websocket connection closed. Attempting to reconnect...');
185
+ });
169
186
  }
170
187
 
171
188
  async messageParse(message) {
@@ -203,14 +220,19 @@ class zwavews extends core.Adapter {
203
220
  break;
204
221
  }
205
222
 
206
- driver = messageObj.result.state.driver;
223
+ if (!messageObj.result?.state || !Array.isArray(messageObj.result.state.nodes)) {
224
+ this.log.warn('<zwavews> Invalid result.state structure received, skipping.');
225
+ break;
226
+ }
227
+
228
+ driver = messageObj.result.state.driver;
207
229
  controller = messageObj.result.state.controller;
208
- allNodes = messageObj.result.state.nodes;
230
+ allNodes = messageObj.result.state.nodes;
209
231
 
210
232
  for (const nodeData of allNodes) {
211
233
  const nodeId = utils.formatNodeId(nodeData.nodeId);
212
234
 
213
- if (debugDevicesState && debugDevicesState.val.includes(nodeId)) {
235
+ if (debugDevicesState && debugDevicesState.val && String(debugDevicesState.val).includes(nodeId)) {
214
236
  this.log.warn(`--->>> fromZ2W_RAW2-> ${JSON.stringify(nodeData)}` );
215
237
  }
216
238
 
@@ -244,7 +266,7 @@ class zwavews extends core.Adapter {
244
266
  const nodeArg = eventTyp.args;
245
267
  const nodeId = utils.formatNodeId(eventTyp.nodeId);
246
268
 
247
- if (debugDevicesState && debugDevicesState.val.includes(nodeId)) {
269
+ if (debugDevicesState && debugDevicesState.val && String(debugDevicesState.val).includes(nodeId)) {
248
270
  this.log.warn(`--->>> fromZ2W_RAW2-> ${JSON.stringify(eventTyp)}` );
249
271
  }
250
272
 
@@ -273,7 +295,7 @@ class zwavews extends core.Adapter {
273
295
  parsePath = `${nodeId}.info.${nodeArg.property}`;
274
296
  break;
275
297
  case 'location':
276
-
298
+ // intentionally ignored
277
299
  break;
278
300
  default:
279
301
  parsePath = `${nodeId}.info.${nodeArg.property}`;
@@ -383,40 +405,49 @@ class zwavews extends core.Adapter {
383
405
  }
384
406
 
385
407
  async onUnload(callback) {
386
- // Close MQTT connections
387
- if (["exmqtt", "intmqtt"].includes(this.config.connectionType)) {
388
- if (mqttClient && !mqttClient.closed) {
408
+ try {
409
+ // Close MQTT connections
410
+ if (["exmqtt", "intmqtt"].includes(this.config.connectionType)) {
411
+ if (mqttClient && !mqttClient.closed) {
412
+ try {
413
+ mqttClient.end();
414
+ } catch (e) {
415
+ this.log.error(e);
416
+ }
417
+ }
418
+ }
419
+ // Internal or Dummy MQTT-Server
420
+ if (this.config.connectionType === "intmqtt" || this.config.dummyMqtt === true) {
389
421
  try {
390
- if (mqttClient) {
391
- mqttClient.end();
422
+ if (mqttServerController) {
423
+ mqttServerController.closeServer();
392
424
  }
393
425
  } catch (e) {
394
426
  this.log.error(e);
395
427
  }
396
428
  }
397
- }
398
- // Internal or Dummy MQTT-Server
399
- if (this.config.connectionType == "intmqtt" || this.config.dummyMqtt == true) {
429
+ // WebSocket cleanup
430
+ if (websocketController) {
431
+ try {
432
+ await websocketController.allTimerClear();
433
+ websocketController.closeConnection();
434
+ } catch (e) {
435
+ this.log.error(e);
436
+ }
437
+ }
438
+ // Set all device available states to false
400
439
  try {
401
- if (mqttServerController) {
402
- mqttServerController.closeServer();
440
+ if (statesController) {
441
+ await statesController.setAllAvailableToFalse();
403
442
  }
404
443
  } catch (e) {
405
444
  this.log.error(e);
406
445
  }
407
- }
408
- // Set all device available states of false
409
- try {
410
- if (statesController) {
411
- await statesController.setAllAvailableToFalse();
412
- }
413
- } catch (e) {
414
- this.log.error(e);
415
- }
416
-
417
- this.setStateChanged("info.connection", false, true);
418
446
 
419
- callback();
447
+ this.setStateChanged("info.connection", false, true);
448
+ } finally {
449
+ callback();
450
+ }
420
451
  }
421
452
 
422
453
  async onStateChange(id, state) {
@@ -424,21 +455,24 @@ class zwavews extends core.Adapter {
424
455
  return;
425
456
  }
426
457
 
427
- if (state && state.ack == false) {
458
+ if (state && state.ack === false) {
428
459
  if (id.endsWith("info.debugId")) {
429
460
  this.setStateChanged(id, state.val, true);
430
461
  return;
431
462
  }
432
463
 
433
- let message;
434
464
  const obj = await this.getObjectAsync(id);
435
465
  if (obj) {
436
- const nativeObj= obj.native || {};
466
+ const nativeObj = obj.native || {};
437
467
 
438
468
  const m = id.match(/nodeID_0*(\d+)/i);
439
- const nodeId = m ? Number(m[1]) : null;
469
+ if (!m) {
470
+ this.log.warn(`<zwavews> Could not extract nodeId from state id: ${id}`);
471
+ return;
472
+ }
473
+ const nodeId = Number(m[1]);
440
474
 
441
- message = {
475
+ const message = {
442
476
  messageId: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
443
477
  command: "node.set_value",
444
478
  nodeId: nodeId,
@@ -448,13 +482,17 @@ class zwavews extends core.Adapter {
448
482
 
449
483
  const sendMessageAllowed = await this.getStateAsync("info.sendMessageAllowed");
450
484
 
451
- if (sendMessageAllowed.val) {
452
- websocketController.send(JSON.stringify(message));
485
+ if (sendMessageAllowed && sendMessageAllowed.val === true) {
486
+ if (websocketController) {
487
+ websocketController.send(JSON.stringify(message));
488
+ } else {
489
+ this.log.warn('<zwavews> websocketController not initialised, cannot send message.');
490
+ }
453
491
  }
454
492
 
455
- this.setStateChanged('info.debugmessages', JSON.stringify(message), true);
456
- this.log.debug(`<zwavews> message onStateChange ${message}`);
457
- }
493
+ this.setStateChanged('info.debugmessages', JSON.stringify(message), true);
494
+ this.log.debug(`<zwavews> message onStateChange ${JSON.stringify(message)}`);
495
+ }
458
496
  }
459
497
  }
460
498
  }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "iobroker.zwavews",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "zwavews adapter for ioBroker",
5
5
  "author": {
6
- "name": "Dennis Rathjen and Arthur Rupp",
6
+ "name": "Arthur Rupp",
7
7
  "email": "arteck@outlook.com"
8
8
  },
9
9
  "homepage": "https://github.com/arteck/ioBroker.zwavews",
@@ -26,28 +26,28 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@iobroker/adapter-core": "^3.3.2",
29
- "@iobroker/dm-utils": "^3.0.0",
29
+ "@iobroker/dm-utils": "^3.0.3",
30
30
  "humanize-duration": "^3.33.2",
31
31
  "aedes": "^0.51.3",
32
32
  "aedes-persistence-nedb": "^2.0.3",
33
- "mqtt": "^5.15.0",
33
+ "mqtt": "^5.15.1",
34
34
  "net": "^1.0.2",
35
35
  "node-schedule": "^2.1.1",
36
36
  "sharp": "^0.34.5",
37
- "ws": "^8.19.0"
37
+ "ws": "^8.20.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@alcalzone/release-script": "^5.1.1",
41
41
  "@alcalzone/release-script-plugin-iobroker": "^5.1.2",
42
42
  "@alcalzone/release-script-plugin-license": "^5.1.1",
43
- "@alcalzone/release-script-plugin-manual-review": "^4.0.0",
43
+ "@alcalzone/release-script-plugin-manual-review": "^5.1.1",
44
44
  "@iobroker/adapter-dev": "^1.5.0",
45
45
  "@iobroker/testing": "^5.2.2",
46
46
  "@iobroker/eslint-config": "^2.2.0",
47
47
  "@tsconfig/node14": "^14.1.8",
48
48
  "@types/node": "^25.5.0",
49
49
  "@types/node-schedule": "^2.1.8",
50
- "typescript": "~5.9.2"
50
+ "typescript": "~6.0.2"
51
51
  },
52
52
  "main": "main.js",
53
53
  "files": [