iobroker.openknx 1.1.4 → 1.1.6
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 +6 -3
- package/admin/index_m.html +62 -39
- package/io-package.json +2 -2
- package/lib/knxproj/applicationLoader.js +188 -240
- package/lib/knxproj/extractor.js +20 -19
- package/lib/knxproj/parser.js +7 -14
- package/lib/tools.js +1 -1
- package/main.js +95 -24
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -23,15 +23,18 @@
|
|
|
23
23
|
ioBroker adapter for KNX IP communication, powered by [KNXUltimate](https://github.com/Supergiovane/knxultimate).
|
|
24
24
|
|
|
25
25
|
- Native .knxproj import (ETS4/5/6, password-protected projects supported) -- import dialog accepts both .knxproj and .xml files
|
|
26
|
+
- No adapter update needed to import new ETS versions
|
|
26
27
|
- ETS group address XML import as fallback
|
|
27
28
|
- Read/Write/Transmit/Update flags from ETS ComObjects (no more guessing)
|
|
28
29
|
- DPT inference from ComObjects when GA-level DPT is missing
|
|
29
30
|
- Room assignment (enum.rooms) from ETS building structure
|
|
31
|
+
- Extended DPT coverage (DPT-22, 213, 222, 235, 242, 249, 251, BigInt DPT29)
|
|
30
32
|
- KNX Secure (IP Secure tunneling via .knxkeys keyfile or password)
|
|
31
33
|
- Tunneling (UDP/TCP) and Routing (Multicast) protocols
|
|
34
|
+
- GA-Tools: edit all GA properties (DPT, type, role, read/write flags) with filter and tree view
|
|
35
|
+
- Direct Link: connect any ioBroker state to a KNX group address (1:1, trigger, toggle modes)
|
|
32
36
|
- Alias generation to merge action and status GAs into a single ioBroker object
|
|
33
|
-
-
|
|
34
|
-
|
|
37
|
+
- Status/Act GA linking without alias mode for migration
|
|
35
38
|
- Automatic gateway discovery
|
|
36
39
|
- Automatic reconnect with exponential backoff
|
|
37
40
|
- Autoread of configured datapoints on startup
|
|
@@ -49,7 +52,7 @@ ioBroker adapter for KNX IP communication, powered by [KNXUltimate](https://gith
|
|
|
49
52
|
Placeholder for the next version (at the beginning of the line):
|
|
50
53
|
### **WORK IN PROGRESS**
|
|
51
54
|
-->
|
|
52
|
-
### 1.1.
|
|
55
|
+
### 1.1.6 (2026-04-12)
|
|
53
56
|
|
|
54
57
|
- (TA2k) **breaking:** KNX communication switched to KNXUltimate
|
|
55
58
|
- (TA2k) **breaking:** DPT21 property names changed (outofservice → outOfService, inalarm → inAlarm, alarmeunack → alarmUnAck), values must be boolean
|
package/admin/index_m.html
CHANGED
|
@@ -310,48 +310,71 @@
|
|
|
310
310
|
reader.addEventListener("load", function readFile(event) {
|
|
311
311
|
$("#etsFile").replaceWith($(etsFile));
|
|
312
312
|
$("#etsFile").change(processXmlFile);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
);
|
|
318
|
-
var
|
|
319
|
-
var estSec = Math.max(1, Math.round(sizeMB * 1));
|
|
313
|
+
// readAsDataURL returns "data:application/...;base64,XXXX"
|
|
314
|
+
var dataUrl = event.target.result;
|
|
315
|
+
var base64 = dataUrl.substring(dataUrl.indexOf(",") + 1);
|
|
316
|
+
var sizeMB = (base64.length * 3 / 4 / 1024 / 1024).toFixed(1);
|
|
317
|
+
console.log("[openknx] knxproj file read: " + sizeMB + " MB");
|
|
318
|
+
var estSec = Math.max(1, Math.round(sizeMB * 0.75));
|
|
320
319
|
showToast(null, _("Import started") + " (" + sizeMB + " MB, ~" + estSec + "s) ...", null, estSec * 1000 + 3000);
|
|
321
320
|
$("#progress").show();
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
321
|
+
|
|
322
|
+
// Split base64 into 500KB chunks to avoid socket.io message size limits
|
|
323
|
+
var CHUNK_SIZE = 500000;
|
|
324
|
+
var totalChunks = Math.ceil(base64.length / CHUNK_SIZE);
|
|
325
|
+
console.log("[openknx] sending " + totalChunks + " chunks...");
|
|
326
|
+
|
|
327
|
+
var sendChunk = function (index) {
|
|
328
|
+
if (index >= totalChunks) {
|
|
329
|
+
// All chunks sent, trigger import
|
|
330
|
+
console.log("[openknx] all chunks sent, triggering import...");
|
|
331
|
+
sendTo(
|
|
332
|
+
null,
|
|
333
|
+
"importKnxprojEnd",
|
|
334
|
+
{
|
|
335
|
+
password: $("#knxprojPassword").val() || undefined,
|
|
336
|
+
language: undefined,
|
|
337
|
+
onlyAddNewObjects: $("#onlyAddNewObjects").prop("checked"),
|
|
338
|
+
removeUnusedObjects: $("#removeUnusedObjects").prop("checked"),
|
|
339
|
+
cleanImport: $("#cleanImport").prop("checked"),
|
|
340
|
+
},
|
|
341
|
+
function (result) {
|
|
342
|
+
console.log("[openknx] importKnxproj callback:", JSON.stringify(result));
|
|
343
|
+
$("#progress").hide();
|
|
344
|
+
if (!result || result.error) {
|
|
345
|
+
var message = "Unknown error";
|
|
346
|
+
if (result && result.error) {
|
|
347
|
+
message = "Imported " + result.count + " states successfully<p>" + result.error + "</p><p>See log for more information</p>";
|
|
348
|
+
}
|
|
349
|
+
showMessage(_(message));
|
|
350
|
+
} else {
|
|
351
|
+
showMessage(
|
|
352
|
+
_("Imported %s states successfully", result.count) +
|
|
353
|
+
"<br/><br/>" +
|
|
354
|
+
_("knxproj import hint"),
|
|
355
|
+
);
|
|
356
|
+
sendTo(null, "restart", null);
|
|
357
|
+
showToast(null, _("Restarting adapter" + "..."), null, 10000);
|
|
358
|
+
}
|
|
359
|
+
window.isProcessingRequest = false;
|
|
360
|
+
},
|
|
361
|
+
);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
var chunk = base64.substring(index * CHUNK_SIZE, (index + 1) * CHUNK_SIZE);
|
|
365
|
+
sendTo(null, "importKnxprojChunk", { index: index, data: chunk }, function () {
|
|
366
|
+
console.log("[openknx] chunk " + (index + 1) + "/" + totalChunks + " sent");
|
|
367
|
+
sendChunk(index + 1);
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// Start: tell adapter how many chunks to expect
|
|
372
|
+
sendTo(null, "importKnxprojStart", { totalChunks: totalChunks, sizeMB: sizeMB }, function () {
|
|
373
|
+
console.log("[openknx] import started, sending chunks...");
|
|
374
|
+
sendChunk(0);
|
|
375
|
+
});
|
|
353
376
|
});
|
|
354
|
-
reader.
|
|
377
|
+
reader.readAsDataURL(file);
|
|
355
378
|
} else {
|
|
356
379
|
showMessage(_("Unsupported file format"));
|
|
357
380
|
}
|
package/io-package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "openknx",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.6",
|
|
5
5
|
"news": {
|
|
6
|
-
"1.1.
|
|
6
|
+
"1.1.6": {
|
|
7
7
|
"en": "**breaking:** KNX communication switched to KNXUltimate\n**breaking:** DPT21 property names changed (outofservice → outOfService, inalarm → inAlarm, alarmeunack → alarmUnAck), values must be boolean\n**breaking:** DPT237 property names changed to camelCase\nfeature: KNX Secure support\nfeature: Native .knxproj import (ETS4/5/6, password-protected) with flags, DPT inference, room assignment\nfeature: Extended DPT coverage (9 additional DPTs, including DPT-22, 213, 222, 235, 242, 249, 251)\nfeature: Improved connection stability\nfeature: Improved role detection (switch, level, value, text, date) based on DPT type\nfeature: Direct Link all iobroker states to a KNX state\nfeature: GA-Tools: all GA properties editable (DPT, type, role, flags) with compact layout",
|
|
8
8
|
"de": "**breaking:** KNX-Kommunikation auf KNXUltimate umgestellt\n**breaking:** DPT21 Eigenschaftsnamen geändert (outofservice → outOfService, inalarm → inAlarm, alarmeunack → alarmUnAck), Werte müssen boolean sein\n**breaking:** DPT237 Eigenschaftsnamen auf camelCase geändert\nFeature: KNX Secure Unterstützung\nFeature: Nativer .knxproj Import (ETS4/5/6, passwortgeschützt) mit Flags, DPT-Erkennung, Raumzuordnung\nFeature: Erweiterte DPT-Abdeckung (9 zusätzliche DPTs, u.a. DPT-22, 213, 222, 235, 242, 249, 251)\nFeature: Verbesserte Verbindungsstabilität\nFeature: Verbesserte Rollenerkennung (switch, level, value, text, date) basierend auf DPT-Typ\nFeature: Direct Link – beliebige ioBroker-States mit KNX-Gruppenadressen verknüpfen\nFeature: GA-Tools: alle GA-Eigenschaften editierbar (DPT, Typ, Rolle, Flags) mit kompaktem Layout",
|
|
9
9
|
"ru": "**breaking:** KNX-коммуникация переключена на KNXUltimate\n**breaking:** Имена свойств DPT21 изменены (outofservice → outOfService, inalarm → inAlarm, alarmeunack → alarmUnAck), значения должны быть boolean\n**breaking:** Имена свойств DPT237 изменены на camelCase\nFeature: Поддержка KNX Secure\nFeature: Нативный импорт .knxproj (ETS4/5/6, с паролем) с флагами, определением DPT, назначением комнат\nFeature: Расширенное покрытие DPT (9 дополнительных DPT, включая DPT-22, 213, 222, 235, 242, 249, 251)\nFeature: Улучшенная стабильность соединения\nFeature: Улучшенное определение ролей (switch, level, value, text, date) на основе типа DPT\nFeature: Direct Link – привязка любых состояний ioBroker к групповым адресам KNX\nFeature: GA-Tools: все свойства GA редактируемы (DPT, тип, роль, флаги) с компактным макетом",
|
|
@@ -5,104 +5,26 @@
|
|
|
5
5
|
* Port of xknxproject/loader/application_program_loader.py
|
|
6
6
|
* Parses M-xxxx/<application_program>.xml files to extract ComObjects,
|
|
7
7
|
* ComObjectRefs, Allocators, ModuleDef Arguments, Channels, and Translations.
|
|
8
|
+
*
|
|
9
|
+
* Uses SAX streaming parser instead of DOM to handle very large XMLs (100+ MB)
|
|
10
|
+
* without excessive memory consumption.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
"use strict";
|
|
11
14
|
|
|
12
|
-
const
|
|
15
|
+
const sax = require("sax");
|
|
13
16
|
const { parseDptTypes, parseXmlFlag } = require("./util");
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* @param {Node} node
|
|
19
|
-
* @param {string} localName
|
|
20
|
-
* @returns {Element[]}
|
|
21
|
-
*/
|
|
22
|
-
function byTagNS(node, localName) {
|
|
23
|
-
return Array.from(node.getElementsByTagNameNS("*", localName));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Get direct child elements with a given local name.
|
|
28
|
-
*
|
|
29
|
-
* @param {Node} node
|
|
30
|
-
* @param {string} localName
|
|
31
|
-
* @returns {Element[]}
|
|
32
|
-
*/
|
|
33
|
-
function childrenByTag(node, localName) {
|
|
34
|
-
const result = [];
|
|
35
|
-
const children = node.childNodes;
|
|
36
|
-
for (let i = 0; i < children.length; i++) {
|
|
37
|
-
const child = children[i];
|
|
38
|
-
if (child.nodeType === 1) {
|
|
39
|
-
const ln = child.localName || child.nodeName.replace(/^.*:/, "");
|
|
40
|
-
if (ln === localName) {
|
|
41
|
-
result.push(child);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return result;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Parse a ComObject element.
|
|
50
|
-
*
|
|
51
|
-
* @param {Element} elem
|
|
52
|
-
* @returns {object} ComObject
|
|
53
|
-
*/
|
|
54
|
-
function parseComObject(elem) {
|
|
55
|
-
const identifier = elem.getAttribute("Id");
|
|
56
|
-
return {
|
|
57
|
-
identifier,
|
|
58
|
-
name: elem.getAttribute("Name") || "",
|
|
59
|
-
text: elem.getAttribute("Text") || "",
|
|
60
|
-
number: parseInt(elem.getAttribute("Number") || "0", 10),
|
|
61
|
-
functionText: elem.getAttribute("FunctionText") || "",
|
|
62
|
-
objectSize: elem.getAttribute("ObjectSize") || "",
|
|
63
|
-
readFlag: parseXmlFlag(elem.getAttribute("ReadFlag"), false),
|
|
64
|
-
writeFlag: parseXmlFlag(elem.getAttribute("WriteFlag"), false),
|
|
65
|
-
communicationFlag: parseXmlFlag(elem.getAttribute("CommunicationFlag"), false),
|
|
66
|
-
transmitFlag: parseXmlFlag(elem.getAttribute("TransmitFlag"), false),
|
|
67
|
-
updateFlag: parseXmlFlag(elem.getAttribute("UpdateFlag"), false),
|
|
68
|
-
readOnInitFlag: parseXmlFlag(elem.getAttribute("ReadOnInitFlag"), false),
|
|
69
|
-
datapointTypes: parseDptTypes(elem.getAttribute("DatapointType")),
|
|
70
|
-
baseNumberArgumentRef: elem.getAttribute("BaseNumber") || null,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Parse a ComObjectRef element.
|
|
76
|
-
*
|
|
77
|
-
* @param {Element} elem
|
|
78
|
-
* @returns {object} ComObjectRef
|
|
19
|
+
* Strip namespace prefix from a tag name: "knx:ComObject" -> "ComObject"
|
|
79
20
|
*/
|
|
80
|
-
function
|
|
81
|
-
const
|
|
82
|
-
return
|
|
83
|
-
identifier,
|
|
84
|
-
refId: elem.getAttribute("RefId") || "",
|
|
85
|
-
name: elem.getAttribute("Name") || null,
|
|
86
|
-
text: elem.getAttribute("Text") || null,
|
|
87
|
-
functionText: elem.getAttribute("FunctionText") || null,
|
|
88
|
-
objectSize: elem.getAttribute("ObjectSize") || null,
|
|
89
|
-
readFlag: parseXmlFlag(elem.getAttribute("ReadFlag"), null),
|
|
90
|
-
writeFlag: parseXmlFlag(elem.getAttribute("WriteFlag"), null),
|
|
91
|
-
communicationFlag: parseXmlFlag(elem.getAttribute("CommunicationFlag"), null),
|
|
92
|
-
transmitFlag: parseXmlFlag(elem.getAttribute("TransmitFlag"), null),
|
|
93
|
-
updateFlag: parseXmlFlag(elem.getAttribute("UpdateFlag"), null),
|
|
94
|
-
readOnInitFlag: parseXmlFlag(elem.getAttribute("ReadOnInitFlag"), null),
|
|
95
|
-
datapointTypes: parseDptTypes(elem.getAttribute("DatapointType")),
|
|
96
|
-
textParameterRefId: elem.getAttribute("TextParameterRefId") || null,
|
|
97
|
-
};
|
|
21
|
+
function localName(name) {
|
|
22
|
+
const i = name.indexOf(":");
|
|
23
|
+
return i >= 0 ? name.substring(i + 1) : name;
|
|
98
24
|
}
|
|
99
25
|
|
|
100
26
|
/**
|
|
101
27
|
* Apply translations to translatable objects (ComObjects, ComObjectRefs, Channels).
|
|
102
|
-
* Modifies objects in place.
|
|
103
|
-
*
|
|
104
|
-
* @param {object} objectMap - { [id]: object }
|
|
105
|
-
* @param {object} translationMap - { [refId]: { [attributeName]: text } }
|
|
106
28
|
*/
|
|
107
29
|
function applyTranslations(objectMap, translationMap) {
|
|
108
30
|
for (const identifier of Object.keys(objectMap)) {
|
|
@@ -121,182 +43,208 @@ function applyTranslations(objectMap, translationMap) {
|
|
|
121
43
|
}
|
|
122
44
|
|
|
123
45
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* @param {Document} doc - Parsed XML document
|
|
127
|
-
* @param {object} comObjects
|
|
128
|
-
* @param {object} comObjectRefs
|
|
129
|
-
* @param {Set<string>} usedComObjectRefIds
|
|
130
|
-
* @param {object} channels
|
|
131
|
-
* @param {string} languageCode
|
|
132
|
-
*/
|
|
133
|
-
function parseTranslations(doc, comObjects, comObjectRefs, usedComObjectRefIds, channels, languageCode) {
|
|
134
|
-
// Build set of IDs we need translations for
|
|
135
|
-
const usedComObjectIds = new Set();
|
|
136
|
-
for (const ref of Object.values(comObjectRefs)) {
|
|
137
|
-
usedComObjectIds.add(ref.refId);
|
|
138
|
-
}
|
|
139
|
-
const usedTranslationIds = new Set([...usedComObjectIds, ...usedComObjectRefIds, ...Object.keys(channels)]);
|
|
140
|
-
|
|
141
|
-
// translationMap: { refId: { attributeName: text } }
|
|
142
|
-
const translationMap = {};
|
|
143
|
-
|
|
144
|
-
// Find the Language element matching our languageCode
|
|
145
|
-
const languageNodes = byTagNS(doc, "Language");
|
|
146
|
-
let targetLanguageNode = null;
|
|
147
|
-
for (const langNode of languageNodes) {
|
|
148
|
-
if (langNode.getAttribute && langNode.getAttribute("Identifier") === languageCode) {
|
|
149
|
-
targetLanguageNode = langNode;
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (!targetLanguageNode) {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Find TranslationElement descendants within this Language node
|
|
159
|
-
const translationElements = byTagNS(targetLanguageNode, "TranslationElement");
|
|
160
|
-
|
|
161
|
-
for (const transElem of translationElements) {
|
|
162
|
-
const refId = transElem.getAttribute("RefId");
|
|
163
|
-
if (!refId || !usedTranslationIds.has(refId)) {
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Find Translation direct children
|
|
168
|
-
const translations = childrenByTag(transElem, "Translation");
|
|
169
|
-
|
|
170
|
-
for (const trans of translations) {
|
|
171
|
-
const attributeName = trans.getAttribute("AttributeName");
|
|
172
|
-
const text = trans.getAttribute("Text");
|
|
173
|
-
if (attributeName && text) {
|
|
174
|
-
if (!translationMap[refId]) {
|
|
175
|
-
translationMap[refId] = {};
|
|
176
|
-
}
|
|
177
|
-
translationMap[refId][attributeName] = text;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
applyTranslations(comObjectRefs, translationMap);
|
|
183
|
-
applyTranslations(comObjects, translationMap);
|
|
184
|
-
applyTranslations(channels, translationMap);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Load application program data from an XML file inside the KNX project archive.
|
|
46
|
+
* Load application program data from an XML file using SAX streaming.
|
|
189
47
|
*
|
|
190
48
|
* @param {import('./extractor').KNXProjContents} knxProjContents
|
|
191
49
|
* @param {Set<string>} usedComObjectRefIds - Only load ComObjectRefs with these IDs
|
|
192
|
-
* @param {string} applicationProgramXmlPath
|
|
50
|
+
* @param {string} applicationProgramXmlPath
|
|
193
51
|
* @param {string} languageCode
|
|
194
52
|
* @returns {Promise<object>} ApplicationProgram
|
|
195
53
|
*/
|
|
196
54
|
async function load(knxProjContents, usedComObjectRefIds, applicationProgramXmlPath, languageCode) {
|
|
197
55
|
let xmlContent = await knxProjContents.readFile(applicationProgramXmlPath);
|
|
198
56
|
|
|
199
|
-
|
|
200
|
-
const comObjectRefs = {}; // { Id: ComObjectRef }
|
|
201
|
-
const allocators = {}; // { Id: Allocator }
|
|
202
|
-
const moduleDefArguments = {}; // { Id: { name, allocates } }
|
|
203
|
-
const numericArgs = {}; // { RefId: { allocatorRefId, value, baseValue } }
|
|
204
|
-
const channels = {}; // { Id: ApplicationProgramChannel }
|
|
205
|
-
|
|
206
|
-
// Strip BOM if present - @xmldom/xmldom fails if BOM precedes <?xml declaration
|
|
57
|
+
// Strip BOM if present
|
|
207
58
|
if (xmlContent.charCodeAt(0) === 0xfeff) {
|
|
208
59
|
xmlContent = xmlContent.slice(1);
|
|
209
60
|
}
|
|
210
61
|
|
|
211
|
-
const
|
|
212
|
-
const
|
|
62
|
+
const comObjects = {};
|
|
63
|
+
const comObjectRefs = {};
|
|
64
|
+
const allocators = {};
|
|
65
|
+
const moduleDefArguments = {};
|
|
66
|
+
const numericArgs = {};
|
|
67
|
+
const channels = {};
|
|
213
68
|
|
|
214
|
-
//
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
69
|
+
// Translation state
|
|
70
|
+
const usedTranslationIds = new Set();
|
|
71
|
+
const translationMap = {};
|
|
72
|
+
let inTargetLanguage = false;
|
|
73
|
+
let currentTranslationRefId = null;
|
|
220
74
|
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
for (const elem of comObjectRefNodes) {
|
|
224
|
-
const id = elem.getAttribute("Id");
|
|
225
|
-
if (usedComObjectRefIds.has(id)) {
|
|
226
|
-
const cor = parseComObjectRef(elem);
|
|
227
|
-
comObjectRefs[id] = cor;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
75
|
+
// Track parent context for ModuleDef > Argument
|
|
76
|
+
let inModuleDef = false;
|
|
230
77
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
name: elem.getAttribute("Name") || "",
|
|
238
|
-
start: parseInt(elem.getAttribute("Start") || "0", 10),
|
|
239
|
-
end: parseInt(elem.getAttribute("maxInclusive") || "0", 10),
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const parser = sax.parser(true, { trim: false, xmlns: false });
|
|
80
|
+
|
|
81
|
+
parser.onerror = (err) => {
|
|
82
|
+
// sax recovers from most errors, just log
|
|
83
|
+
parser.resume();
|
|
240
84
|
};
|
|
241
|
-
}
|
|
242
85
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
86
|
+
parser.onopentag = (node) => {
|
|
87
|
+
const tag = localName(node.name);
|
|
88
|
+
const a = node.attributes;
|
|
89
|
+
|
|
90
|
+
switch (tag) {
|
|
91
|
+
case "ComObject": {
|
|
92
|
+
const id = a.Id;
|
|
93
|
+
if (id) {
|
|
94
|
+
comObjects[id] = {
|
|
95
|
+
identifier: id,
|
|
96
|
+
name: a.Name || "",
|
|
97
|
+
text: a.Text || "",
|
|
98
|
+
number: parseInt(a.Number || "0", 10),
|
|
99
|
+
functionText: a.FunctionText || "",
|
|
100
|
+
objectSize: a.ObjectSize || "",
|
|
101
|
+
readFlag: parseXmlFlag(a.ReadFlag, false),
|
|
102
|
+
writeFlag: parseXmlFlag(a.WriteFlag, false),
|
|
103
|
+
communicationFlag: parseXmlFlag(a.CommunicationFlag, false),
|
|
104
|
+
transmitFlag: parseXmlFlag(a.TransmitFlag, false),
|
|
105
|
+
updateFlag: parseXmlFlag(a.UpdateFlag, false),
|
|
106
|
+
readOnInitFlag: parseXmlFlag(a.ReadOnInitFlag, false),
|
|
107
|
+
datapointTypes: parseDptTypes(a.DatapointType),
|
|
108
|
+
baseNumberArgumentRef: a.BaseNumber || null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case "ComObjectRef": {
|
|
114
|
+
const id = a.Id;
|
|
115
|
+
if (id && usedComObjectRefIds.has(id)) {
|
|
116
|
+
comObjectRefs[id] = {
|
|
117
|
+
identifier: id,
|
|
118
|
+
refId: a.RefId || "",
|
|
119
|
+
name: a.Name || null,
|
|
120
|
+
text: a.Text || null,
|
|
121
|
+
functionText: a.FunctionText || null,
|
|
122
|
+
objectSize: a.ObjectSize || null,
|
|
123
|
+
readFlag: parseXmlFlag(a.ReadFlag, null),
|
|
124
|
+
writeFlag: parseXmlFlag(a.WriteFlag, null),
|
|
125
|
+
communicationFlag: parseXmlFlag(a.CommunicationFlag, null),
|
|
126
|
+
transmitFlag: parseXmlFlag(a.TransmitFlag, null),
|
|
127
|
+
updateFlag: parseXmlFlag(a.UpdateFlag, null),
|
|
128
|
+
readOnInitFlag: parseXmlFlag(a.ReadOnInitFlag, null),
|
|
129
|
+
datapointTypes: parseDptTypes(a.DatapointType),
|
|
130
|
+
textParameterRefId: a.TextParameterRefId || null,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case "Allocator": {
|
|
136
|
+
const id = a.Id;
|
|
137
|
+
if (id) {
|
|
138
|
+
allocators[id] = {
|
|
139
|
+
identifier: id,
|
|
140
|
+
name: a.Name || "",
|
|
141
|
+
start: parseInt(a.Start || "0", 10),
|
|
142
|
+
end: parseInt(a.maxInclusive || "0", 10),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "ModuleDef":
|
|
148
|
+
inModuleDef = true;
|
|
149
|
+
break;
|
|
150
|
+
case "Argument": {
|
|
151
|
+
if (inModuleDef) {
|
|
152
|
+
const id = a.Id;
|
|
153
|
+
if (id) {
|
|
154
|
+
moduleDefArguments[id] = {
|
|
155
|
+
name: a.Name || "",
|
|
156
|
+
allocates: a.Allocates != null ? parseInt(a.Allocates, 10) : null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case "NumericArg": {
|
|
163
|
+
const refId = a.RefId;
|
|
164
|
+
if (refId) {
|
|
165
|
+
numericArgs[refId] = {
|
|
166
|
+
allocatorRefId: a.AllocatorRefId || null,
|
|
167
|
+
value: a.Value != null ? parseInt(a.Value, 10) : null,
|
|
168
|
+
baseValue: a.BaseValue || null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case "Channel": {
|
|
174
|
+
const id = a.Id;
|
|
175
|
+
if (id) {
|
|
176
|
+
channels[id] = {
|
|
177
|
+
identifier: id,
|
|
178
|
+
name: a.Name || "",
|
|
179
|
+
number: a.Number || "",
|
|
180
|
+
text: a.Text || null,
|
|
181
|
+
textParameterRefId: a.TextParameterRefId || null,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
// Translation handling
|
|
187
|
+
case "Language":
|
|
188
|
+
if (languageCode && a.Identifier === languageCode) {
|
|
189
|
+
inTargetLanguage = true;
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
case "TranslationElement":
|
|
193
|
+
if (inTargetLanguage && a.RefId) {
|
|
194
|
+
currentTranslationRefId = a.RefId;
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
case "Translation":
|
|
198
|
+
if (currentTranslationRefId && a.AttributeName && a.Text) {
|
|
199
|
+
if (!translationMap[currentTranslationRefId]) {
|
|
200
|
+
translationMap[currentTranslationRefId] = {};
|
|
201
|
+
}
|
|
202
|
+
translationMap[currentTranslationRefId][a.AttributeName] = a.Text;
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
257
207
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
}
|
|
208
|
+
parser.onclosetag = (name) => {
|
|
209
|
+
const tag = localName(name);
|
|
210
|
+
if (tag === "ModuleDef") {
|
|
211
|
+
inModuleDef = false;
|
|
212
|
+
} else if (tag === "Language") {
|
|
213
|
+
inTargetLanguage = false;
|
|
214
|
+
} else if (tag === "TranslationElement") {
|
|
215
|
+
currentTranslationRefId = null;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
271
218
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
219
|
+
parser.onend = () => {
|
|
220
|
+
// Build translation IDs from parsed data and apply
|
|
221
|
+
if (languageCode) {
|
|
222
|
+
for (const ref of Object.values(comObjectRefs)) {
|
|
223
|
+
usedTranslationIds.add(ref.refId);
|
|
224
|
+
}
|
|
225
|
+
for (const id of usedComObjectRefIds) {
|
|
226
|
+
usedTranslationIds.add(id);
|
|
227
|
+
}
|
|
228
|
+
for (const id of Object.keys(channels)) {
|
|
229
|
+
usedTranslationIds.add(id);
|
|
230
|
+
}
|
|
231
|
+
applyTranslations(comObjectRefs, translationMap);
|
|
232
|
+
applyTranslations(comObjects, translationMap);
|
|
233
|
+
applyTranslations(channels, translationMap);
|
|
234
|
+
}
|
|
286
235
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
236
|
+
resolve({
|
|
237
|
+
comObjects,
|
|
238
|
+
comObjectRefs,
|
|
239
|
+
allocators,
|
|
240
|
+
moduleDefArguments,
|
|
241
|
+
numericArgs,
|
|
242
|
+
channels,
|
|
243
|
+
});
|
|
244
|
+
};
|
|
291
245
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
comObjectRefs,
|
|
295
|
-
allocators,
|
|
296
|
-
moduleDefArguments,
|
|
297
|
-
numericArgs,
|
|
298
|
-
channels,
|
|
299
|
-
};
|
|
246
|
+
parser.write(xmlContent).close();
|
|
247
|
+
});
|
|
300
248
|
}
|
|
301
249
|
|
|
302
250
|
module.exports = {
|
package/lib/knxproj/extractor.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
"use strict";
|
|
12
12
|
|
|
13
|
-
const crypto = require("crypto");
|
|
13
|
+
const crypto = require("node:crypto");
|
|
14
14
|
const { BlobReader, ZipReader, Uint8ArrayWriter, configure } = require("@zip.js/zip.js");
|
|
15
15
|
|
|
16
16
|
// Disable web workers — we run in Node.js
|
|
@@ -47,33 +47,34 @@ async function openZipBuffer(buffer, password) {
|
|
|
47
47
|
throw err;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
//
|
|
50
|
+
// Lazy extraction: only decompress entries when buffer() is called.
|
|
51
|
+
// Buffers are NOT cached — each call re-decompresses from the ZIP entry.
|
|
52
|
+
// This frees memory immediately after each file is processed, avoiding
|
|
53
|
+
// hundreds of MB of simultaneous buffer allocations on large projects.
|
|
51
54
|
const files = [];
|
|
52
55
|
for (const entry of entries) {
|
|
53
56
|
if (entry.directory) {
|
|
54
57
|
continue;
|
|
55
58
|
}
|
|
56
|
-
const filename = entry.filename;
|
|
57
|
-
let buf;
|
|
58
|
-
try {
|
|
59
|
-
const data = await entry.getData(new Uint8ArrayWriter());
|
|
60
|
-
buf = Buffer.from(data);
|
|
61
|
-
} catch (err) {
|
|
62
|
-
if (err.message && (err.message.includes("password") || err.message.includes("Password"))) {
|
|
63
|
-
await reader.close().catch(() => {});
|
|
64
|
-
throw new InvalidPasswordException(`Failed to decrypt entry "${filename}" - wrong password? (${err.message})`);
|
|
65
|
-
}
|
|
66
|
-
await reader.close().catch(() => {});
|
|
67
|
-
throw err;
|
|
68
|
-
}
|
|
69
59
|
files.push({
|
|
70
|
-
path: filename,
|
|
60
|
+
path: entry.filename,
|
|
71
61
|
type: "File",
|
|
72
|
-
uncompressedSize:
|
|
73
|
-
|
|
62
|
+
uncompressedSize: entry.uncompressedSize,
|
|
63
|
+
_entry: entry,
|
|
64
|
+
buffer: async function () {
|
|
65
|
+
try {
|
|
66
|
+
const data = await this._entry.getData(new Uint8ArrayWriter());
|
|
67
|
+
return Buffer.from(data);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (err.message && (err.message.includes("password") || err.message.includes("Password"))) {
|
|
70
|
+
throw new InvalidPasswordException(`Failed to decrypt entry "${this.path}" - wrong password? (${err.message})`);
|
|
71
|
+
}
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
74
75
|
});
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
+
// Keep reader open — entries are decompressed lazily in buffer()
|
|
77
78
|
|
|
78
79
|
return { files };
|
|
79
80
|
}
|
package/lib/knxproj/parser.js
CHANGED
|
@@ -677,9 +677,10 @@ class Parser {
|
|
|
677
677
|
}
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
-
// 5.
|
|
680
|
+
// 5+6. Load each application program, merge into devices immediately,
|
|
681
|
+
// then discard to free memory. Processing one at a time avoids holding
|
|
682
|
+
// multiple large DOM trees simultaneously.
|
|
681
683
|
const applicationProgramFiles = getApplicationProgramFilesForDevices(this.devices);
|
|
682
|
-
const applications = new Map();
|
|
683
684
|
|
|
684
685
|
for (const [xmlFile, devicesForApp] of applicationProgramFiles) {
|
|
685
686
|
const usedRefIds = collectUsedComObjectRefIds(devicesForApp);
|
|
@@ -689,20 +690,12 @@ class Parser {
|
|
|
689
690
|
xmlFile,
|
|
690
691
|
this.languageCode,
|
|
691
692
|
);
|
|
692
|
-
applications.set(xmlFile, appProgram);
|
|
693
|
-
}
|
|
694
693
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
continue;
|
|
699
|
-
}
|
|
700
|
-
const xmlFile = applicationProgramXml(device);
|
|
701
|
-
const application = applications.get(xmlFile);
|
|
702
|
-
if (!application) {
|
|
703
|
-
continue;
|
|
694
|
+
// Merge into devices that use this application program
|
|
695
|
+
for (const device of devicesForApp) {
|
|
696
|
+
mergeDeviceApplicationProgramInfo(device, appProgram);
|
|
704
697
|
}
|
|
705
|
-
|
|
698
|
+
// appProgram goes out of scope here — DOM tree and parsed data freed
|
|
706
699
|
}
|
|
707
700
|
}
|
|
708
701
|
|
package/lib/tools.js
CHANGED
package/main.js
CHANGED
|
@@ -200,22 +200,58 @@ class openknx extends utils.Adapter {
|
|
|
200
200
|
};
|
|
201
201
|
if (obj.message.cleanImport) {
|
|
202
202
|
this.log.info("Clean import: deleting all existing KNX objects before import...");
|
|
203
|
-
this.cleanImport().then(doXmlImport)
|
|
203
|
+
this.cleanImport().then(doXmlImport).catch(e => {
|
|
204
|
+
this.log.error(`Clean import failed: ${e.message}`);
|
|
205
|
+
if (obj.callback) {
|
|
206
|
+
this.sendTo(obj.from, obj.command, { error: `Clean import failed: ${e.message}`, count: 0 }, obj.callback);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
204
209
|
} else {
|
|
205
210
|
doXmlImport();
|
|
206
211
|
}
|
|
207
212
|
break;
|
|
208
213
|
}
|
|
209
|
-
case "
|
|
210
|
-
this.
|
|
214
|
+
case "importKnxprojStart": {
|
|
215
|
+
this.knxprojChunks = [];
|
|
216
|
+
this.log.info(`ETS .knxproj chunked import started (${obj.message.totalChunks} chunks, ${obj.message.sizeMB} MB)`);
|
|
217
|
+
if (obj.callback) {
|
|
218
|
+
this.sendTo(obj.from, obj.command, {}, obj.callback);
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case "importKnxprojChunk": {
|
|
223
|
+
// Decode each chunk to Buffer immediately to avoid keeping full base64 string in memory
|
|
224
|
+
this.knxprojChunks[obj.message.index] = Buffer.from(obj.message.data, "base64");
|
|
225
|
+
if (obj.callback) {
|
|
226
|
+
this.sendTo(obj.from, obj.command, {}, obj.callback);
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case "importKnxprojEnd": {
|
|
231
|
+
this.log.info("ETS .knxproj all chunks received, assembling...");
|
|
232
|
+
const buffer = Buffer.concat(this.knxprojChunks);
|
|
233
|
+
this.knxprojChunks = null;
|
|
211
234
|
const doKnxprojImport = async () => {
|
|
212
235
|
try {
|
|
213
|
-
|
|
236
|
+
this.log.info(`knxproj file size: ${(buffer.length / 1024 / 1024).toFixed(1)} MB`);
|
|
237
|
+
// Warn if heap limit might be too low for large projects
|
|
238
|
+
const v8 = require("v8");
|
|
239
|
+
const heapStats = v8.getHeapStatistics();
|
|
240
|
+
const heapLimitMB = Math.round(heapStats.heap_size_limit / 1024 / 1024);
|
|
241
|
+
const fileSizeMB = buffer.length / 1024 / 1024;
|
|
242
|
+
if (fileSizeMB > 10 && heapLimitMB < 1024) {
|
|
243
|
+
this.log.warn(`Large knxproj (${fileSizeMB.toFixed(0)} MB) with ${heapLimitMB} MB heap limit. If the adapter crashes with "heap out of memory", increase Node.js memory: Instances > openknx > wrench icon > Node.js Options: --max-old-space-size=2048`);
|
|
244
|
+
}
|
|
214
245
|
const password = obj.message.password || undefined;
|
|
215
246
|
const language = obj.message.language || undefined;
|
|
247
|
+
this.log.debug("knxproj: extracting ZIP and parsing XML...");
|
|
216
248
|
const knxProject = await parseKnxproj(buffer, password, language);
|
|
249
|
+
const gaCount = knxProject.groupAddresses ? Object.keys(knxProject.groupAddresses).length : 0;
|
|
250
|
+
this.log.info(`knxproj: parsed ${gaCount} group addresses`);
|
|
217
251
|
const res = projectImport.convertKnxProject(this, knxProject);
|
|
252
|
+
this.log.info(`knxproj: converted to ${res.objects.length} ioBroker objects`);
|
|
218
253
|
await this._createRoomEnums(res.rooms);
|
|
254
|
+
this.log.debug(`knxproj: created ${Object.keys(res.rooms || {}).length} room enums`);
|
|
219
255
|
this._finishImport(res.objects, res.error, obj);
|
|
220
256
|
} catch (e) {
|
|
221
257
|
this.log.error(`knxproj import failed: ${e.message}`);
|
|
@@ -235,7 +271,12 @@ class openknx extends utils.Adapter {
|
|
|
235
271
|
};
|
|
236
272
|
if (obj.message.cleanImport) {
|
|
237
273
|
this.log.info("Clean import: deleting all existing KNX objects before import...");
|
|
238
|
-
this.cleanImport().then(doKnxprojImport)
|
|
274
|
+
this.cleanImport().then(doKnxprojImport).catch(e => {
|
|
275
|
+
this.log.error(`Clean import failed: ${e.message}`);
|
|
276
|
+
if (obj.callback) {
|
|
277
|
+
this.sendTo(obj.from, obj.command, { error: `Clean import failed: ${e.message}`, count: 0 }, obj.callback);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
239
280
|
} else {
|
|
240
281
|
doKnxprojImport();
|
|
241
282
|
}
|
|
@@ -371,6 +412,7 @@ class openknx extends utils.Adapter {
|
|
|
371
412
|
* Common import finish logic shared between XML and knxproj import.
|
|
372
413
|
*/
|
|
373
414
|
_finishImport(res, parseError, obj) {
|
|
415
|
+
this.log.info(`Importing ${res.length} objects into ioBroker...`);
|
|
374
416
|
this.updateObjects(res, 0, obj.message.onlyAddNewObjects, (updateError, length) => {
|
|
375
417
|
const msg = {
|
|
376
418
|
error:
|
|
@@ -426,12 +468,26 @@ class openknx extends utils.Adapter {
|
|
|
426
468
|
* delete all existing KNX objects before a clean re-import
|
|
427
469
|
*/
|
|
428
470
|
async cleanImport() {
|
|
429
|
-
|
|
471
|
+
try {
|
|
472
|
+
await this.delObjectAsync("", { recursive: true });
|
|
473
|
+
} catch (e) {
|
|
474
|
+
this.log.warn(`delObjectAsync("") failed (${e.message}), falling back to manual deletion`);
|
|
475
|
+
const objects = await this.getAdapterObjectsAsync();
|
|
476
|
+
for (const id of Object.keys(objects)) {
|
|
477
|
+
await this.delObjectAsync(id).catch(() => {});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
430
480
|
this.log.info("cleanImport: deleted all existing KNX objects");
|
|
481
|
+
// Re-create info objects that are needed during runtime
|
|
482
|
+
await this.setObjectNotExistsAsync("info.busload", { type: "state", common: { role: "info", name: "Busload", type: "number", read: true, write: false, def: 0, unit: "%" }, native: {} });
|
|
483
|
+
await this.setObjectNotExistsAsync("info.messagecount", { type: "state", common: { role: "info", name: "Message count", type: "number", read: true, write: false, def: 0 }, native: {} });
|
|
431
484
|
}
|
|
432
485
|
|
|
433
486
|
// write found communication objects to adapter object tree
|
|
434
487
|
updateObjects(objects, i, onlyAddNewObjects, callback) {
|
|
488
|
+
if (i > 0 && i % 100 === 0) {
|
|
489
|
+
this.log.debug(`updateObjects: ${i}/${objects.length} objects written`);
|
|
490
|
+
}
|
|
435
491
|
if (i >= objects.length) {
|
|
436
492
|
// end of recursion reached
|
|
437
493
|
let err = this.warnDuplicates(objects);
|
|
@@ -858,6 +914,29 @@ class openknx extends utils.Adapter {
|
|
|
858
914
|
}
|
|
859
915
|
}
|
|
860
916
|
|
|
917
|
+
// Reconnect delays in seconds: 10, 30, 60, 120, 120, 120, 120
|
|
918
|
+
static reconnectDelays = [10, 30, 60, 120, 120, 120, 120];
|
|
919
|
+
|
|
920
|
+
scheduleReconnect() {
|
|
921
|
+
if (this.stopping || this.reconnectCount >= openknx.reconnectDelays.length) {
|
|
922
|
+
if (!this.stopping) {
|
|
923
|
+
this.log.error(`Giving up after ${openknx.reconnectDelays.length} reconnect attempts`);
|
|
924
|
+
}
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
const delay = openknx.reconnectDelays[this.reconnectCount];
|
|
928
|
+
this.reconnectCount++;
|
|
929
|
+
this.log.info(`Reconnect attempt ${this.reconnectCount}/${openknx.reconnectDelays.length} in ${delay}s...`);
|
|
930
|
+
this.reconnectTimer = setTimeout(() => {
|
|
931
|
+
try {
|
|
932
|
+
this.startKnxStack();
|
|
933
|
+
} catch (e) {
|
|
934
|
+
this.log.error(`Reconnect failed: ${e.message || e}`);
|
|
935
|
+
this.scheduleReconnect();
|
|
936
|
+
}
|
|
937
|
+
}, delay * 1000);
|
|
938
|
+
}
|
|
939
|
+
|
|
861
940
|
startKnxStack() {
|
|
862
941
|
// Clean up previous connection (reconnect case)
|
|
863
942
|
if (this.knxConnection) {
|
|
@@ -1014,8 +1093,6 @@ class openknx extends utils.Adapter {
|
|
|
1014
1093
|
});
|
|
1015
1094
|
|
|
1016
1095
|
// Event: disconnected
|
|
1017
|
-
// Reconnect delays in seconds: 10, 30, 60, 120, 120, 120, 120
|
|
1018
|
-
const reconnectDelays = [10, 30, 60, 120, 120, 120, 120];
|
|
1019
1096
|
this.knxConnection.on(KNXClientEvents.disconnected, reason => {
|
|
1020
1097
|
if (this.connected) {
|
|
1021
1098
|
this.log.error(`Connection lost: ${reason}`);
|
|
@@ -1023,21 +1100,7 @@ class openknx extends utils.Adapter {
|
|
|
1023
1100
|
this.connected = false;
|
|
1024
1101
|
this.setState("info.connection", this.connected, true);
|
|
1025
1102
|
this.setState("info.busload", 0, true);
|
|
1026
|
-
|
|
1027
|
-
if (!this.stopping && this.reconnectCount < reconnectDelays.length) {
|
|
1028
|
-
const delay = reconnectDelays[this.reconnectCount];
|
|
1029
|
-
this.reconnectCount++;
|
|
1030
|
-
this.log.info(`Reconnect attempt ${this.reconnectCount}/${reconnectDelays.length} in ${delay}s...`);
|
|
1031
|
-
this.reconnectTimer = setTimeout(() => {
|
|
1032
|
-
try {
|
|
1033
|
-
this.startKnxStack();
|
|
1034
|
-
} catch (e) {
|
|
1035
|
-
this.log.error(`Reconnect failed: ${e.message || e}`);
|
|
1036
|
-
}
|
|
1037
|
-
}, delay * 1000);
|
|
1038
|
-
} else if (this.reconnectCount >= reconnectDelays.length) {
|
|
1039
|
-
this.log.error(`Giving up after ${reconnectDelays.length} reconnect attempts`);
|
|
1040
|
-
}
|
|
1103
|
+
this.scheduleReconnect();
|
|
1041
1104
|
});
|
|
1042
1105
|
|
|
1043
1106
|
// Event: error
|
|
@@ -1233,7 +1296,15 @@ class openknx extends utils.Adapter {
|
|
|
1233
1296
|
});
|
|
1234
1297
|
|
|
1235
1298
|
// Start connection
|
|
1236
|
-
|
|
1299
|
+
try {
|
|
1300
|
+
this.knxConnection.Connect();
|
|
1301
|
+
} catch (e) {
|
|
1302
|
+
if (e.message === "No client socket defined") {
|
|
1303
|
+
this.log.error(`Connect failed: KNX client socket was not created. Check that the configured network interface (${this.config.localInterface || "auto"}) is available and the protocol (${this.config.hostProtocol || "TunnelUDP"}) is correct.`);
|
|
1304
|
+
} else {
|
|
1305
|
+
this.log.error(`Connect failed: ${e.message}`);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1237
1308
|
}
|
|
1238
1309
|
|
|
1239
1310
|
countObjectsNotification(cnt_withDPT) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.openknx",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "ioBroker knx Adapter",
|
|
5
5
|
"author": "boellner",
|
|
6
6
|
"contributors": [
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@iobroker/adapter-core": "^3.3.2",
|
|
38
|
-
"@xmldom/xmldom": "^0.9.
|
|
39
|
-
"@zip.js/zip.js": "^2.8.
|
|
38
|
+
"@xmldom/xmldom": "^0.9.9",
|
|
39
|
+
"@zip.js/zip.js": "^2.8.26",
|
|
40
40
|
"ipaddr.js": "^2.3.0",
|
|
41
41
|
"knxultimate": "^5.4.0",
|
|
42
42
|
"xpath": "^0.0.34"
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@iobroker/eslint-config": "^2.2.0",
|
|
51
51
|
"@iobroker/testing": "^5.2.2",
|
|
52
52
|
"@tsconfig/node20": "^20.1.9",
|
|
53
|
-
"@types/node": "^
|
|
53
|
+
"@types/node": "^25.6.0",
|
|
54
54
|
"typescript": "~5.9.3"
|
|
55
55
|
},
|
|
56
56
|
"main": "main.js",
|