node-red-contrib-knx-ultimate 6.3.24 → 6.3.26
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/CHANGELOG.md +14 -1
- package/examples/IoT Bridge - Modbus Flex Adapter.json +361 -0
- package/examples/KNX AI - Conversational Control with Confirmation.json +4 -2
- package/examples/KNX AI - Summary Anomalies and Ask.json +4 -0
- package/examples/KNX AI - Telegrambot Direct Chat.json +7 -5
- package/nodes/knxUltimate-config.js +13 -4
- package/nodes/knxUltimateAI.html +310 -124
- package/nodes/knxUltimateAI.js +1742 -264
- package/nodes/knxUltimateIoTBridge.html +349 -20
- package/nodes/knxUltimateIoTBridge.js +474 -44
- package/nodes/locales/de/knxUltimateAI.html +33 -7
- package/nodes/locales/de/knxUltimateAI.json +34 -15
- package/nodes/locales/de/knxUltimateIoTBridge.html +28 -2
- package/nodes/locales/de/knxUltimateIoTBridge.json +20 -2
- package/nodes/locales/en/knxUltimateAI.html +33 -7
- package/nodes/locales/en/knxUltimateAI.json +34 -15
- package/nodes/locales/en/knxUltimateIoTBridge.html +28 -2
- package/nodes/locales/en/knxUltimateIoTBridge.json +20 -2
- package/nodes/locales/es/knxUltimateAI.html +33 -7
- package/nodes/locales/es/knxUltimateAI.json +34 -15
- package/nodes/locales/es/knxUltimateIoTBridge.html +28 -2
- package/nodes/locales/es/knxUltimateIoTBridge.json +20 -2
- package/nodes/locales/fr/knxUltimateAI.html +33 -7
- package/nodes/locales/fr/knxUltimateAI.json +34 -15
- package/nodes/locales/fr/knxUltimateIoTBridge.html +28 -2
- package/nodes/locales/fr/knxUltimateIoTBridge.json +20 -2
- package/nodes/locales/it/knxUltimateAI.html +33 -7
- package/nodes/locales/it/knxUltimateAI.json +34 -15
- package/nodes/locales/it/knxUltimateIoTBridge.html +28 -2
- package/nodes/locales/it/knxUltimateIoTBridge.json +20 -2
- package/nodes/locales/zh-CN/knxUltimateAI.html +33 -7
- package/nodes/locales/zh-CN/knxUltimateAI.json +34 -15
- package/nodes/locales/zh-CN/knxUltimateIoTBridge.html +28 -2
- package/nodes/locales/zh-CN/knxUltimateIoTBridge.json +20 -2
- package/nodes/plugins/knxUltimateAI-vue/assets/app.css +1 -1
- package/nodes/plugins/knxUltimateAI-vue/assets/app.js +4 -4
- package/nodes/utils/knxAiTelegramVoice.js +475 -0
- package/nodes/utils/knxAiWebAccess.js +631 -0
- package/package.json +3 -3
- package/resources/KNXAIChatAdapterMappings.js +96 -2
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
|
|
2
2
|
|
|
3
3
|
<script type="text/javascript">
|
|
4
|
+
function knxUltimateIoTBridgeHasValue(value) {
|
|
5
|
+
return value !== undefined && value !== null && !(typeof value === 'string' && value.trim() === '');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function knxUltimateIoTBridgeFiniteNumber(value) {
|
|
9
|
+
if (!knxUltimateIoTBridgeHasValue(value) || typeof value === 'boolean') return null;
|
|
10
|
+
const parsed = typeof value === 'number' ? value : Number(String(value).trim());
|
|
11
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function knxUltimateIoTBridgeFlexMappingsValid(mappings) {
|
|
15
|
+
if (!Array.isArray(mappings)) return false;
|
|
16
|
+
|
|
17
|
+
return mappings.every(function (mapping) {
|
|
18
|
+
if (!mapping || typeof mapping !== 'object') return false;
|
|
19
|
+
if (mapping.enabled === false || mapping.iotType !== 'modbus' || mapping.modbusMessageFormat !== 'flex') return true;
|
|
20
|
+
|
|
21
|
+
const unitId = knxUltimateIoTBridgeFiniteNumber(
|
|
22
|
+
knxUltimateIoTBridgeHasValue(mapping.modbusUnitId) ? mapping.modbusUnitId : 1
|
|
23
|
+
);
|
|
24
|
+
const address = knxUltimateIoTBridgeFiniteNumber(
|
|
25
|
+
knxUltimateIoTBridgeHasValue(mapping.modbusAddress) ? mapping.modbusAddress : mapping.target
|
|
26
|
+
);
|
|
27
|
+
const scale = knxUltimateIoTBridgeFiniteNumber(
|
|
28
|
+
knxUltimateIoTBridgeHasValue(mapping.scale) ? mapping.scale : 1
|
|
29
|
+
);
|
|
30
|
+
const offset = knxUltimateIoTBridgeFiniteNumber(
|
|
31
|
+
knxUltimateIoTBridgeHasValue(mapping.offset) ? mapping.offset : 0
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return Number.isInteger(unitId) && unitId >= 0 && unitId <= 255 &&
|
|
35
|
+
Number.isInteger(address) && address >= 0 && address <= 65535 &&
|
|
36
|
+
scale !== null && scale !== 0 && offset !== null;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function knxUltimateIoTBridgeSavedFlexField(originalMapping, key, preserveOriginal, currentValue) {
|
|
41
|
+
if (preserveOriginal) {
|
|
42
|
+
if (originalMapping && Object.prototype.hasOwnProperty.call(originalMapping, key)) {
|
|
43
|
+
return { present: true, value: originalMapping[key] };
|
|
44
|
+
}
|
|
45
|
+
return { present: false };
|
|
46
|
+
}
|
|
47
|
+
return { present: true, value: currentValue };
|
|
48
|
+
}
|
|
49
|
+
|
|
4
50
|
RED.nodes.registerType('knxUltimateIoTBridge', {
|
|
5
51
|
category: 'KNX Ultimate',
|
|
6
52
|
color: '#C7E9C0',
|
|
@@ -11,7 +57,13 @@
|
|
|
11
57
|
emitOnChangeOnly: { value: true },
|
|
12
58
|
readOnDeploy: { value: true },
|
|
13
59
|
acceptFlowInput: { value: true },
|
|
14
|
-
mappings: {
|
|
60
|
+
mappings: {
|
|
61
|
+
value: [],
|
|
62
|
+
validate: function (value) {
|
|
63
|
+
if (this.nodeMode === 'homeassistant') return true;
|
|
64
|
+
return knxUltimateIoTBridgeFlexMappingsValid(value);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
15
67
|
nodeMode: { value: 'homeassistant' },
|
|
16
68
|
haBusMode: { value: 'standalone' },
|
|
17
69
|
inputs: { value: 1 },
|
|
@@ -81,6 +133,24 @@
|
|
|
81
133
|
{ value: 'modbus', label: node._('knxUltimateIoTBridge.type.modbus') }
|
|
82
134
|
];
|
|
83
135
|
|
|
136
|
+
const modbusMessageFormats = [
|
|
137
|
+
{ value: 'flex', label: node._('knxUltimateIoTBridge.modbus.formatFlex') },
|
|
138
|
+
{ value: 'legacy', label: node._('knxUltimateIoTBridge.modbus.formatLegacy') }
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const modbusAreas = [
|
|
142
|
+
{ value: 'coil', label: node._('knxUltimateIoTBridge.modbus.areaCoil') },
|
|
143
|
+
{ value: 'discrete-input', label: node._('knxUltimateIoTBridge.modbus.areaDiscreteInput') },
|
|
144
|
+
{ value: 'holding-register', label: node._('knxUltimateIoTBridge.modbus.areaHoldingRegister') },
|
|
145
|
+
{ value: 'input-register', label: node._('knxUltimateIoTBridge.modbus.areaInputRegister') }
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
const modbusDataTypes = [
|
|
149
|
+
{ value: 'bool', label: node._('knxUltimateIoTBridge.modbus.dataTypeBool') },
|
|
150
|
+
{ value: 'uint16', label: node._('knxUltimateIoTBridge.modbus.dataTypeUint16') },
|
|
151
|
+
{ value: 'int16', label: node._('knxUltimateIoTBridge.modbus.dataTypeInt16') }
|
|
152
|
+
];
|
|
153
|
+
|
|
84
154
|
function createLabeledField(row, key, element, options = {}) {
|
|
85
155
|
const wrapper = $('<div/>', {
|
|
86
156
|
class: 'bridge-field-wrapper',
|
|
@@ -111,10 +181,34 @@
|
|
|
111
181
|
};
|
|
112
182
|
|
|
113
183
|
const container = $('#node-input-mapping-container');
|
|
184
|
+
const validateAllFlexRows = () => {
|
|
185
|
+
let valid = true;
|
|
186
|
+
try {
|
|
187
|
+
if ($('#node-input-nodeMode').val() === 'homeassistant') {
|
|
188
|
+
container.editableList('items').each(function () {
|
|
189
|
+
const validateRow = $(this).data('validate-flex');
|
|
190
|
+
if (typeof validateRow === 'function') validateRow();
|
|
191
|
+
});
|
|
192
|
+
} else {
|
|
193
|
+
container.editableList('items').each(function () {
|
|
194
|
+
const validateRow = $(this).data('validate-flex');
|
|
195
|
+
if (typeof validateRow === 'function' && !validateRow()) valid = false;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
} catch (error) { }
|
|
199
|
+
$('#node-dialog-ok').prop('disabled', !valid).toggleClass('disabled', !valid);
|
|
200
|
+
return valid;
|
|
201
|
+
};
|
|
202
|
+
const scheduleFlexValidation = () => setTimeout(validateAllFlexRows, 0);
|
|
203
|
+
|
|
114
204
|
container.css('min-height', '320px').css('min-width', '640px').editableList({
|
|
115
205
|
sortable: true,
|
|
116
206
|
removable: true,
|
|
207
|
+
removeItem: scheduleFlexValidation,
|
|
117
208
|
addItem: function (row, index, data) {
|
|
209
|
+
const originalMapping = data && data.mapping && typeof data.mapping === 'object'
|
|
210
|
+
? $.extend(true, {}, data.mapping)
|
|
211
|
+
: null;
|
|
118
212
|
const mapping = $.extend(true, {
|
|
119
213
|
id: '',
|
|
120
214
|
enabled: true,
|
|
@@ -126,13 +220,26 @@
|
|
|
126
220
|
target: '',
|
|
127
221
|
method: 'POST',
|
|
128
222
|
modbusFunction: 'writeHoldingRegister',
|
|
223
|
+
modbusMessageFormat: 'flex',
|
|
224
|
+
modbusUnitId: 1,
|
|
225
|
+
modbusArea: 'holding-register',
|
|
226
|
+
modbusDataType: 'uint16',
|
|
129
227
|
scale: 1,
|
|
130
228
|
offset: 0,
|
|
131
229
|
template: '',
|
|
132
230
|
property: '',
|
|
133
231
|
timeout: 0,
|
|
134
232
|
retry: 0
|
|
135
|
-
},
|
|
233
|
+
}, originalMapping || {});
|
|
234
|
+
|
|
235
|
+
// A missing format identifies the scalar contract used by older saved flows.
|
|
236
|
+
// Do not migrate it merely because the editor was opened and saved untouched.
|
|
237
|
+
if (originalMapping && originalMapping.iotType === 'modbus' && !Object.prototype.hasOwnProperty.call(originalMapping, 'modbusMessageFormat')) {
|
|
238
|
+
mapping.modbusMessageFormat = 'legacy';
|
|
239
|
+
}
|
|
240
|
+
if (!knxUltimateIoTBridgeHasValue(mapping.modbusAddress) && knxUltimateIoTBridgeHasValue(mapping.target)) {
|
|
241
|
+
mapping.modbusAddress = mapping.target;
|
|
242
|
+
}
|
|
136
243
|
|
|
137
244
|
if (!mapping.id || mapping.id === '') {
|
|
138
245
|
try {
|
|
@@ -206,7 +313,12 @@
|
|
|
206
313
|
const targetInput = createLabeledField(midRow, 'target', $('<input/>', {
|
|
207
314
|
type: 'text',
|
|
208
315
|
class: 'form-control bridge-target'
|
|
209
|
-
}).attr('placeholder', node._('knxUltimateIoTBridge.placeholders.target')).val(
|
|
316
|
+
}).attr('placeholder', node._('knxUltimateIoTBridge.placeholders.target')).val(
|
|
317
|
+
mapping.iotType === 'modbus' && mapping.modbusMessageFormat === 'flex'
|
|
318
|
+
? mapping.modbusAddress
|
|
319
|
+
: mapping.target
|
|
320
|
+
), { flex: '1 1 280px' });
|
|
321
|
+
targetInput.on('input change', function () { row.data('target-edited', true); });
|
|
210
322
|
|
|
211
323
|
const methodInput = createLabeledField(midRow, 'method', $('<input/>', {
|
|
212
324
|
type: 'text',
|
|
@@ -218,6 +330,20 @@
|
|
|
218
330
|
class: 'form-control bridge-modbus'
|
|
219
331
|
}).attr('placeholder', node._('knxUltimateIoTBridge.placeholders.modbusFunction')).val(mapping.modbusFunction), { width: '180px' });
|
|
220
332
|
|
|
333
|
+
const modbusFormatSelect = createLabeledField(midRow, 'modbusMessageFormat', buildSelect(modbusMessageFormats, mapping.modbusMessageFormat).addClass('bridge-modbus-format'), { width: '230px' });
|
|
334
|
+
|
|
335
|
+
const modbusUnitIdInput = createLabeledField(midRow, 'modbusUnitId', $('<input/>', {
|
|
336
|
+
type: 'number',
|
|
337
|
+
class: 'form-control bridge-modbus-unitid',
|
|
338
|
+
min: 0,
|
|
339
|
+
max: 255,
|
|
340
|
+
step: 1
|
|
341
|
+
}).val(mapping.modbusUnitId), { width: '110px' });
|
|
342
|
+
|
|
343
|
+
const modbusAreaSelect = createLabeledField(midRow, 'modbusArea', buildSelect(modbusAreas, mapping.modbusArea).addClass('bridge-modbus-area'), { width: '190px' });
|
|
344
|
+
|
|
345
|
+
const modbusDataTypeSelect = createLabeledField(midRow, 'modbusDataType', buildSelect(modbusDataTypes, mapping.modbusDataType).addClass('bridge-modbus-datatype'), { width: '170px' });
|
|
346
|
+
|
|
221
347
|
const scaleInput = createLabeledField(midRow, 'scale', $('<input/>', {
|
|
222
348
|
type: 'number',
|
|
223
349
|
class: 'form-control bridge-scale'
|
|
@@ -248,9 +374,63 @@
|
|
|
248
374
|
class: 'form-control bridge-property'
|
|
249
375
|
}).attr('placeholder', node._('knxUltimateIoTBridge.placeholders.property')).val(mapping.property), { flex: '1 1 260px' });
|
|
250
376
|
|
|
377
|
+
const modbusHint = $('<div/>', {
|
|
378
|
+
class: 'bridge-modbus-hint',
|
|
379
|
+
style: 'display:none; width:100%; margin-top:4px; padding:7px 9px; border-left:3px solid #5b8def; background:rgba(91,141,239,0.08); font-size:11px; line-height:1.4;'
|
|
380
|
+
}).appendTo(bottomRow);
|
|
381
|
+
|
|
382
|
+
const validateFlexInputs = () => {
|
|
383
|
+
const isFlexModbus = $('#node-input-nodeMode').val() !== 'homeassistant' && enabled.is(':checked') && typeSelect.val() === 'modbus' && modbusFormatSelect.val() === 'flex';
|
|
384
|
+
const validations = [
|
|
385
|
+
{
|
|
386
|
+
element: modbusUnitIdInput,
|
|
387
|
+
valid: (value) => {
|
|
388
|
+
const parsed = knxUltimateIoTBridgeFiniteNumber(value);
|
|
389
|
+
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 255;
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
element: targetInput,
|
|
394
|
+
valid: (value) => {
|
|
395
|
+
const parsed = knxUltimateIoTBridgeFiniteNumber(value);
|
|
396
|
+
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 65535;
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
element: scaleInput,
|
|
401
|
+
valid: (value) => {
|
|
402
|
+
const parsed = knxUltimateIoTBridgeFiniteNumber(value);
|
|
403
|
+
return parsed !== null && parsed !== 0;
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
element: offsetInput,
|
|
408
|
+
valid: (value) => knxUltimateIoTBridgeFiniteNumber(value) !== null
|
|
409
|
+
}
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
let valid = true;
|
|
413
|
+
validations.forEach(function (validation) {
|
|
414
|
+
const fieldValid = !isFlexModbus || validation.valid(validation.element.val());
|
|
415
|
+
validation.element.toggleClass('input-error', !fieldValid);
|
|
416
|
+
validation.element.attr('aria-invalid', fieldValid ? 'false' : 'true');
|
|
417
|
+
valid = valid && fieldValid;
|
|
418
|
+
});
|
|
419
|
+
return valid;
|
|
420
|
+
};
|
|
421
|
+
|
|
251
422
|
const updateChannelPresentation = (channel) => {
|
|
423
|
+
const targetWrapper = targetInput.data('bridgeWrapper');
|
|
252
424
|
const methodWrapper = methodInput.data('bridgeWrapper');
|
|
253
425
|
const modbusWrapper = modbusInput.data('bridgeWrapper');
|
|
426
|
+
const modbusFormatWrapper = modbusFormatSelect.data('bridgeWrapper');
|
|
427
|
+
const modbusUnitIdWrapper = modbusUnitIdInput.data('bridgeWrapper');
|
|
428
|
+
const modbusAreaWrapper = modbusAreaSelect.data('bridgeWrapper');
|
|
429
|
+
const modbusDataTypeWrapper = modbusDataTypeSelect.data('bridgeWrapper');
|
|
430
|
+
const timeoutWrapper = timeoutInput.data('bridgeWrapper');
|
|
431
|
+
const retryWrapper = retryInput.data('bridgeWrapper');
|
|
432
|
+
const templateWrapper = templateInput.data('bridgeWrapper');
|
|
433
|
+
const propertyWrapper = propertyInput.data('bridgeWrapper');
|
|
254
434
|
const targetLabel = targetInput.data('bridgeLabel');
|
|
255
435
|
const methodLabel = methodInput.data('bridgeLabel');
|
|
256
436
|
const modbusLabel = modbusInput.data('bridgeLabel');
|
|
@@ -274,16 +454,40 @@
|
|
|
274
454
|
return placeholder;
|
|
275
455
|
};
|
|
276
456
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
457
|
+
const isRest = channel === 'rest';
|
|
458
|
+
const isModbus = channel === 'modbus';
|
|
459
|
+
const isFlexModbus = isModbus && modbusFormatSelect.val() === 'flex';
|
|
460
|
+
const isBitArea = ['coil', 'discrete-input'].includes(modbusAreaSelect.val());
|
|
461
|
+
const isReadOnlyArea = ['discrete-input', 'input-register'].includes(modbusAreaSelect.val());
|
|
462
|
+
|
|
463
|
+
targetWrapper.show();
|
|
464
|
+
methodWrapper.toggle(isRest);
|
|
465
|
+
modbusWrapper.toggle(isModbus && !isFlexModbus);
|
|
466
|
+
modbusFormatWrapper.toggle(isModbus);
|
|
467
|
+
modbusUnitIdWrapper.toggle(isFlexModbus);
|
|
468
|
+
modbusAreaWrapper.toggle(isFlexModbus);
|
|
469
|
+
modbusDataTypeWrapper.toggle(isFlexModbus);
|
|
470
|
+
timeoutWrapper.toggle(isRest || (isModbus && !isFlexModbus));
|
|
471
|
+
retryWrapper.toggle(isRest || (isModbus && !isFlexModbus));
|
|
472
|
+
templateWrapper.toggle(!isFlexModbus);
|
|
473
|
+
propertyWrapper.toggle(!isFlexModbus);
|
|
474
|
+
if (isFlexModbus && isReadOnlyArea) directionSelect.val('iot-to-knx');
|
|
475
|
+
directionSelect.prop('disabled', isFlexModbus && isReadOnlyArea);
|
|
476
|
+
|
|
477
|
+
if (isFlexModbus) {
|
|
478
|
+
targetInput.attr({ type: 'number', min: 0, max: 65535, step: 1 });
|
|
479
|
+
if (isBitArea) modbusDataTypeSelect.val('bool');
|
|
480
|
+
modbusDataTypeSelect.prop('disabled', isBitArea);
|
|
481
|
+
const hints = [
|
|
482
|
+
node._('knxUltimateIoTBridge.modbus.zeroBasedHint'),
|
|
483
|
+
node._('knxUltimateIoTBridge.modbus.flexHint')
|
|
484
|
+
];
|
|
485
|
+
if (isReadOnlyArea) hints.push(node._('knxUltimateIoTBridge.modbus.readOnlyHint'));
|
|
486
|
+
modbusHint.text(hints.join(' ')).show();
|
|
285
487
|
} else {
|
|
286
|
-
|
|
488
|
+
targetInput.attr('type', 'text').removeAttr('min max step');
|
|
489
|
+
modbusDataTypeSelect.prop('disabled', false);
|
|
490
|
+
modbusHint.hide();
|
|
287
491
|
}
|
|
288
492
|
|
|
289
493
|
targetLabel.text(translateVariant('target', channel, 'target'));
|
|
@@ -291,15 +495,56 @@
|
|
|
291
495
|
|
|
292
496
|
methodLabel.text(translateVariant('method', channel, 'method'));
|
|
293
497
|
modbusLabel.text(translateVariant('modbusFunction', channel, 'modbusFunction'));
|
|
498
|
+
scheduleFlexValidation();
|
|
294
499
|
};
|
|
295
500
|
|
|
501
|
+
let previousChannel = typeSelect.val();
|
|
296
502
|
typeSelect.on('change', function () {
|
|
297
|
-
|
|
503
|
+
row.data('channel-edited', true);
|
|
504
|
+
const channel = $(this).val();
|
|
505
|
+
if (channel === 'modbus' && previousChannel !== 'modbus') modbusFormatSelect.val('flex');
|
|
506
|
+
previousChannel = channel;
|
|
507
|
+
updateChannelPresentation(channel);
|
|
298
508
|
});
|
|
299
509
|
|
|
300
|
-
|
|
510
|
+
modbusFormatSelect.on('change', function () {
|
|
511
|
+
row.data('modbus-format-edited', true);
|
|
512
|
+
updateChannelPresentation(typeSelect.val());
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
modbusAreaSelect.on('change', function () {
|
|
516
|
+
row.data('modbus-area-edited', true);
|
|
517
|
+
if (['discrete-input', 'input-register'].includes($(this).val())) directionSelect.val('iot-to-knx');
|
|
518
|
+
updateChannelPresentation(typeSelect.val());
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
modbusDataTypeSelect.on('change', function () {
|
|
522
|
+
row.data('modbus-datatype-edited', true);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
modbusUnitIdInput.on('input change', function () {
|
|
526
|
+
row.data('modbus-unitid-edited', true);
|
|
527
|
+
scheduleFlexValidation();
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
targetInput.on('input change', scheduleFlexValidation);
|
|
531
|
+
|
|
532
|
+
enabled.on('change', scheduleFlexValidation);
|
|
533
|
+
|
|
534
|
+
scaleInput.on('input change', function () {
|
|
535
|
+
row.data('scale-edited', true);
|
|
536
|
+
scheduleFlexValidation();
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
offsetInput.on('input change', function () {
|
|
540
|
+
row.data('offset-edited', true);
|
|
541
|
+
scheduleFlexValidation();
|
|
542
|
+
});
|
|
301
543
|
|
|
302
544
|
row.data('mapping-id', mapping.id || '');
|
|
545
|
+
row.data('original-mapping', originalMapping);
|
|
546
|
+
row.data('validate-flex', validateFlexInputs);
|
|
547
|
+
updateChannelPresentation(typeSelect.val());
|
|
303
548
|
}
|
|
304
549
|
});
|
|
305
550
|
|
|
@@ -316,6 +561,7 @@
|
|
|
316
561
|
$('.iot-mode-row').toggle(!ha);
|
|
317
562
|
// The flow-mode hint is only relevant while the "flow msg" bus mode is picked.
|
|
318
563
|
$('.ha-flow-hint').toggle(ha && $('#node-input-haBusMode').val() === 'flow');
|
|
564
|
+
scheduleFlexValidation();
|
|
319
565
|
}
|
|
320
566
|
$('#node-input-nodeMode').on('change', refreshMode);
|
|
321
567
|
$('#node-input-haBusMode').on('change', refreshMode);
|
|
@@ -542,20 +788,34 @@
|
|
|
542
788
|
} catch (error) { }
|
|
543
789
|
},
|
|
544
790
|
oneditsave: function () {
|
|
791
|
+
$('#node-dialog-ok').prop('disabled', false).removeClass('disabled');
|
|
545
792
|
const node = this;
|
|
546
793
|
const items = $('#node-input-mapping-container').editableList('items');
|
|
547
794
|
node.mappings = [];
|
|
548
795
|
items.each(function () {
|
|
549
796
|
const row = $(this);
|
|
550
|
-
|
|
797
|
+
const originalMapping = row.data('original-mapping');
|
|
798
|
+
const channel = row.find('.bridge-type').val();
|
|
799
|
+
const target = row.find('.bridge-target').val();
|
|
800
|
+
const modbusFormat = row.find('.bridge-modbus-format').val() || 'legacy';
|
|
801
|
+
const modbusArea = row.find('.bridge-modbus-area').val() || 'holding-register';
|
|
802
|
+
const hasOriginalField = (key) => originalMapping && Object.prototype.hasOwnProperty.call(originalMapping, key);
|
|
803
|
+
const parseEditorNumber = (value) => {
|
|
804
|
+
if (!knxUltimateIoTBridgeHasValue(value)) return '';
|
|
805
|
+
const parsed = knxUltimateIoTBridgeFiniteNumber(value);
|
|
806
|
+
return parsed === null ? value : parsed;
|
|
807
|
+
};
|
|
808
|
+
const savedMapping = {
|
|
551
809
|
id: row.data('mapping-id') || '',
|
|
552
810
|
enabled: row.find('.bridge-enabled').is(':checked'),
|
|
553
811
|
label: row.find('.bridge-label').val(),
|
|
554
812
|
ga: row.find('.bridge-ga').val(),
|
|
555
813
|
dpt: row.find('.bridge-dpt').val(),
|
|
556
814
|
direction: row.find('.bridge-direction').val(),
|
|
557
|
-
iotType:
|
|
558
|
-
target: row.
|
|
815
|
+
iotType: channel,
|
|
816
|
+
target: originalMapping && originalMapping.iotType === 'modbus' && originalMapping.modbusMessageFormat === 'flex' && channel === 'modbus' && modbusFormat === 'flex' && !row.data('target-edited')
|
|
817
|
+
? originalMapping.target
|
|
818
|
+
: target,
|
|
559
819
|
method: row.find('.bridge-method').val(),
|
|
560
820
|
modbusFunction: row.find('.bridge-modbus').val(),
|
|
561
821
|
scale: row.find('.bridge-scale').val(),
|
|
@@ -564,7 +824,63 @@
|
|
|
564
824
|
retry: row.find('.bridge-retry').val(),
|
|
565
825
|
template: row.find('.bridge-template').val(),
|
|
566
826
|
property: row.find('.bridge-property').val()
|
|
567
|
-
}
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
if (channel === 'modbus') {
|
|
830
|
+
const originalHadFormat = hasOriginalField('modbusMessageFormat');
|
|
831
|
+
if (!originalMapping || originalHadFormat || modbusFormat === 'flex') savedMapping.modbusMessageFormat = modbusFormat;
|
|
832
|
+
|
|
833
|
+
if (modbusFormat === 'flex') {
|
|
834
|
+
const sameOriginalFlexMapping = originalMapping &&
|
|
835
|
+
originalMapping.iotType === 'modbus' &&
|
|
836
|
+
originalMapping.modbusMessageFormat === 'flex' &&
|
|
837
|
+
!row.data('channel-edited') &&
|
|
838
|
+
!row.data('modbus-format-edited');
|
|
839
|
+
const saveFlexField = (key, editedDataKey, currentValue, parser, relatedFieldEdited) => {
|
|
840
|
+
const parsedValue = parser ? parser(currentValue) : currentValue;
|
|
841
|
+
const field = knxUltimateIoTBridgeSavedFlexField(
|
|
842
|
+
originalMapping,
|
|
843
|
+
key,
|
|
844
|
+
sameOriginalFlexMapping && !row.data(editedDataKey) && !relatedFieldEdited,
|
|
845
|
+
parsedValue
|
|
846
|
+
);
|
|
847
|
+
if (field.present) savedMapping[key] = field.value;
|
|
848
|
+
else delete savedMapping[key];
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
saveFlexField('modbusUnitId', 'modbus-unitid-edited', row.find('.bridge-modbus-unitid').val(), parseEditorNumber);
|
|
852
|
+
saveFlexField('modbusAddress', 'target-edited', target, parseEditorNumber);
|
|
853
|
+
saveFlexField('modbusArea', 'modbus-area-edited', modbusArea);
|
|
854
|
+
saveFlexField(
|
|
855
|
+
'modbusDataType',
|
|
856
|
+
'modbus-datatype-edited',
|
|
857
|
+
row.find('.bridge-modbus-datatype').val(),
|
|
858
|
+
null,
|
|
859
|
+
row.data('modbus-area-edited')
|
|
860
|
+
);
|
|
861
|
+
saveFlexField('scale', 'scale-edited', row.find('.bridge-scale').val(), parseEditorNumber);
|
|
862
|
+
saveFlexField('offset', 'offset-edited', row.find('.bridge-offset').val(), parseEditorNumber);
|
|
863
|
+
|
|
864
|
+
if (!sameOriginalFlexMapping || row.data('modbus-area-edited')) {
|
|
865
|
+
savedMapping.modbusFunction = {
|
|
866
|
+
coil: 'writeCoil',
|
|
867
|
+
'discrete-input': 'readDiscreteInputs',
|
|
868
|
+
'holding-register': 'writeHoldingRegister',
|
|
869
|
+
'input-register': 'readInputRegisters'
|
|
870
|
+
}[modbusArea];
|
|
871
|
+
}
|
|
872
|
+
} else if (originalMapping) {
|
|
873
|
+
['modbusUnitId', 'modbusAddress', 'modbusArea', 'modbusDataType'].forEach(function (key) {
|
|
874
|
+
if (Object.prototype.hasOwnProperty.call(originalMapping, key)) savedMapping[key] = originalMapping[key];
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
} else if (originalMapping) {
|
|
878
|
+
['modbusMessageFormat', 'modbusUnitId', 'modbusAddress', 'modbusArea', 'modbusDataType'].forEach(function (key) {
|
|
879
|
+
if (Object.prototype.hasOwnProperty.call(originalMapping, key)) savedMapping[key] = originalMapping[key];
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
node.mappings.push(savedMapping);
|
|
568
884
|
});
|
|
569
885
|
// Serialize Home Assistant composite entities (cover / climate).
|
|
570
886
|
try {
|
|
@@ -624,6 +940,9 @@
|
|
|
624
940
|
RED.sidebar.show('info');
|
|
625
941
|
} catch (error) { }
|
|
626
942
|
},
|
|
943
|
+
oneditcancel: function () {
|
|
944
|
+
$('#node-dialog-ok').prop('disabled', false).removeClass('disabled');
|
|
945
|
+
},
|
|
627
946
|
oneditresize: function (size) {
|
|
628
947
|
const rows = $('#dialog-form>div:not(.node-input-mapping-container-row)');
|
|
629
948
|
let height = size.height;
|
|
@@ -764,11 +1083,21 @@ Configure bidirectional maps between KNX group addresses and IoT backends such a
|
|
|
764
1083
|
|
|
765
1084
|
### Mapping options
|
|
766
1085
|
- **Direction**: choose between KNX→IoT, IoT→KNX or bidirectional.
|
|
767
|
-
- **Channel type**: pick MQTT, REST or Modbus. The
|
|
1086
|
+
- **Channel type**: pick MQTT, REST or Modbus. The editor shows only the fields used by that protocol.
|
|
768
1087
|
- **Template**: optional string; placeholders `{{value}}`, `{{ga}}`, `{{target}}`, `{{label}}`, `{{type}}`, `{{isoTimestamp}}` are replaced at runtime.
|
|
769
1088
|
- **Scale & Offset**: numeric transformation applied on KNX→IoT. For IoT→KNX the inverse is used.
|
|
770
1089
|
- **Timeout/Retries**: retained for flow logic; the node does not execute external requests but exposes the desired values to downstream nodes.
|
|
771
1090
|
|
|
1091
|
+
### Modbus adapter
|
|
1092
|
+
|
|
1093
|
+
The bridge does not open a Modbus connection. Install a version of `node-red-contrib-modbus` compatible with your Node.js/Node-RED host and let its nodes own TCP/RTU, polling, queues and reconnection.
|
|
1094
|
+
|
|
1095
|
+
- Choose **node-red-contrib-modbus (Flex)** for new mappings. Existing mappings without this setting retain their legacy scalar message unchanged.
|
|
1096
|
+
- Configure Unit ID, area, zero-based protocol address and Boolean/unsigned 16-bit/signed 16-bit data format. Vendor references such as `40001` are not converted automatically because their notation is ambiguous.
|
|
1097
|
+
- A KNX `GroupValue_Write` produces the exact `modbus-flex-write` input: `msg.payload = {value, fc, unitid, address, quantity}`. KNX read responses never become Modbus writes.
|
|
1098
|
+
- Wire the data output of `modbus-read` or `modbus-flex-getter` to this node. It reads the request from `msg.input.payload` (Modbus Read) or `msg.modbusRequest` (Flex Getter), accepts values from `msg.payload`/`msg.values`, and writes only changed values to KNX. Flex Write acknowledgements are ignored as state.
|
|
1099
|
+
- Coil and Holding Register support writes (FC5/FC6). Discrete Input and Input Register are read-only (FC2/FC4). This first adapter intentionally maps one bit or one 16-bit register; use Scale/Offset for fixed-point values.
|
|
1100
|
+
|
|
772
1101
|
### Tips
|
|
773
1102
|
- Place HTTP or MQTT nodes after the bridge outputs to perform the actual transport.
|
|
774
1103
|
- Use `msg.bridge.id` to route acknowledgements or correlate responses.
|
|
@@ -809,4 +1138,4 @@ Covers and thermostats group several group addresses into one Home Assistant ent
|
|
|
809
1138
|
|
|
810
1139
|
Datapoint types come from the imported ETS list when available, otherwise from the KNX defaults shown above. For reliable status feedback, those group addresses should be present in the ETS import.
|
|
811
1140
|
|
|
812
|
-
</script>
|
|
1141
|
+
</script>
|