UNPKG

node-red-contrib-knx-ultimate

Version:

Control your KNX and KNX Secure intallation via Node-Red! A bunch of KNX nodes, with integrated Philips HUE control and ETS group address importer. Easy to use and highly configurable.

1,000 lines (934 loc) 64.7 kB
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script> <script type="text/javascript"> RED.nodes.registerType('knxUltimate-config', { category: 'config', defaults: { host: { value: "224.0.23.12", required: true }, port: { value: 3671, required: true, validate: RED.validators.number() }, // the KNX physical address we'd like to use physAddr: { value: "15.15.22", required: false }, hostProtocol: { value: "Auto", required: false }, // TunnelUDP/TunnelTCP/Multicast // enable this option to suppress the acknowledge flag with outgoing L_Data.req requests. LoxOne needs this suppressACKRequest: { value: false }, csv: { value: "", required: false }, KNXEthInterface: { value: "Auto" }, KNXEthInterfaceManuallyInput: { value: "" }, stopETSImportIfNoDatapoint: { value: "fake" }, loglevel: { value: "error" }, name: { value: "KNX Gateway" }, delaybetweentelegrams: { value: 25, required: false, validate: RED.validators.number() }, ignoreTelegramsWithRepeatedFlag: { value: false, required: false }, keyringFileXML: { value: "" }, knxSecureSelected: { value: false }, secureCredentialsMode: { value: "keyring" }, tunnelIASelection: { value: "Auto" }, tunnelIA: { value: "" }, tunnelInterfaceIndividualAddress: { value: "" }, tunnelUserPassword: { value: "" }, tunnelUserId: { value: "" }, autoReconnect: { value: "yes" }, statusUpdateThrottle: { value: "0" } }, credentials: { keyringFilePassword: { type: "password" } }, oneditprepare: function () { // Go to the help panel try { RED.sidebar.show("help"); } catch (error) { } var node = this; // TIMER BLINK #################################################### let blinkStatus = 2; let timerBlinkBackground; function blinkBackgroundArray(_arrayElementIDwithHashAtTheBeginning) { if (timerBlinkBackground !== undefined) clearInterval(timerBlinkBackground); timerBlinkBackground = setInterval(() => { for (let index = 0; index < _arrayElementIDwithHashAtTheBeginning.length; index++) { const _elementIDwithHashAtTheBeginning = _arrayElementIDwithHashAtTheBeginning[index]; if (isEven(blinkStatus)) $(_elementIDwithHashAtTheBeginning).css("background-color", "lightgreen"); if (!isEven(blinkStatus)) $(_elementIDwithHashAtTheBeginning).css("background-color", ""); } blinkStatus += 1; if (blinkStatus >= 14) { clearInterval(timerBlinkBackground); blinkStatus = 2; for (let index = 0; index < _arrayElementIDwithHashAtTheBeginning.length; index++) { const _elementIDwithHashAtTheBeginning = _arrayElementIDwithHashAtTheBeginning[index]; $(_elementIDwithHashAtTheBeginning).css("background-color", ""); } } }, 100); } function isEven(n) { return (n % 2 == 0); } // ################################################################ // 22/07/2021 Main tab // Initialize tabs strictly from saved config, not from keyring presence $("#tabsMain").tabs({ active: (node.knxSecureSelected === true || node.knxSecureSelected === 'true') ? 1 : 0, activate: function (event, ui) { node.knxSecureSelected = $(ui.newTab).index() === 1; try { updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); enforceProtocolFromIP(); } catch (e) { } } }); // Ensure the correct tab is enforced after all UI init try { const initialActive = (node.knxSecureSelected === true || node.knxSecureSelected === 'true') ? 1 : 0; setTimeout(() => { $("#tabsMain").tabs("option", "active", initialActive); updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); enforceProtocolFromIP(); }, 0); } catch (e) { } // Keyring ACE editor setup try { const initialKeyring = $("#node-config-input-keyringFileXML").val() || (node.keyringFileXML || ''); node._keyringEditor = RED.editor.createEditor({ id: 'node-config-input-keyringFileXML-editor', mode: 'ace/mode/xml', value: initialKeyring }); try { node._keyringEditor.getSession().setUseWrapMode(true); } catch (e) { } } catch (e) { } // ETS CSV editor setup (plain text) try { const initialCSV = $("#node-config-input-csv").val() || ''; node._csvEditor = RED.editor.createEditor({ id: 'node-config-input-csv-editor', mode: 'ace/mode/text', value: initialCSV }); try { node._csvEditor.getSession().setUseWrapMode(true); } catch (e) { } } catch (e) { } function getSecureCredentialsMode() { try { const sel = $("#node-config-input-secureCredentialsMode").val(); return (typeof sel === 'string' && sel.length > 0) ? sel : 'keyring'; } catch (e) { return 'keyring'; } } function isSecureTabActive() { try { return $("#tabsMain").tabs("option", "active") === 1; } catch (e) { return !!(typeof node.knxSecureSelected !== 'undefined' ? node.knxSecureSelected : false); } } function parseIPv4Address(str) { if (typeof str !== 'string') return null; const parts = str.trim().split('.'); if (parts.length !== 4) return null; const octets = []; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (!/^\d+$/.test(part)) return null; const value = Number(part); if (value < 0 || value > 255) return null; octets.push(value); } return octets; } function isMulticastIPv4(octets) { if (!Array.isArray(octets) || octets.length !== 4) return false; const first = octets[0]; return first >= 224 && first <= 239; } const NETMASK_BIT_LOOKUP = (() => { const lookup = new Array(256); for (let i = 0; i < 256; i++) { let bits = 0; let value = i; while (value > 0) { bits += value & 1; value >>= 1; } lookup[i] = bits; } return lookup; })(); function countNetmaskBits(maskOctets) { if (!Array.isArray(maskOctets) || maskOctets.length !== 4) return 0; let total = 0; for (let i = 0; i < 4; i++) { const oct = maskOctets[i]; if (!Number.isInteger(oct) || oct < 0 || oct > 255) return 0; total += NETMASK_BIT_LOOKUP[oct]; } return total; } function computeIPv4NetworkKey(ipOctets, maskOctets) { if (!Array.isArray(ipOctets) || ipOctets.length !== 4 || !Array.isArray(maskOctets) || maskOctets.length !== 4) return null; const parts = []; for (let i = 0; i < 4; i++) { const ipVal = ipOctets[i]; const maskVal = maskOctets[i]; if (!Number.isInteger(ipVal) || ipVal < 0 || ipVal > 255 || !Number.isInteger(maskVal) || maskVal < 0 || maskVal > 255) return null; parts.push(ipVal & maskVal); } return parts.join('.'); } function normalizeTunnelIAValue(value) { if (typeof value !== 'string') return ''; const trimmed = value.trim(); return (trimmed && trimmed !== 'undefined') ? trimmed : ''; } function setTunnelIAValue(value) { const normalized = normalizeTunnelIAValue(value || ''); try { $("#node-config-input-tunnelIA").val(normalized); } catch (e) { } try { $("#node-config-input-tunnelInterfaceIndividualAddress").val(normalized); } catch (e) { } } function syncTunnelIAFromManual() { try { setTunnelIAValue($("#node-config-input-tunnelInterfaceIndividualAddress").val()); } catch (e) { } } function syncTunnelIAFromKeyring() { try { setTunnelIAValue($("#node-config-input-tunnelIA").val()); } catch (e) { } } function enforceProtocolFromIP() { try { const ipTyped = String($("#node-config-input-host").val() || '').trim(); if (!ipTyped) return; const octets = parseIPv4Address(ipTyped); if (!octets || isMulticastIPv4(octets)) return; const autoProto = isSecureTabActive() ? 'TunnelTCP' : 'TunnelUDP'; const $sel = $("#node-config-input-hostProtocol"); if ($sel.val() !== autoProto) { $sel.val(autoProto); updateTunnelIAVisibility(); updateSecureMulticastHint(); updatePhysAddrVisibility(); } } catch (e) { } } function updateSecureCredentialMode() { try { const mode = getSecureCredentialsMode(); const showKeyringFields = (mode === 'keyring' || mode === 'combined'); const showKeyringIAControls = (mode === 'keyring'); $("#secureKeyringFields").toggle(showKeyringFields); if (!showKeyringIAControls) { $("#rowTunnelIASelection").hide(); $("#divTunnelIA").hide(); } } catch (e) { } updateTunnelIAVisibility(); enforceProtocolFromIP(); } // Helper: show/hide Tunnel IA controls depending on mode function updateTunnelIAVisibility() { try { const isSecure = (node.knxSecureSelected === true || node.knxSecureSelected === 'true'); const hostProtocol = $("#node-config-input-hostProtocol").val(); const ip = $("#node-config-input-host").val(); const isMulticast = (hostProtocol === 'Multicast') || (ip === '224.0.23.12'); const mode = getSecureCredentialsMode(); const keyringMode = (mode === 'keyring'); const manualMode = (mode === 'manual' || mode === 'combined'); const showTunnelIA = isSecure && !isMulticast && keyringMode; if (showTunnelIA) { $("#rowTunnelIASelection").show(); if ($("#node-config-input-tunnelIASelection").val() === 'Manual') { $("#divTunnelIA").show(); } else { $("#divTunnelIA").hide(); } } else { $("#rowTunnelIASelection").hide(); $("#divTunnelIA").hide(); } // Allow manual/combined credentials to surface even before the user selects a unicast IP. const showManualTunnelFields = isSecure && manualMode; $("#rowTunnelInterfaceIndividualAddress").toggle(showManualTunnelFields); $("#rowTunnelUserPassword").toggle(showManualTunnelFields); $("#rowTunnelUserId").toggle(showManualTunnelFields); if (!showManualTunnelFields) { try { $("#node-config-input-tunnelInterfaceIndividualAddress").removeClass('input-error'); } catch (e) { } } } catch (e) { } } // Helper: ensure Physical Address row is visible (required also in multicast) function updatePhysAddrVisibility() { try { $("#rowPhysAddr").show(); } catch (e) { } } // Helper: show contextual hint for Secure + Multicast (Data Secure senders requirement) let isSecureMulticastHintOpen = false; function updateSecureMulticastHint() { try { const isSecure = (node.knxSecureSelected === true || node.knxSecureSelected === 'true'); const hostProtocol = $("#node-config-input-hostProtocol").val(); const ip = $("#node-config-input-host").val(); const isMulticast = (hostProtocol === 'Multicast') || (ip === '224.0.23.12'); if (isSecure && isMulticast) { $("#hintSecureMulticastToggle").show(); $("#hintSecureMulticast").toggle(isSecureMulticastHintOpen); } else { $("#hintSecureMulticastToggle").hide(); $("#hintSecureMulticast").hide(); isSecureMulticastHintOpen = false; } } catch (e) { } } // Toggle behavior for the hint try { $(document).on('click', '#hintSecureMulticastToggle', function () { isSecureMulticastHintOpen = !isSecureMulticastHintOpen; $("#hintSecureMulticast").toggle(isSecureMulticastHintOpen); }); } catch (e) { } // Helper: suggest a coherent physAddr from a gateway IA string like A.L.D function suggestPhysAddrFromGatewayIA(gatewayIA) { let prefix = ''; let gwIaParts = []; if (typeof gatewayIA === 'string' && gatewayIA.includes('.')) { gwIaParts = gatewayIA.split('.'); prefix = `${gwIaParts[0]}.${gwIaParts[1]}`; } else { const currentPhys = $("#node-config-input-physAddr").val() || ''; const m = String(currentPhys).trim().match(/^(\d{1,2})\.(\d{1,3})/); prefix = m ? `${m[1]}.${m[2]}` : '15.15'; } let rnd = 200 + Math.floor(Math.random() * 55); // 200..254 try { const gwLast = parseInt(gwIaParts[2]); if (!isNaN(gwLast) && gwLast >= 200 && gwLast <= 254 && rnd === gwLast) { rnd = (gwLast < 254 ? gwLast + 1 : 200); } } catch (e) { } const phys = `${prefix}.${rnd}`; $("#node-config-input-physAddr").val(phys); } // Helper: enforce host protocol depending on Secure/Plain and IP function enforceHostProtocol(isSecure, ip) { try { const $sel = $("#node-config-input-hostProtocol"); const isMulticast = (ip === '224.0.23.12'); const forced = isSecure ? (isMulticast ? 'Multicast' : 'TunnelTCP') : (isMulticast ? 'Multicast' : 'TunnelUDP'); // Set value $sel.val(forced); // Force selection and prevent changes for both Secure and Plain. $sel.prop('disabled', true); // Reset options visibility so that UI remains consistent if user reselects $sel.find("option").show().prop('disabled', false); if (isSecure) { // Keep only valid choices visible conceptually (info only) $sel.find("option[value='TunnelUDP']").prop('disabled', true).hide(); $sel.find("option[value='Auto']").prop('disabled', true).hide(); } else { // For plain, hide TunnelTCP which is not used $sel.find("option[value='TunnelTCP']").prop('disabled', true).hide(); $sel.find("option[value='Auto']").prop('disabled', true).hide(); } } catch (e) { } updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); } // List of IA from keyring and loader let aTunnelIAs = []; function populateTunnelIAList() { try { const mode = getSecureCredentialsMode(); const keyringMode = (mode === 'keyring'); if (!keyringMode) { aTunnelIAs = []; try { const $ia = $("#node-config-input-tunnelIA"); if ($ia.data('ui-autocomplete')) { $ia.autocomplete('option', 'source', aTunnelIAs); } } catch (e) { } return; } const keyring = $("#node-config-input-keyringFileXML").val(); const pwd = $("#node-config-input-keyringFilePassword").val(); const params = { serverId: node.id }; if (typeof keyring === 'string' && keyring.trim() !== '') params.keyring = keyring.trim(); if (typeof pwd === 'string' && pwd.trim() !== '') params.pwd = pwd.trim(); $.getJSON('knxUltimateKeyringInterfaces', params, (data) => { aTunnelIAs = []; if (Array.isArray(data)) { data.forEach(item => { const ia = item.ia || ''; const userId = (item.userId !== undefined && item.userId !== null) ? item.userId : ''; if (ia) { aTunnelIAs.push({ label: userId !== '' ? `${ia} (user ${userId})` : ia, value: ia }); } }); } try { const $ia = $("#node-config-input-tunnelIA"); if ($ia.data('ui-autocomplete')) { $ia.autocomplete('option', 'source', aTunnelIAs); } } catch (e) { } }); } catch (error) { } } // Secure: Tunnel IA selection try { const storedTunnelIA = (() => { const manualStored = normalizeTunnelIAValue(node.tunnelInterfaceIndividualAddress); const keyringStored = normalizeTunnelIAValue(node.tunnelIA); return manualStored || keyringStored; })(); setTunnelIAValue(storedTunnelIA); $("#node-config-input-tunnelIASelection").val(typeof node.tunnelIASelection === 'undefined' ? 'Auto' : node.tunnelIASelection); $("#node-config-input-tunnelIA").on('change input', syncTunnelIAFromKeyring); const toggleTunnelIA = () => { if ($("#node-config-input-tunnelIASelection").val() === 'Manual') { $("#divTunnelIA").show(); populateTunnelIAList(); } else { $("#divTunnelIA").hide(); } }; $("#node-config-input-tunnelIASelection").on('change', function () { toggleTunnelIA(); updateTunnelIAVisibility(); updateSecureMulticastHint(); }); toggleTunnelIA(); updateTunnelIAVisibility(); updateSecureMulticastHint(); updatePhysAddrVisibility(); // Also refresh IA list when user edits keyring/pwd $("#node-config-input-keyringFileXML").on('change', function () { if ($("#node-config-input-tunnelIASelection").val() === 'Manual') populateTunnelIAList(); }); $("#node-config-input-keyringFilePassword").on('change', function () { if ($("#node-config-input-tunnelIASelection").val() === 'Manual') populateTunnelIAList(); }); // Autocomplete on IA input using the discovered IA list try { $("#node-config-input-tunnelIA").autocomplete({ minLength: 0, source: function (request, response) { response(aTunnelIAs); }, focus: function (event, ui) { setTunnelIAValue(ui.item.value); return false; }, select: function (event, ui) { setTunnelIAValue(ui.item.value); return false; } }).on('focus click', function () { // Show dropdown on focus/click $(this).autocomplete('search', ''); }); } catch (e) { } } catch (error) { } try { $("#node-config-input-secureCredentialsMode").val(typeof node.secureCredentialsMode === 'undefined' ? 'keyring' : node.secureCredentialsMode); $("#node-config-input-tunnelInterfaceIndividualAddress").on('change input', syncTunnelIAFromManual); $("#node-config-input-tunnelUserPassword").val(typeof node.tunnelUserPassword === 'undefined' ? '' : node.tunnelUserPassword); $("#node-config-input-tunnelUserId").val(typeof node.tunnelUserId === 'undefined' ? '' : node.tunnelUserId); $("#node-config-input-secureCredentialsMode").on('change', function () { updateSecureCredentialMode(); updateSecureMulticastHint(); }); updateSecureCredentialMode(); } catch (error) { } // Autocomplete with KNX IP Interfaces + Refresh capability let aKNXInterfaces = []; function refreshKNXGateways(openDropdown) { const startedAt = Date.now(); // UI: show spinner and disable host input while searching $("#interfaces-count").html('<i class="fa fa-circle-o-notch fa-spin"></i> ' + node._('knxUltimate-config.properties.discovering')); $("#node-config-input-host").prop('disabled', true); aKNXInterfaces = []; $.getJSON("knxUltimateDiscoverKNXGateways", { _: new Date().getTime() }, (data) => { for (let index = 0; index < data.length; index++) { const element = data[index]; const parts = String(element).split(':'); const ip = parts[0]; const port = parts[1]; const name = parts[2]; const address = parts[3]; const security = parts[4] || ''; const protoRaw = parts[5] || ''; const isSecure = security === 'Secure KNX'; const proto = (protoRaw === 'TCP') ? 'TunnelTCP' : (protoRaw === 'UDP' ? 'TunnelUDP' : 'Multicast'); const addrLabel = (typeof address === 'string' && address !== '' && address !== 'undefined') ? address : 'n/a'; aKNXInterfaces.push({ label: `${name || ''} -> ${ip || ''}:${port || ''} phys addr:${addrLabel}`, value: element, isSecure, proto }); } const finalize = () => { $("#interfaces-count").text(`${aKNXInterfaces.length} ` + node._('knxUltimate-config.properties.interfaces_found')); try { $("#node-config-input-host").autocomplete('option', 'source', aKNXInterfaces); if (openDropdown === true) { $("#node-config-input-host").autocomplete("search", ""); } } catch (e) { } $("#node-config-input-host").prop('disabled', false); }; const elapsed = Date.now() - startedAt; const wait = Math.max(0, 1000 - elapsed); setTimeout(finalize, wait); }).fail(function () { const finalizeFail = () => { $("#interfaces-count").text(node._('knxUltimate-config.properties.discovery_failed')); $("#node-config-input-host").prop('disabled', false); }; const elapsed = Date.now() - startedAt; const wait = Math.max(0, 2000 - elapsed); setTimeout(finalizeFail, wait); }); } // Initialize autocomplete once $("#node-config-input-host").autocomplete({ minLength: 0, source: aKNXInterfaces, select: function (event, ui) { const parts = String(ui.item.value).split(':'); const ip = parts[0]; const port = parts[1]; const name = parts[2]; const address = parts[3]; const security = parts[4] || ''; const protoRaw = parts[5] || ''; const isSecure = security === 'Secure KNX'; const proto = (protoRaw === 'TCP') ? 'TunnelTCP' : (protoRaw === 'UDP' ? 'TunnelUDP' : 'Multicast'); $("#node-config-input-host").val(ip); $("#node-config-input-port").val(port); $("#node-config-input-name").val(name || ""); if (isSecure) { try { $("#tabsMain").tabs("option", "active", 1); } catch (e) { } } else { try { $("#tabsMain").tabs("option", "active", 0); } catch (e) { } } // Set protocol coming from discovery and lock selection try { const $sel = $("#node-config-input-hostProtocol"); $sel.val(proto); $sel.prop('disabled', true); } catch (e) { } // Ensure UI visibility follows protocol choice updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); // Suggest a physAddr coherent with gateway IA (area.line preserved) let prefix = ''; let gwIaParts = []; if (typeof address === 'string' && address.includes('.')) { gwIaParts = address.split('.'); prefix = `${gwIaParts[0]}.${gwIaParts[1]}`; } else { const currentPhys = $("#node-config-input-physAddr").val() || ''; const m = String(currentPhys).trim().match(/^(\d{1,2})\.(\d{1,3})/); prefix = m ? `${m[1]}.${m[2]}` : '15.15'; } // pick a last byte in the safer range 200..254 let rnd = 200 + Math.floor(Math.random() * 55); // avoid same last byte as gateway IA if known and within range try { const gwLast = parseInt(gwIaParts[2]); if (!isNaN(gwLast) && gwLast >= 200 && gwLast <= 254 && rnd === gwLast) { rnd = (gwLast < 254 ? gwLast + 1 : 200); } } catch (e) { } const phys = `${prefix}.${rnd}`; $("#node-config-input-physAddr").val(phys); autoSelectEthernetInterfaceForHost({ force: true }); blinkBackgroundArray(["#node-config-input-host", "#node-config-input-port", "#node-config-input-name", "#node-config-input-physAddr"]); return false; }, focus: function (event, ui) { $("#node-config-input-host").val(ui.item.value.split(':')[0]); return false; } }).on("focus click", function () { const $input = $(this); const currentValue = $input.val(); $input.data('knx-restore-host', currentValue); if (currentValue) { $input.val(''); } $input.autocomplete("search", ""); if (currentValue) { setTimeout(() => { if ($input.data('knx-restore-host') === currentValue) { $input.val(currentValue); } }, 0); } }).on("blur", function () { $(this).data('knx-restore-host', null); }).autocomplete("instance")._renderItem = function (ul, item) { const parts = String(item.value).split(':'); const ip = parts[0]; const port = parts[1]; const name = parts[2]; const address = parts[3]; const security = parts[4] || ''; const protoRaw = parts[5] || ''; const isSecure = security === 'Secure KNX'; const addrLabel = (typeof address === 'string' && address !== '' && address !== 'undefined') ? address : 'n/a'; const colorStyle = isSecure ? 'color: green;' : ''; return $("<li>") .append(`<div style="${colorStyle}"><b>${ip || ''}:${port || ''}</b> [${protoRaw || ''}] -> ${name || ''} - ${node._('knxUltimate-config.properties.address')}:${addrLabel}${isSecure ? ' (' + node._('knxUltimate-config.properties.secure_knx_label') + ')' : ''}</div>`) .appendTo(ul); } // Initial discovery refreshKNXGateways(false); // Refresh button handler try { $("#refresh-interfaces").on('click', function (e) { try { e.preventDefault(); e.stopPropagation(); } catch (_) { } refreshKNXGateways(false); // refresh silently, don't open dropdown }); } catch (e) { } // Track protocol changes to sync visibility and suggest physAddr when needed try { $("#node-config-input-hostProtocol").on('change', function () { updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); try { const ipNow = String($("#node-config-input-host").val() || '').trim(); if (!$("#node-config-input-physAddr").val()) { let gwIA = null; for (let i = 0; i < aKNXInterfaces.length; i++) { const parts = String(aKNXInterfaces[i].value).split(':'); if (parts[0] === ipNow) { gwIA = parts[3] || null; break; } } suggestPhysAddrFromGatewayIA(gwIA); } } catch (e) { } }); } catch (e) { } const $knxEthInterfaceSelect = $("#node-config-input-KNXEthInterface"); const $knxEthInterfaceManualRow = $("#divKNXEthInterfaceManuallyInput"); const $knxEthInterfaceManualInput = $("#node-config-input-KNXEthInterfaceManuallyInput"); let ethernetInterfaces = []; let suppressInterfaceSelectionChange = false; let userLockedInterfaceSelection = false; function updateEthernetManualVisibility(value) { if (value === 'Manual') { $knxEthInterfaceManualRow.show(); } else { $knxEthInterfaceManualRow.hide(); } } function setEthernetInterfaceSelection(value, triggerChange) { const finalValue = value || 'Auto'; if ($knxEthInterfaceSelect.val() === finalValue) { if (triggerChange) { suppressInterfaceSelectionChange = true; $knxEthInterfaceSelect.trigger('change'); suppressInterfaceSelectionChange = false; } return; } suppressInterfaceSelectionChange = true; $knxEthInterfaceSelect.val(finalValue); if (triggerChange) { $knxEthInterfaceSelect.trigger('change'); } suppressInterfaceSelectionChange = false; } function findEthernetInterfaceMatch(ipOctets) { if (!Array.isArray(ipOctets) || ethernetInterfaces.length === 0) return null; let bestMatch = null; let bestMaskBits = -1; ethernetInterfaces.forEach((iface) => { const entries = Array.isArray(iface.addresses) ? iface.addresses : []; entries.forEach((entry) => { if (!entry || entry.family !== 'IPv4') return; const ifaceOctets = parseIPv4Address(entry.address); const maskOctets = parseIPv4Address(entry.netmask || ''); if (!ifaceOctets || !maskOctets) return; const ifaceNetwork = computeIPv4NetworkKey(ifaceOctets, maskOctets); const hostNetwork = computeIPv4NetworkKey(ipOctets, maskOctets); if (!ifaceNetwork || !hostNetwork || ifaceNetwork !== hostNetwork) return; const maskBits = countNetmaskBits(maskOctets); if (maskBits > bestMaskBits) { bestMaskBits = maskBits; bestMatch = iface; } }); }); return bestMatch; } function autoSelectEthernetInterfaceForHost(options = {}) { const { force } = options; if (!force && userLockedInterfaceSelection) return; const hostValue = String($("#node-config-input-host").val() || '').trim(); const hostOctets = parseIPv4Address(hostValue); if (!hostOctets || isMulticastIPv4(hostOctets)) return; const match = findEthernetInterfaceMatch(hostOctets); if (!match || !match.name) return; if ($knxEthInterfaceSelect.val() === match.name) return; setEthernetInterfaceSelection(match.name, true); } $knxEthInterfaceSelect.empty() .append($("<option></option>") .attr("value", "Auto") .text(node._('knxUltimate-config.properties.iface_auto'))) .append($("<option></option>") .attr("value", "Manual") .text(node._('knxUltimate-config.properties.iface_manual'))); $knxEthInterfaceManualInput.val(typeof node.KNXEthInterfaceManuallyInput === "undefined" ? "" : node.KNXEthInterfaceManuallyInput); $knxEthInterfaceSelect.on('change', function () { const value = $knxEthInterfaceSelect.val(); updateEthernetManualVisibility(value); if (!suppressInterfaceSelectionChange) { userLockedInterfaceSelection = !!value && value !== 'Auto'; } }); $.getJSON('knxUltimateETHInterfaces', (data) => { ethernetInterfaces = Array.isArray(data) ? data.slice() : []; ethernetInterfaces.sort((a, b) => { const nameA = (a && a.name) ? String(a.name) : ''; const nameB = (b && b.name) ? String(b.name) : ''; return nameA.localeCompare(nameB); }); $knxEthInterfaceSelect.find('option').filter(function () { const val = $(this).val(); return val !== 'Auto' && val !== 'Manual'; }).remove(); ethernetInterfaces.forEach((iFace) => { if (!iFace || !iFace.name) return; const labelAddress = (typeof iFace.address === 'string' && iFace.address.length > 0) ? iFace.address : ''; const displayText = labelAddress ? `${iFace.name} (${labelAddress})` : iFace.name; const $opt = $("<option></option>") .attr("value", iFace.name) .text(displayText); $opt.data('iface', iFace); $knxEthInterfaceSelect.append($opt); }); const initialValue = (typeof node.KNXEthInterface === "undefined") ? "Auto" : node.KNXEthInterface; userLockedInterfaceSelection = initialValue !== 'Auto'; setEthernetInterfaceSelection(initialValue, true); if (!userLockedInterfaceSelection) { autoSelectEthernetInterfaceForHost({ force: true }); } }); // Abilita manualmente la lista dei protocolli quando l'utente modifica l'IP a mano try { $("#node-config-input-host").on('input', function () { const isSecure = isSecureTabActive(); const $sel = $("#node-config-input-hostProtocol"); // Abilita select $sel.prop('disabled', false); // Mostra opzioni consentite in base a Secure/Plain $sel.find('option').show().prop('disabled', false); if (isSecure) { $sel.find("option[value='TunnelUDP']").prop('disabled', true).hide(); $sel.find("option[value='Auto']").prop('disabled', true).hide(); } else { $sel.find("option[value='TunnelTCP']").prop('disabled', true).hide(); $sel.find("option[value='Auto']").prop('disabled', true).hide(); } // Sync visibility when user types a multicast/unicast IP updateTunnelIAVisibility(); updatePhysAddrVisibility(); // Suggest physAddr when typing unicast IP and field is empty try { const ipTyped = String($(this).val() || '').trim(); if (ipTyped) { let gwIA = null; let proto = null; for (let i = 0; i < aKNXInterfaces.length; i++) { const parts = String(aKNXInterfaces[i].value).split(':'); if (parts[0] === ipTyped) { gwIA = parts[3] || null; const protoRaw = parts[5] || ''; proto = (protoRaw === 'TCP') ? 'TunnelTCP' : (protoRaw === 'UDP' ? 'TunnelUDP' : 'Multicast'); break; } } // Suggest phys only if empty if (!$("#node-config-input-physAddr").val()) { suggestPhysAddrFromGatewayIA(gwIA); } // If discovery knows protocol, preselect it (keep select enabled for manual override) if (proto) { $("#node-config-input-hostProtocol").val(proto); } const octets = parseIPv4Address(ipTyped); if (octets && !isMulticastIPv4(octets)) enforceProtocolFromIP(); } } catch (e) { } autoSelectEthernetInterfaceForHost(); }); } catch (e) { } // 14/08/2021 Elimino il file delle persistenze di questo nodo $.getJSON("deletePersistGAFile?serverId=" + node.id, (data) => { }); // 06/07/2023 Tabs // ***************************** $("#tabs").tabs(); // ***************************** var sRetDebugText = ""; $("#getinfocam").click(function () { sRetDebugText = ""; $("#divDebugText").show(); for (const [key, value] of Object.entries(node)) { sRetDebugText += (`-> ${key}: ${value}\r`); } $("#debugText").val(sRetDebugText); // Store the config-node); }); $("#getallgaused").click(function () { sRetDebugText = ""; $("#divDebugText").show(); let aFound = []; RED.nodes.eachNode(function (node) { if (!aFound.includes(node.topic)) { aFound.push(node.topic); sRetDebugText += node.topic + "\r" } }); sRetDebugText = node._('knxUltimate-config.utility.copy_ga_router') + "\r" + sRetDebugText; $("#debugText").val(sRetDebugText); // Store the config-node); }); }, oneditcancel: function () { try { if (this._keyringEditor) { this._keyringEditor.destroy(); this._keyringEditor = null; } if (this._csvEditor) { this._csvEditor.destroy(); this._csvEditor = null; } } catch (e) { } }, oneditsave: function () { // Return to the info tab try { RED.sidebar.show("info"); } catch (error) { } var node = this; try { if (node._keyringEditor) { $("#node-config-input-keyringFileXML").val(node._keyringEditor.getValue()); node._keyringEditor.destroy(); node._keyringEditor = null; } if (node._csvEditor) { $("#node-config-input-csv").val(node._csvEditor.getValue()); node._csvEditor.destroy(); node._csvEditor = null; } try { $("#node-config-input-tunnelIA").val($("#node-config-input-tunnelInterfaceIndividualAddress").val()); } catch (e) { } } catch (e) { } // Check if the csv file contains errors if (($("#node-config-input-csv").val() != 'undefined' && $("#node-config-input-csv").val() != "") || ($("#node-config-input-keyring").val() != 'undefined' && $("#node-config-input-keyring").val() != "")) { var checkResult = node._("knxUltimate-config.ets.deploywithETS"); var myNotification = RED.notify(checkResult, { modal: true, fixed: true, type: 'info', buttons: [ { text: "OK", click: function (e) { myNotification.close(); } }] }) } }, label: function () { return typeof this.name === undefined ? (this.host + ":" + this.port) : this.name + " " + (this.host + ":" + this.port); } }); </script> <style> .ui-tabs { background: transparent; border: none; } .ui-tabs .ui-widget-header { background: transparent; border: none; border-bottom: 1px solid #c0c0c0; -moz-border-radius: 0px; -webkit-border-radius: 0px; border-radius: 0px; } .ui-tabs .ui-tabs-nav .ui-state-default { background: transparent; border: none; } .ui-tabs .ui-tabs-nav .ui-state-active { background: transparent no-repeat bottom center; border: none; } .ui-tabs .ui-tabs-nav .ui-state-default a { color: #878787; outline: none; } .ui-tabs .ui-tabs-nav .ui-state-active a { color: #377e00; border-bottom: 1px solid#377e00; outline: none; } #knxUltimate-config-template .form-row { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; } #knxUltimate-config-template .form-row>label { flex: 0 0 230px; display: flex; align-items: center; gap: 6px; margin: 0; } #knxUltimate-config-template .form-row>label+input, #knxUltimate-config-template .form-row>label+select, #knxUltimate-config-template .form-row>label+textarea { flex: 1 1 auto; } #knxUltimate-config-template .form-row.form-row-offset { display: block; margin-left: 230px; } #knxUltimate-config-template .form-row.form-row-checkbox { align-items: flex-start; } #knxUltimate-config-template .form-row.form-row-checkbox input[type="checkbox"] { margin-top: 4px; } #knxUltimate-config-template .form-row.form-row-checkbox label { flex: 1 1 auto; } #knxUltimate-config-template #hintSecureMulticast, #knxUltimate-config-template .form-row .form-tips { margin-left: 230px; } #knxUltimate-config-template .form-row.form-row-no-label { display: block; } #knxUltimate-config-template i.fa.fa-shield { color: #0f5d16; } #node-config-input-keyringFileXML-editor .monaco-editor .minimap, #node-config-input-csv-editor .monaco-editor .minimap { display: none !important; } #node-config-input-keyringFileXML-editor .ace_gutter { display: none !important; } #node-config-input-keyringFileXML-editor .ace_scroller { left: 0 !important; } #knxUltimate-config-template .form-section-heading { margin-bottom: 12px; } </style> <script type="text/html" data-template-name="knxUltimate-config"> <div id="knxUltimate-config-template"> <div class="form-section-heading"> <b><span data-i18n="knxUltimate-config.properties.title"></span></b> </div> <div class="form-row"> <label for="node-config-input-name"> <i class="fa fa-tag"></i> <span data-i18n="knxUltimate-config.properties.node-config-input-name"></span></label> <input type="text" id="node-config-input-name" style="width: 200px"> </div> <div class="form-row"> <label for="node-config-input-host"><i class="fa fa-server"></i> <span data-i18n="knxUltimate-config.properties.host"></span></label> <input type="text" id="node-config-input-host" style="width: 200px"> <i id="refresh-interfaces" class="fa fa-refresh" style="cursor:pointer; color:#377e00; margin-left:6px;" data-i18n="[title]knxUltimate-config.properties.refresh_interfaces"></i> </div> <div class="form-row form-row-offset"> <span id="interfaces-count" style="font-size: 12px; color:#377e00; margin-top: 2px;"></span> </div> <div class="form-row" id="rowPhysAddr"> <label for="node-config-input-physAddr"> <i class="fa fa-microchip"></i> <span data-i18n="knxUltimate-config.advanced.knx_phy_addr"></span> </label> <input type="text" id="node-config-input-physAddr" style="width:100px"> <span id="hintSecureMulticastToggle" style="display:none; cursor:pointer; color:#c97f00; margin-left:10px;"> <i class="fa fa-exclamation-triangle"></i> <span data-i18n="knxUltimate-config.properties.secure_multicast_physaddr_hint_toggle"></span> </span> </div> <div id="hintSecureMulticast" class="form-tips" style="display:none; margin-top:6px;"> <i class="fa fa-shield"></i> <span data-i