iobroker.zwavews 0.1.1 → 0.1.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 +8 -0
- package/io-package.json +28 -28
- package/lib/devicemgmt.js +18 -11
- package/lib/helper.js +78 -54
- package/lib/mqttServerController.js +1 -1
- package/lib/statesController.js +13 -9
- package/lib/utils.js +45 -19
- package/lib/websocketController.js +12 -8
- package/main.js +10 -15
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -35,6 +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.3 (2026-04-03)
|
|
39
|
+
* (arteck) fix unknown state from scene
|
|
40
|
+
* (arteck) del last dot from DP
|
|
41
|
+
* (arteck) fix scene
|
|
42
|
+
|
|
43
|
+
### 0.1.2 (2026-03-15)
|
|
44
|
+
* (arteck) typo
|
|
45
|
+
|
|
38
46
|
### 0.1.1 (2026-03-15)
|
|
39
47
|
* (arteck) add debug information
|
|
40
48
|
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "zwavews",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.1.3": {
|
|
7
|
+
"en": "fix unknown state from scene\ndel last dot from DP\nfix scene",
|
|
8
|
+
"de": "unbekannter zustand von szene\nder letzte Punkt von DP\nfixe szene",
|
|
9
|
+
"ru": "неизвестное состояние с места происшествия\nпоследняя точка от DP\nисправить",
|
|
10
|
+
"pt": "corrigir o estado desconhecido da cena\ndo último ponto do DP\ncorrigir cena",
|
|
11
|
+
"nl": "fix onbekende toestand vanaf locatie\nde laatste stip van DP\nscène herstellen",
|
|
12
|
+
"fr": "réparer l'état inconnu de la scène\ndel dernier point de DP\nréparer la scène",
|
|
13
|
+
"it": "fissare stato sconosciuto dalla scena\ndel ultimo punto da DP\ncorrere la scena",
|
|
14
|
+
"es": "arreglar estado desconocido de la escena\ndel último punto de DP\nescena arreglada",
|
|
15
|
+
"pl": "naprawić nieznany stan z miejsca zbrodni\ndel ostatnia kropka z DP\nmiejsce",
|
|
16
|
+
"uk": "виправити невідомого стану з сцени\ndel last dot від DP\nфіксувати сцена",
|
|
17
|
+
"zh-cn": "从现场修复未知状态\ndP 的最后一个点\n修补场景"
|
|
18
|
+
},
|
|
19
|
+
"0.1.2": {
|
|
20
|
+
"en": "typo",
|
|
21
|
+
"de": "typo",
|
|
22
|
+
"ru": "опечатка",
|
|
23
|
+
"pt": "erro de digitação",
|
|
24
|
+
"nl": "type",
|
|
25
|
+
"fr": "typo",
|
|
26
|
+
"it": "tipo",
|
|
27
|
+
"es": "typo",
|
|
28
|
+
"pl": "typo",
|
|
29
|
+
"uk": "типи",
|
|
30
|
+
"zh-cn": "类型"
|
|
31
|
+
},
|
|
6
32
|
"0.1.1": {
|
|
7
33
|
"en": "add debug information",
|
|
8
34
|
"de": "debug information",
|
|
@@ -67,32 +93,6 @@
|
|
|
67
93
|
"pl": "naprawić komunikat ostrzegawczy",
|
|
68
94
|
"uk": "фіксувати повідомлення про попередження",
|
|
69
95
|
"zh-cn": "修补警告消息"
|
|
70
|
-
},
|
|
71
|
-
"0.0.15": {
|
|
72
|
-
"en": "typo\nfix ready status if status is dead",
|
|
73
|
-
"de": "typo\nbereitstellen des status, wenn der status tot ist",
|
|
74
|
-
"ru": "опечатка\nготовый статус, если статус мертв",
|
|
75
|
-
"pt": "erro de digitação\ncorrigir o estado pronto se o estado estiver morto",
|
|
76
|
-
"nl": "type\nfix ready status als status dood is",
|
|
77
|
-
"fr": "typo\nfixer le statut prêt si le statut est mort",
|
|
78
|
-
"it": "tipo\nfissare lo stato pronto se lo stato è morto",
|
|
79
|
-
"es": "typo\nfijar estado listo si el estado está muerto",
|
|
80
|
-
"pl": "typo\nustaw stan gotowy, jeśli stan jest martwy",
|
|
81
|
-
"uk": "типи\nвиправити готовий статус, якщо статус мертвий",
|
|
82
|
-
"zh-cn": "类型\n如果状态已死亡, 则固定状态"
|
|
83
|
-
},
|
|
84
|
-
"0.0.14": {
|
|
85
|
-
"en": "add event ready",
|
|
86
|
-
"de": "event bereit hinzufügen",
|
|
87
|
-
"ru": "добавить событие готово",
|
|
88
|
-
"pt": "adicionar o evento pronto",
|
|
89
|
-
"nl": "evenement klaar toevoegen",
|
|
90
|
-
"fr": "ajouter l'événement prêt",
|
|
91
|
-
"it": "aggiungere evento pronto",
|
|
92
|
-
"es": "agregar evento listo",
|
|
93
|
-
"pl": "dodaj zdarzenie gotowe",
|
|
94
|
-
"uk": "додати захід готовий",
|
|
95
|
-
"zh-cn": "添加已准备的事件"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"titleLang": {
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
"plugins": {
|
|
187
187
|
"docker": {
|
|
188
188
|
"iobDockerComposeFiles": [
|
|
189
|
-
"
|
|
189
|
+
"docker-compose.yaml"
|
|
190
190
|
]
|
|
191
191
|
}
|
|
192
192
|
},
|
package/lib/devicemgmt.js
CHANGED
|
@@ -3,12 +3,13 @@ const dmUtils = require('@iobroker/dm-utils');
|
|
|
3
3
|
const humanizeDuration = require('humanize-duration');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Device management class for the ZWave adapter.
|
|
7
7
|
*/
|
|
8
8
|
class dmZwave extends dmUtils.DeviceManagement {
|
|
9
9
|
/**
|
|
10
|
+
* Creates a new dmZwave instance.
|
|
10
11
|
*
|
|
11
|
-
* @param adapter
|
|
12
|
+
* @param {object} adapter - The ioBroker adapter instance.
|
|
12
13
|
*/
|
|
13
14
|
constructor(adapter) {
|
|
14
15
|
super(adapter);
|
|
@@ -91,9 +92,10 @@ class dmZwave extends dmUtils.DeviceManagement {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
/**
|
|
95
|
+
* Opens the device documentation PDF or link in a form dialog.
|
|
94
96
|
*
|
|
95
|
-
* @param context
|
|
96
|
-
* @param device
|
|
97
|
+
* @param {object} context - The device management context used to show the form.
|
|
98
|
+
* @param {object} device - The ZWave device object containing device config and metadata.
|
|
97
99
|
*/
|
|
98
100
|
async openPDF(context, device) {
|
|
99
101
|
const manual = device?.deviceConfig?.metadata?.manual;
|
|
@@ -140,10 +142,11 @@ class dmZwave extends dmUtils.DeviceManagement {
|
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
/**
|
|
145
|
+
* Returns the detail schema and data for a specific device.
|
|
143
146
|
*
|
|
144
|
-
* @param id
|
|
145
|
-
* @param action
|
|
146
|
-
* @param context
|
|
147
|
+
* @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.
|
|
147
150
|
*/
|
|
148
151
|
async getDeviceDetails(id, action, context) {
|
|
149
152
|
this.adapter.log.debug('getDeviceDetails');
|
|
@@ -362,9 +365,10 @@ class dmZwave extends dmUtils.DeviceManagement {
|
|
|
362
365
|
|
|
363
366
|
|
|
364
367
|
/**
|
|
368
|
+
* Formats a timestamp according to the given format type.
|
|
365
369
|
*
|
|
366
|
-
* @param time
|
|
367
|
-
* @param type
|
|
370
|
+
* @param {number} time - The timestamp in milliseconds (epoch).
|
|
371
|
+
* @param {'ISO_8601'|'ISO_8601_local'|'epoch'|'relative'} type - The desired output format.
|
|
368
372
|
*/
|
|
369
373
|
async formatDate(time, type) { //'ISO_8601' | 'ISO_8601_local' | 'epoch' | 'relative'
|
|
370
374
|
if (type === 'ISO_8601') {
|
|
@@ -381,8 +385,9 @@ return time;
|
|
|
381
385
|
}
|
|
382
386
|
|
|
383
387
|
/**
|
|
388
|
+
* Converts a Date object to a local ISO 8601 string.
|
|
384
389
|
*
|
|
385
|
-
* @param d
|
|
390
|
+
* @param {Date} d - The Date object to convert.
|
|
386
391
|
*/
|
|
387
392
|
toLocalISOString(d) {
|
|
388
393
|
const off = d.getTimezoneOffset();
|
|
@@ -391,8 +396,10 @@ return time;
|
|
|
391
396
|
// Entfernt den ioBroker-Prefix am Anfang, z.B.
|
|
392
397
|
// "zwavews.0.nodeID_1.info.name" -> "nodeID_1.info.name"
|
|
393
398
|
/**
|
|
399
|
+
* Strips the ioBroker adapter prefix from an object ID.
|
|
394
400
|
*
|
|
395
|
-
* @param id
|
|
401
|
+
* @param {string} id - The full ioBroker object ID (e.g. "zwavews.0.nodeID_1.info.name").
|
|
402
|
+
* @returns {string} The ID without the adapter prefix (e.g. "nodeID_1.info.name").
|
|
396
403
|
*/
|
|
397
404
|
stripIobPrefix(id) {
|
|
398
405
|
const s = String(id ?? '');
|
package/lib/helper.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const utils = require("./utils");
|
|
2
2
|
const constant = require("./constants");
|
|
3
|
-
const {isObject} = require("./utils");
|
|
4
3
|
|
|
5
4
|
/*
|
|
6
5
|
options:
|
|
@@ -12,13 +11,14 @@ autoCast (true false) // make JSON.parse to parse numbers correctly
|
|
|
12
11
|
descriptions: Object of names for state keys
|
|
13
12
|
*/
|
|
14
13
|
/**
|
|
15
|
-
*
|
|
14
|
+
* Helper class for creating and managing ioBroker objects and states from ZWave data.
|
|
16
15
|
*/
|
|
17
16
|
class Helper {
|
|
18
17
|
/**
|
|
18
|
+
* Creates a new Helper instance.
|
|
19
19
|
*
|
|
20
|
-
* @param adapter
|
|
21
|
-
* @param alreadyCreatedObjects
|
|
20
|
+
* @param {object} adapter - The ioBroker adapter instance.
|
|
21
|
+
* @param {object} [alreadyCreatedObjects] - Cache of already created object paths.
|
|
22
22
|
*/
|
|
23
23
|
constructor(adapter, alreadyCreatedObjects = {}) {
|
|
24
24
|
this.adapter = adapter;
|
|
@@ -27,17 +27,10 @@ class Helper {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
+
* Creates a ZWave node device and all its value states in ioBroker.
|
|
30
31
|
*
|
|
31
|
-
* @param
|
|
32
|
-
* @param element
|
|
33
|
-
* @param options
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
*
|
|
39
|
-
* @param nodeIdOriginal
|
|
40
|
-
* @param element
|
|
32
|
+
* @param {string|number} nodeIdOriginal - The original node ID as received from the ZWave driver.
|
|
33
|
+
* @param {object} element - The node data object containing values, name and device config.
|
|
41
34
|
*/
|
|
42
35
|
async createNode(nodeIdOriginal, element) {
|
|
43
36
|
try {
|
|
@@ -68,13 +61,10 @@ class Helper {
|
|
|
68
61
|
|
|
69
62
|
if (valuesOnly != null && typeof valuesOnly === "object" && valuesOnly.length > 0) {
|
|
70
63
|
for (const v of valuesOnly) {
|
|
71
|
-
let parsePath = utils.formatObject(`${nodeId}.${v.commandClassName}`);
|
|
64
|
+
let parsePath = utils.deleteLastDot(utils.formatObject(`${nodeId}.${v.commandClassName}`));
|
|
72
65
|
let metadata = v.metadata || {};
|
|
73
66
|
|
|
74
|
-
if (constant.noInfoDP.includes(v.commandClassName)) {
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (constant.noInfoDP.includes(v.propertyName)) {
|
|
67
|
+
if (constant.noInfoDP.includes(v.commandClassName) || constant.noInfoDP.includes(v.propertyName)) {
|
|
78
68
|
continue;
|
|
79
69
|
}
|
|
80
70
|
|
|
@@ -100,10 +90,10 @@ class Helper {
|
|
|
100
90
|
.replace(/[^\p{L}\p{N}\s]/gu, "")
|
|
101
91
|
.replace(/\s+/g, " ")
|
|
102
92
|
.trim()}`;
|
|
93
|
+
}
|
|
103
94
|
|
|
104
|
-
|
|
95
|
+
if (constant.RGB.includes(v.propertyKeyName)) {
|
|
105
96
|
parsePath = utils.replaceLastDot(parsePath);
|
|
106
|
-
}
|
|
107
97
|
}
|
|
108
98
|
|
|
109
99
|
if (this.isObject(v.value)) { // da gibts ein object mit value
|
|
@@ -115,7 +105,7 @@ class Helper {
|
|
|
115
105
|
parsePath = `${parsePath}_${v.endpoint}`;
|
|
116
106
|
}
|
|
117
107
|
|
|
118
|
-
parsePath = utils.formatObject(parsePath); // entferne sonderzeichen und
|
|
108
|
+
parsePath = utils.deleteLastDot(utils.formatObject(parsePath)); // entferne sonderzeichen und blank aus dem namen und letzten dot
|
|
119
109
|
|
|
120
110
|
const nam_id = v.label ?? v.propertyName;
|
|
121
111
|
|
|
@@ -127,7 +117,7 @@ class Helper {
|
|
|
127
117
|
if (constant.mixedType.includes(nam_id)) {
|
|
128
118
|
typeDp = "mixed";
|
|
129
119
|
}
|
|
130
|
-
|
|
120
|
+
|
|
131
121
|
const common = {
|
|
132
122
|
id: nam_id,
|
|
133
123
|
name: nam_id,
|
|
@@ -166,7 +156,7 @@ class Helper {
|
|
|
166
156
|
this.adapter.subscribeStates(parsePath);
|
|
167
157
|
}
|
|
168
158
|
|
|
169
|
-
this.
|
|
159
|
+
await this.changeState(parsePath, valDp);
|
|
170
160
|
|
|
171
161
|
this.alreadyCreatedObjects[parsePath] = {};
|
|
172
162
|
}
|
|
@@ -177,16 +167,18 @@ class Helper {
|
|
|
177
167
|
}
|
|
178
168
|
|
|
179
169
|
/**
|
|
170
|
+
* Recursively parses an element and creates the corresponding ioBroker objects and states.
|
|
180
171
|
*
|
|
181
|
-
* @param path
|
|
182
|
-
* @param element
|
|
183
|
-
* @param options
|
|
172
|
+
* @param {string} path - The ioBroker object path to write to.
|
|
173
|
+
* @param {*} element - The value or object to parse and persist.
|
|
174
|
+
* @param {object} [options] - Parsing options (e.g. write, channelName, descriptions).
|
|
175
|
+
* @param {boolean} [change] - If true, forces setState instead of setStateChanged.
|
|
184
176
|
*/
|
|
185
|
-
async parse(path, element, options = { write: false }) {
|
|
186
|
-
let parsePath = utils.formatObject(path);
|
|
177
|
+
async parse(path, element, options = { write: false },change = false) {
|
|
178
|
+
let parsePath = utils.deleteLastDot(utils.formatObject(path));
|
|
187
179
|
|
|
188
|
-
if (element
|
|
189
|
-
this.adapter.log.
|
|
180
|
+
if (element === undefined) {
|
|
181
|
+
this.adapter.log.error(`Skip undefined value for ${parsePath}`);
|
|
190
182
|
return;
|
|
191
183
|
}
|
|
192
184
|
|
|
@@ -220,13 +212,16 @@ class Helper {
|
|
|
220
212
|
|
|
221
213
|
this.alreadyCreatedObjects[parsePath] = {};
|
|
222
214
|
} catch (error) {
|
|
215
|
+
this.adapter.log.error(`parse error ${ parsePath}`);
|
|
223
216
|
this.adapter.log.error(error);
|
|
224
217
|
}
|
|
225
218
|
}
|
|
226
219
|
|
|
227
|
-
this.
|
|
220
|
+
await this.changeState(parsePath, valDp, change);
|
|
221
|
+
|
|
228
222
|
return;
|
|
229
223
|
}
|
|
224
|
+
|
|
230
225
|
options.channelName = utils.getLastSegment(parsePath);
|
|
231
226
|
|
|
232
227
|
if (!this.alreadyCreatedObjects[parsePath]) {
|
|
@@ -242,6 +237,7 @@ class Helper {
|
|
|
242
237
|
this.alreadyCreatedObjects[parsePath] = { };
|
|
243
238
|
delete options.channelName;
|
|
244
239
|
} catch (error) {
|
|
240
|
+
this.adapter.log.error(`parse error ${ parsePath}`);
|
|
245
241
|
this.adapter.log.error(error);
|
|
246
242
|
}
|
|
247
243
|
}
|
|
@@ -310,6 +306,8 @@ class Helper {
|
|
|
310
306
|
typeDp = "mixed";
|
|
311
307
|
}
|
|
312
308
|
|
|
309
|
+
fullPath = utils.deleteLastDot(fullPath);
|
|
310
|
+
|
|
313
311
|
const common = {
|
|
314
312
|
id: objectName,
|
|
315
313
|
name: objectName,
|
|
@@ -333,14 +331,15 @@ class Helper {
|
|
|
333
331
|
}
|
|
334
332
|
|
|
335
333
|
try {
|
|
336
|
-
if (valDP !== undefined) {
|
|
337
|
-
this.adapter.setStateChanged(fullPath, valDP, true);
|
|
338
334
|
|
|
335
|
+
await this.changeState(fullPath, valDP, change);
|
|
336
|
+
|
|
337
|
+
if (valDP !== undefined) {
|
|
339
338
|
if (fullPath.endsWith('ready') ) {
|
|
340
339
|
valDP = element['status'];
|
|
341
|
-
if (utils.isNumeric(valDP) && valDP
|
|
340
|
+
if (utils.isNumeric(valDP) && valDP === 3) {
|
|
342
341
|
fullPath = fullPath.replace(".status", ".ready");
|
|
343
|
-
this.
|
|
342
|
+
await this.changeState(fullPath, false);
|
|
344
343
|
}
|
|
345
344
|
}
|
|
346
345
|
}
|
|
@@ -353,19 +352,22 @@ class Helper {
|
|
|
353
352
|
|
|
354
353
|
|
|
355
354
|
/**
|
|
355
|
+
* Checks whether a value is a non-null object.
|
|
356
356
|
*
|
|
357
|
-
* @param value
|
|
357
|
+
* @param {*} value - The value to check.
|
|
358
|
+
* @returns {boolean}
|
|
358
359
|
*/
|
|
359
360
|
isObject(value) {
|
|
360
361
|
return value !== null && typeof value === "object";
|
|
361
362
|
}
|
|
362
363
|
|
|
363
364
|
/**
|
|
365
|
+
* Extracts and processes an array from an element, creating ioBroker objects for each entry.
|
|
364
366
|
*
|
|
365
|
-
* @param element
|
|
366
|
-
* @param key
|
|
367
|
-
* @param path
|
|
368
|
-
* @param options
|
|
367
|
+
* @param {object|Array} element - The element containing the array, or the array itself.
|
|
368
|
+
* @param {string} key - The key of the array within the element, or empty string if element is the array.
|
|
369
|
+
* @param {string} path - The ioBroker base path to write to.
|
|
370
|
+
* @param {object} options - Parsing options forwarded to the parse method.
|
|
369
371
|
*/
|
|
370
372
|
async extractArray(element, key, path, options) {
|
|
371
373
|
try {
|
|
@@ -396,13 +398,15 @@ class Helper {
|
|
|
396
398
|
}
|
|
397
399
|
|
|
398
400
|
/**
|
|
401
|
+
* Determines the ioBroker role string for a datapoint based on its value and metadata.
|
|
399
402
|
*
|
|
400
|
-
* @param element
|
|
401
|
-
* @param options
|
|
402
|
-
* @param dpName
|
|
403
|
+
* @param {*} element - The value or metadata object to derive the role from.
|
|
404
|
+
* @param {object|boolean} options - Parsing options or write flag.
|
|
405
|
+
* @param {string} [dpName] - The datapoint name used to detect time-based roles.
|
|
406
|
+
* @returns {string} The ioBroker role string (e.g. "state", "switch", "text").
|
|
403
407
|
*/
|
|
404
408
|
getRole(element, options, dpName) {
|
|
405
|
-
const write = options.write;
|
|
409
|
+
// const write = options.write;
|
|
406
410
|
const hasStates = element && typeof element === "object" && element.states !== undefined;
|
|
407
411
|
|
|
408
412
|
|
|
@@ -412,7 +416,7 @@ class Helper {
|
|
|
412
416
|
}
|
|
413
417
|
|
|
414
418
|
if (hasStates) {
|
|
415
|
-
if (element.type
|
|
419
|
+
if (element.type === "boolean") {
|
|
416
420
|
delete element.states;
|
|
417
421
|
return "button";
|
|
418
422
|
}
|
|
@@ -431,8 +435,10 @@ class Helper {
|
|
|
431
435
|
return "state";
|
|
432
436
|
}
|
|
433
437
|
/**
|
|
438
|
+
* Resolves and normalises the value from a ZWave command class metadata object.
|
|
434
439
|
*
|
|
435
|
-
* @param element
|
|
440
|
+
* @param {object} element - The metadata object containing type, value, min, writeable and readable fields.
|
|
441
|
+
* @returns {*} The resolved and normalised value ready for use as an ioBroker state value.
|
|
436
442
|
*/
|
|
437
443
|
resolveCommandClassValue(element) {
|
|
438
444
|
const type = element.type;
|
|
@@ -494,8 +500,9 @@ class Helper {
|
|
|
494
500
|
|
|
495
501
|
|
|
496
502
|
/**
|
|
503
|
+
* Creates the ready and status state objects directly on the node device.
|
|
497
504
|
*
|
|
498
|
-
* @param nodeId
|
|
505
|
+
* @param {string} nodeId - The formatted node ID used as the ioBroker object path prefix.
|
|
499
506
|
*/
|
|
500
507
|
async createReadyStatus(nodeId) {
|
|
501
508
|
// leg die status direkt auch an
|
|
@@ -529,12 +536,13 @@ class Helper {
|
|
|
529
536
|
native: {},
|
|
530
537
|
});
|
|
531
538
|
}
|
|
532
|
-
/**
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
539
|
+
/**
|
|
540
|
+
* Updates the name or description of an existing ioBroker device object.
|
|
541
|
+
*
|
|
542
|
+
* @param {string} nodeId - The ioBroker object ID of the device to update.
|
|
543
|
+
* @param {object} element - The element containing the new name, productLabel, manufacturer or desc.
|
|
544
|
+
* @param {boolean} [nameChange] - If true, updates the common name; otherwise updates the description.
|
|
545
|
+
*/
|
|
538
546
|
async updateDevice(nodeId, element, nameChange = true) {
|
|
539
547
|
const obj = await this.adapter.getObjectAsync(nodeId);
|
|
540
548
|
if (obj) {
|
|
@@ -553,6 +561,22 @@ class Helper {
|
|
|
553
561
|
}
|
|
554
562
|
}
|
|
555
563
|
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Sets or conditionally updates an ioBroker state value.
|
|
567
|
+
*
|
|
568
|
+
* @param {string} path - The ioBroker state ID to set.
|
|
569
|
+
* @param {*} value - The value to write to the state.
|
|
570
|
+
* @param {boolean} [change] - If true, uses setState (unconditional); otherwise uses setStateChanged.
|
|
571
|
+
*/
|
|
572
|
+
async changeState(path, value, change = false) {
|
|
573
|
+
if (change) {
|
|
574
|
+
this.adapter.setState(path, value, true);
|
|
575
|
+
} else {
|
|
576
|
+
this.adapter.setStateChanged(path, value, true);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
556
580
|
}
|
|
557
581
|
|
|
558
582
|
module.exports = {
|
package/lib/statesController.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Controls reading and writing of ioBroker states for the ZWave adapter.
|
|
3
3
|
*/
|
|
4
4
|
class StatesController {
|
|
5
5
|
/**
|
|
6
|
+
* Creates a new StatesController instance.
|
|
6
7
|
*
|
|
7
|
-
* @param adapter
|
|
8
|
-
* @param deviceCache
|
|
8
|
+
* @param {object} adapter - The ioBroker adapter instance.
|
|
9
9
|
*/
|
|
10
10
|
constructor(adapter) {
|
|
11
11
|
this.adapter = adapter;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
+
* Sets a state value unconditionally, skipping null/undefined values.
|
|
15
16
|
*
|
|
16
|
-
* @param stateName
|
|
17
|
-
* @param value
|
|
17
|
+
* @param {string} stateName - The ioBroker state ID to set.
|
|
18
|
+
* @param {*} value - The value to write to the state.
|
|
18
19
|
*/
|
|
19
20
|
async setStateSafelyAsync(stateName, value) {
|
|
20
21
|
if (value === undefined || value === null) {
|
|
@@ -24,9 +25,10 @@ class StatesController {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/**
|
|
28
|
+
* Sets a state value only if it has changed, skipping null/undefined values.
|
|
27
29
|
*
|
|
28
|
-
* @param stateName
|
|
29
|
-
* @param value
|
|
30
|
+
* @param {string} stateName - The ioBroker state ID to set.
|
|
31
|
+
* @param {*} value - The value to write to the state.
|
|
30
32
|
*/
|
|
31
33
|
async setStateChangedSafelyAsync(stateName, value) {
|
|
32
34
|
if (value === undefined || value === null) {
|
|
@@ -36,8 +38,9 @@ class StatesController {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
/**
|
|
41
|
+
* Reads all writable ZWave states from ioBroker and returns them as a map keyed by object ID.
|
|
39
42
|
*
|
|
40
|
-
* @
|
|
43
|
+
* @returns {Promise<object>} A map of writable state IDs to their MQTT path and write flag.
|
|
41
44
|
*/
|
|
42
45
|
async subscribeAllWritableExistsStates() {
|
|
43
46
|
const writableStates = {};
|
|
@@ -62,7 +65,8 @@ class StatesController {
|
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
/**
|
|
65
|
-
*
|
|
68
|
+
* Sets all node ready-states to false, all status-states to "unknown"
|
|
69
|
+
* and the gateway status to "offline".
|
|
66
70
|
*/
|
|
67
71
|
async setAllAvailableToFalse() {
|
|
68
72
|
const readyStates = await this.adapter.getStatesAsync("*.ready");
|
package/lib/utils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Converts a byte array to a word array.
|
|
2
3
|
*
|
|
3
|
-
* @param ba
|
|
4
|
+
* @param {number[]} ba - The byte array to convert.
|
|
4
5
|
*/
|
|
5
6
|
function bytesArrayToWordArray(ba) {
|
|
6
7
|
const wa = [];
|
|
@@ -13,8 +14,9 @@ function bytesArrayToWordArray(ba) {
|
|
|
13
14
|
// If the value is greater than 1000, kelvin is assumed.
|
|
14
15
|
// If smaller, it is assumed to be mired.
|
|
15
16
|
/**
|
|
17
|
+
* Converts a temperature value to mired.
|
|
16
18
|
*
|
|
17
|
-
* @param t
|
|
19
|
+
* @param {number} t - Temperature value in Kelvin or mired.
|
|
18
20
|
*/
|
|
19
21
|
function toMired(t) {
|
|
20
22
|
let miredValue = t;
|
|
@@ -25,8 +27,9 @@ function toMired(t) {
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/**
|
|
30
|
+
* Converts between mired and Kelvin.
|
|
28
31
|
*
|
|
29
|
-
* @param t
|
|
32
|
+
* @param {number} t - Temperature value to convert.
|
|
30
33
|
*/
|
|
31
34
|
function miredKelvinConversion(t) {
|
|
32
35
|
return Math.round(1000000 / t);
|
|
@@ -35,8 +38,8 @@ function miredKelvinConversion(t) {
|
|
|
35
38
|
/**
|
|
36
39
|
* Converts a decimal number to a hex string with zero-padding
|
|
37
40
|
*
|
|
38
|
-
* @param decimal
|
|
39
|
-
* @param padding
|
|
41
|
+
* @param {number} decimal - The decimal number to convert.
|
|
42
|
+
* @param {number} padding - The minimum length of the resulting hex string.
|
|
40
43
|
*/
|
|
41
44
|
function decimalToHex(decimal, padding) {
|
|
42
45
|
let hex = Number(decimal).toString(16);
|
|
@@ -53,8 +56,9 @@ function decimalToHex(decimal, padding) {
|
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
/**
|
|
59
|
+
* Removes all elements from an array in place.
|
|
56
60
|
*
|
|
57
|
-
* @param array
|
|
61
|
+
* @param {any[]} array - The array to clear.
|
|
58
62
|
*/
|
|
59
63
|
function clearArray(array) {
|
|
60
64
|
while (array.length > 0) {
|
|
@@ -63,9 +67,10 @@ function clearArray(array) {
|
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
/**
|
|
70
|
+
* Moves all elements from source array into target array.
|
|
66
71
|
*
|
|
67
|
-
* @param source
|
|
68
|
-
* @param target
|
|
72
|
+
* @param {any[]} source - The source array to move elements from.
|
|
73
|
+
* @param {any[]} target - The target array to move elements into.
|
|
69
74
|
*/
|
|
70
75
|
function moveArray(source, target) {
|
|
71
76
|
while (source.length > 0) {
|
|
@@ -74,16 +79,18 @@ function moveArray(source, target) {
|
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
/**
|
|
82
|
+
* Checks whether a value is a plain object.
|
|
77
83
|
*
|
|
78
|
-
* @param item
|
|
84
|
+
* @param {any} item - The value to check.
|
|
79
85
|
*/
|
|
80
86
|
function isObject(item) {
|
|
81
87
|
return typeof item === "object" && !Array.isArray(item) && item !== null;
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
/**
|
|
91
|
+
* Checks whether a value is valid JSON.
|
|
85
92
|
*
|
|
86
|
-
* @param item
|
|
93
|
+
* @param {any} item - The value to check.
|
|
87
94
|
*/
|
|
88
95
|
function isJson(item) {
|
|
89
96
|
let value = typeof item !== "string" ? JSON.stringify(item) : item;
|
|
@@ -97,8 +104,9 @@ function isJson(item) {
|
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
/**
|
|
107
|
+
* Returns the last segment of a dot- or slash-separated string.
|
|
100
108
|
*
|
|
101
|
-
* @param input
|
|
109
|
+
* @param {string} input - The input string to parse.
|
|
102
110
|
*/
|
|
103
111
|
function getLastSegment(input) {
|
|
104
112
|
if (typeof input !== "string") {
|
|
@@ -109,7 +117,9 @@ function getLastSegment(input) {
|
|
|
109
117
|
}
|
|
110
118
|
|
|
111
119
|
/**
|
|
112
|
-
*
|
|
120
|
+
* Checks whether a value is numeric (finite number or numeric string).
|
|
121
|
+
*
|
|
122
|
+
* @param {any} value - The value to check.
|
|
113
123
|
* @returns {boolean}
|
|
114
124
|
*/
|
|
115
125
|
function isNumeric(value) {
|
|
@@ -130,8 +140,9 @@ function isNumeric(value) {
|
|
|
130
140
|
}
|
|
131
141
|
|
|
132
142
|
/**
|
|
143
|
+
* Replaces the last dot in a string with an underscore.
|
|
133
144
|
*
|
|
134
|
-
* @param str
|
|
145
|
+
* @param {string} str - The string to process.
|
|
135
146
|
*/
|
|
136
147
|
function replaceLastDot(str) {
|
|
137
148
|
const idx = str.lastIndexOf(".");
|
|
@@ -139,8 +150,18 @@ function replaceLastDot(str) {
|
|
|
139
150
|
}
|
|
140
151
|
|
|
141
152
|
/**
|
|
153
|
+
* Removes a trailing dot from a string if present.
|
|
154
|
+
*
|
|
155
|
+
* @param {string|undefined} str - The string to process.
|
|
156
|
+
*/
|
|
157
|
+
function deleteLastDot(str) {
|
|
158
|
+
return str.endsWith(".") ? str.slice(0, -1) : str;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Trims and normalises an object name string.
|
|
142
163
|
*
|
|
143
|
-
* @param str
|
|
164
|
+
* @param {string} str - The string to format.
|
|
144
165
|
*/
|
|
145
166
|
function formatObject(str) {
|
|
146
167
|
if (typeof str !== "string") {
|
|
@@ -150,8 +171,9 @@ function formatObject(str) {
|
|
|
150
171
|
}
|
|
151
172
|
|
|
152
173
|
/**
|
|
174
|
+
* Replaces all dots in an MQTT topic with slashes.
|
|
153
175
|
*
|
|
154
|
-
* @param input
|
|
176
|
+
* @param {string} input - The MQTT topic string to format.
|
|
155
177
|
*/
|
|
156
178
|
function formatMQTT(input) {
|
|
157
179
|
if (typeof input !== "string") {
|
|
@@ -161,17 +183,19 @@ function formatMQTT(input) {
|
|
|
161
183
|
}
|
|
162
184
|
|
|
163
185
|
/**
|
|
186
|
+
* Zero-pads the numeric suffix of a node ID string.
|
|
164
187
|
*
|
|
165
|
-
* @param nodeId
|
|
166
|
-
* @param width
|
|
188
|
+
* @param {string} nodeId - The node ID string to pad.
|
|
189
|
+
* @param {number} [width] - The desired minimum width of the numeric part.
|
|
167
190
|
*/
|
|
168
191
|
function padNodeId(nodeId, width = 3) {
|
|
169
192
|
return nodeId.replace(/(\d+)$/, (m) => m.padStart(width, "0"));
|
|
170
193
|
}
|
|
171
194
|
|
|
172
195
|
/**
|
|
196
|
+
* Returns a human-readable status text for a given node status code.
|
|
173
197
|
*
|
|
174
|
-
* @param status
|
|
198
|
+
* @param {number} status - The numeric status code.
|
|
175
199
|
*/
|
|
176
200
|
function getStatusText(status) {
|
|
177
201
|
const nodeStatus = {
|
|
@@ -186,8 +210,9 @@ function getStatusText(status) {
|
|
|
186
210
|
}
|
|
187
211
|
|
|
188
212
|
/**
|
|
213
|
+
* Formats a node ID, padding numeric IDs with a prefix.
|
|
189
214
|
*
|
|
190
|
-
* @param nodeIdOriginal
|
|
215
|
+
* @param {string|number} nodeIdOriginal - The original node ID to format.
|
|
191
216
|
*/
|
|
192
217
|
function formatNodeId(nodeIdOriginal) {
|
|
193
218
|
let nodeId = nodeIdOriginal;
|
|
@@ -215,4 +240,5 @@ module.exports = {
|
|
|
215
240
|
padNodeId,
|
|
216
241
|
getStatusText,
|
|
217
242
|
formatObject,
|
|
243
|
+
deleteLastDot,
|
|
218
244
|
};
|
|
@@ -7,19 +7,22 @@ let pingTimeout;
|
|
|
7
7
|
let autoRestartTimeout;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Manages the WebSocket connection to the zwave-js-ui server.
|
|
11
11
|
*/
|
|
12
12
|
class WebsocketController {
|
|
13
13
|
/**
|
|
14
|
+
* Creates a new WebsocketController instance.
|
|
14
15
|
*
|
|
15
|
-
* @param adapter
|
|
16
|
+
* @param {object} adapter - The ioBroker adapter instance.
|
|
16
17
|
*/
|
|
17
18
|
constructor(adapter) {
|
|
18
19
|
this.adapter = adapter;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/**
|
|
23
|
+
* Initialises and connects the WebSocket client to the zwave-js-ui server.
|
|
22
24
|
*
|
|
25
|
+
* @returns {WebSocket} The created WebSocket client instance.
|
|
23
26
|
*/
|
|
24
27
|
initWsClient() {
|
|
25
28
|
try {
|
|
@@ -64,8 +67,9 @@ class WebsocketController {
|
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
/**
|
|
70
|
+
* Sends a message to the zwave-js-ui server via the WebSocket connection.
|
|
67
71
|
*
|
|
68
|
-
* @param message
|
|
72
|
+
* @param {string} message - The message payload to send.
|
|
69
73
|
*/
|
|
70
74
|
send(message) {
|
|
71
75
|
if (wsClient.readyState !== WebSocket.OPEN) {
|
|
@@ -76,7 +80,7 @@ class WebsocketController {
|
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
/**
|
|
79
|
-
*
|
|
83
|
+
* Sends a WebSocket ping to the server and schedules the next ping.
|
|
80
84
|
*/
|
|
81
85
|
sendPingToServer() {
|
|
82
86
|
//this.logDebug('Send ping to server');
|
|
@@ -87,7 +91,7 @@ class WebsocketController {
|
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
/**
|
|
90
|
-
*
|
|
94
|
+
* Resets the heartbeat timeout; terminates the connection if no pong is received in time.
|
|
91
95
|
*/
|
|
92
96
|
wsHeartbeat() {
|
|
93
97
|
clearTimeout(pingTimeout);
|
|
@@ -98,7 +102,7 @@ class WebsocketController {
|
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
/**
|
|
101
|
-
*
|
|
105
|
+
* Schedules an automatic reconnect attempt after the configured restart timeout.
|
|
102
106
|
*/
|
|
103
107
|
async autoRestart() {
|
|
104
108
|
this.adapter.log.warn(`Start try again in ${restartTimeout / 1000} seconds...`);
|
|
@@ -108,7 +112,7 @@ class WebsocketController {
|
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
/**
|
|
111
|
-
*
|
|
115
|
+
* Closes the WebSocket connection if it is currently open.
|
|
112
116
|
*/
|
|
113
117
|
closeConnection() {
|
|
114
118
|
if (wsClient && wsClient.readyState !== WebSocket.CLOSED) {
|
|
@@ -117,7 +121,7 @@ class WebsocketController {
|
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
/**
|
|
120
|
-
*
|
|
124
|
+
* Clears all active timers (ping, pingTimeout, autoRestartTimeout).
|
|
121
125
|
*/
|
|
122
126
|
async allTimerClear() {
|
|
123
127
|
clearTimeout(pingTimeout);
|
package/main.js
CHANGED
|
@@ -15,8 +15,6 @@ const MqttServerController = require("./lib/mqttServerController").MqttServerCon
|
|
|
15
15
|
|
|
16
16
|
let mqttClient;
|
|
17
17
|
let deviceCache = {};
|
|
18
|
-
const logCustomizations = { debugDevices: "", logfilter: [] };
|
|
19
|
-
|
|
20
18
|
let websocketController;
|
|
21
19
|
let mqttServerController;
|
|
22
20
|
let statesController;
|
|
@@ -54,11 +52,6 @@ class zwavews extends core.Adapter {
|
|
|
54
52
|
|
|
55
53
|
helper = new Helper(this, deviceCache);
|
|
56
54
|
|
|
57
|
-
const debugDevicesState = await this.getStateAsync("info.debugId");
|
|
58
|
-
if (debugDevicesState && debugDevicesState.val) {
|
|
59
|
-
logCustomizations.debugDevices = String(debugDevicesState.val.toLowerCase());
|
|
60
|
-
}
|
|
61
|
-
|
|
62
55
|
this.deviceManagement = new dmZwave(this);
|
|
63
56
|
|
|
64
57
|
if (this.config.wsOnStart) {
|
|
@@ -167,6 +160,7 @@ class zwavews extends core.Adapter {
|
|
|
167
160
|
this.setStateChanged('info.connection', false, true);
|
|
168
161
|
await statesController.setAllAvailableToFalse();
|
|
169
162
|
startListening = false;
|
|
163
|
+
allNodesCreated = false;
|
|
170
164
|
deviceCache = [];
|
|
171
165
|
this.nodeCache = [];
|
|
172
166
|
this.log.info('Websocket connection closed. Attempting to reconnect...');
|
|
@@ -186,9 +180,6 @@ class zwavews extends core.Adapter {
|
|
|
186
180
|
const messageObj = JSON.parse(message);
|
|
187
181
|
|
|
188
182
|
const debugDevicesState = await this.getStateAsync("info.debugId");
|
|
189
|
-
if (debugDevicesState && debugDevicesState.val) {
|
|
190
|
-
logCustomizations.debugDevices = String(debugDevicesState.val.toLowerCase());
|
|
191
|
-
}
|
|
192
183
|
|
|
193
184
|
this.log.debug(`--->>> fromZ2W_RAW1 -> ${JSON.stringify(messageObj)}`);
|
|
194
185
|
|
|
@@ -219,7 +210,7 @@ class zwavews extends core.Adapter {
|
|
|
219
210
|
for (const nodeData of allNodes) {
|
|
220
211
|
const nodeId = utils.formatNodeId(nodeData.nodeId);
|
|
221
212
|
|
|
222
|
-
if (
|
|
213
|
+
if (debugDevicesState && debugDevicesState.val.includes(nodeId)) {
|
|
223
214
|
this.log.warn(`--->>> fromZ2W_RAW2-> ${JSON.stringify(nodeData)}` );
|
|
224
215
|
}
|
|
225
216
|
|
|
@@ -253,7 +244,7 @@ class zwavews extends core.Adapter {
|
|
|
253
244
|
const nodeArg = eventTyp.args;
|
|
254
245
|
const nodeId = utils.formatNodeId(eventTyp.nodeId);
|
|
255
246
|
|
|
256
|
-
if (
|
|
247
|
+
if (debugDevicesState && debugDevicesState.val.includes(nodeId)) {
|
|
257
248
|
this.log.warn(`--->>> fromZ2W_RAW2-> ${JSON.stringify(eventTyp)}` );
|
|
258
249
|
}
|
|
259
250
|
|
|
@@ -273,7 +264,7 @@ class zwavews extends core.Adapter {
|
|
|
273
264
|
}
|
|
274
265
|
}
|
|
275
266
|
|
|
276
|
-
parsePath = utils.formatObject(parsePath);
|
|
267
|
+
parsePath = utils.deleteLastDot(utils.formatObject(parsePath));
|
|
277
268
|
|
|
278
269
|
if (nodeArg.commandClass === 119) { // sonderlocke für node naming
|
|
279
270
|
switch (nodeArg.property) {
|
|
@@ -301,8 +292,13 @@ class zwavews extends core.Adapter {
|
|
|
301
292
|
parsePath = `${parsePath}_${nodeArg.endpoint}`;
|
|
302
293
|
}
|
|
303
294
|
|
|
304
|
-
|
|
295
|
+
parsePath = utils.deleteLastDot(parsePath); // check again
|
|
305
296
|
|
|
297
|
+
if (eventTyp.event === 'value notification') {
|
|
298
|
+
await helper.parse(`${parsePath}`, nodeArg.newValue, options, true);
|
|
299
|
+
} else {
|
|
300
|
+
await helper.parse(`${parsePath}`, nodeArg.newValue, options, false);
|
|
301
|
+
}
|
|
306
302
|
break;
|
|
307
303
|
}
|
|
308
304
|
|
|
@@ -430,7 +426,6 @@ class zwavews extends core.Adapter {
|
|
|
430
426
|
|
|
431
427
|
if (state && state.ack == false) {
|
|
432
428
|
if (id.endsWith("info.debugId")) {
|
|
433
|
-
logCustomizations.debugDevices = state.val.toLowerCase();
|
|
434
429
|
this.setStateChanged(id, state.val, true);
|
|
435
430
|
return;
|
|
436
431
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.zwavews",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "zwavews adapter for ioBroker",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Dennis Rathjen and Arthur Rupp",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@iobroker/adapter-core": "^3.3.2",
|
|
29
|
-
"@iobroker/dm-utils": "^
|
|
29
|
+
"@iobroker/dm-utils": "^3.0.0",
|
|
30
30
|
"humanize-duration": "^3.33.2",
|
|
31
31
|
"aedes": "^0.51.3",
|
|
32
32
|
"aedes-persistence-nedb": "^2.0.3",
|
|
@@ -38,14 +38,14 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@alcalzone/release-script": "^5.1.1",
|
|
41
|
-
"@alcalzone/release-script-plugin-iobroker": "^
|
|
41
|
+
"@alcalzone/release-script-plugin-iobroker": "^5.1.2",
|
|
42
42
|
"@alcalzone/release-script-plugin-license": "^5.1.1",
|
|
43
43
|
"@alcalzone/release-script-plugin-manual-review": "^4.0.0",
|
|
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
|
-
"@types/node": "^25.
|
|
48
|
+
"@types/node": "^25.5.0",
|
|
49
49
|
"@types/node-schedule": "^2.1.8",
|
|
50
50
|
"typescript": "~5.9.2"
|
|
51
51
|
},
|