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.
917 lines (870 loc) • 50.9 kB
HTML
<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: "" },
            autoReconnect: { value: "yes" }
        },
        credentials: {
            keyringFilePassword: { type: "password" },
            tunnelUserPassword: { 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(); } 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(); }, 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 updateSecureCredentialMode() {
                try {
                    const mode = getSecureCredentialsMode();
                    const isKeyring = (mode === 'keyring');
                    $("#secureKeyringFields").toggle(isKeyring);
                    if (!isKeyring) {
                        $("#rowTunnelIASelection").hide();
                        $("#divTunnelIA").hide();
                    }
                } catch (e) { }
                updateTunnelIAVisibility();
            }
            // 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 showTunnelIA = isSecure && !isMulticast && mode === 'keyring';
                    if (showTunnelIA) {
                        $("#rowTunnelIASelection").show();
                        if ($("#node-config-input-tunnelIASelection").val() === 'Manual') {
                            $("#divTunnelIA").show();
                        } else {
                            $("#divTunnelIA").hide();
                        }
                    } else {
                        $("#rowTunnelIASelection").hide();
                        $("#divTunnelIA").hide();
                    }
                    const showManualTunnelFields = isSecure && !isMulticast && mode === 'manual';
                    $("#rowTunnelInterfaceIndividualAddress").toggle(showManualTunnelFields);
                    $("#rowTunnelUserPassword").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 {
                    if (getSecureCredentialsMode() !== 'keyring') {
                        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 {
                $("#node-config-input-tunnelIASelection").val(typeof node.tunnelIASelection === 'undefined' ? 'Auto' : node.tunnelIASelection);
                $("#node-config-input-tunnelIA").val(typeof node.tunnelIA === 'undefined' ? '' : node.tunnelIA);
                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) { $(this).val(ui.item.value); return false; },
                        select: function (event, ui) { $(this).val(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").val(typeof node.tunnelInterfaceIndividualAddress === 'undefined' ? '' : node.tunnelInterfaceIndividualAddress);
                $("#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);
                    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 () {
                $(this).autocomplete("search", "");
            }).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) { }
            $("#node-config-input-KNXEthInterface").append($("<option></option>")
                .attr("value", "Auto")
                .text(node._('knxUltimate-config.properties.iface_auto'))
            );
            $("#node-config-input-KNXEthInterface").append($("<option></option>")
                .attr("value", "Manual")
                .text(node._('knxUltimate-config.properties.iface_manual'))
            );
            $.getJSON('knxUltimateETHInterfaces', (data) => {
                data.sort().forEach(iFace => {
                    $("#node-config-input-KNXEthInterface").append($("<option></option>")
                        .attr("value", iFace.name)
                        .text(iFace.name + " (" + iFace.address + ")")
                    )
                });
                $("#node-config-input-KNXEthInterface").val(typeof node.KNXEthInterface === "undefined" ? "Auto" : node.KNXEthInterface)
                if (node.KNXEthInterface === "Manual") {
                    // Show input
                    $("#divKNXEthInterfaceManuallyInput").show();
                } else {
                    $("#divKNXEthInterfaceManuallyInput").hide()
                }
                $("#node-config-input-KNXEthInterface").on('change', function () {
                    if ($("#node-config-input-KNXEthInterface").val() === "Manual") {
                        // Show input
                        $("#divKNXEthInterfaceManuallyInput").show();
                    } else {
                        // Hide input
                        $("#divKNXEthInterfaceManuallyInput").hide()
                    }
                });
            });
            // Abilita manualmente la lista dei protocolli quando l'utente modifica l'IP a mano
            try {
                $("#node-config-input-host").on('input', function () {
                    const isSecure = (function () {
                        try { return $("#tabsMain").tabs("option", "active") === 1; } catch (e) { return !!node.knxSecureSelected; }
                    })();
                    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); }
                        }
                    } catch (e) { }
                });
            } 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;
                }
            } 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;
    }
</style>
<script type="text/html" data-template-name="knxUltimate-config">
    <div class="form-row">
       <b><span data-i18n="knxUltimate-config.properties.title"></span></b>
        <br/><br/>       
    </div>
   
        <div class="form-row">
            <label for="node-config-input-name" style="width: 200px" >
                <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" style="width: 200px"><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">
            <label style="width: 200px"></label>
            <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" style="width: 200px">
                <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-i18n="knxUltimate-config.properties.secure_multicast_physaddr_hint"></span>
        </div>
    
    <!-- KNX Secure / Unsecure tabbed selector -->
    <div id="tabsMain">
        <ul>
          <li><a href="#unsecureKNX"><i class="fa fa-circle-o"></i> <span data-i18n="knxUltimate-config.properties.unsecureKNX"></span></a></li>
          <li><a href="#SecureKNX"><i class="fa fa-shield"></i> <span data-i18n="knxUltimate-config.properties.secureKNX"></span></a></li>
        </ul>
        <div id="unsecureKNX" style="margin: 5px 5px 5px 5px;">
            <p></p>
        </div>
        <div id="SecureKNX" style="margin: 5px 5px 5px 5px;" >
            <p>
                <div class="form-row">
                    <i class="fa fa-youtube"></i>
                    <a href="https://youtu.be/OpR7ZQTlMRU" target="_blank">
                        <span data-i18n="knxUltimate-config.ets.youtubeKeyring"></span>
                    </a>
                </div>
                <div class="form-row">
                    <label for="node-config-input-secureCredentialsMode"><i class="fa fa-shield"></i> <span data-i18n="knxUltimate-config.ets.secure_credentials_mode"></span></label>
                    <select id="node-config-input-secureCredentialsMode" style="width:200px;">
                        <option value="keyring" data-i18n="knxUltimate-config.ets.secure_credentials_mode_keyring"></option>
                        <option value="manual" data-i18n="knxUltimate-config.ets.secure_credentials_mode_manual"></option>
                    </select>
                </div>
                <div id="secureKeyringFields">
                <div class="form-row">
                    <label for="node-config-input-keyringFileXML"><span data-i18n="knxUltimate-config.ets.keyring_file"></span></label>
                    <div id="node-config-input-keyringFileXML-editor" class="node-text-editor" style="height:200px; min-height:140px; width:100%"></div>
                    <input type="hidden" id="node-config-input-keyringFileXML" data-i18n="[placeholder]knxUltimate-config.ets.keyring" />
                </div>
                <div class="form-row">
                    <label for="node-config-input-keyringFilePassword"><i class="fa fa-shield"></i> <span data-i18n="knxUltimate-config.ets.password"></span></label>
                    <input type="password" id="node-config-input-keyringFilePassword" style="width:200px;">               
                </div>
                <div class="form-row" id="rowTunnelIASelection">
                    <label for="node-config-input-tunnelIASelection"><i class="fa fa-exchange"></i> <span data-i18n="knxUltimate-config.properties.tunnel_ia_label"></span></label>
                    <select id="node-config-input-tunnelIASelection" style="width: 150px;">
                        <option value="Auto" data-i18n="knxUltimate-config.properties.tunnel_ia_mode_auto"></option>
                        <option value="Manual" data-i18n="knxUltimate-config.properties.tunnel_ia_mode_manual"></option>
                    </select>
                </div>
                <div class="form-row" id="divTunnelIA" style="display:none;">
                    <label for="node-config-input-tunnelIA"><i class="fa fa-map-marker"></i> <span data-i18n="knxUltimate-config.properties.tunnel_ia_input_label"></span></label>
                    <input type="text" id="node-config-input-tunnelIA" style="width:150px;" data-i18n="[placeholder]knxUltimate-config.properties.tunnel_ia_placeholder"/>
                </div>
                </div>
                <div class="form-row" id="rowTunnelInterfaceIndividualAddress" style="display:none;">
                    <label for="node-config-input-tunnelInterfaceIndividualAddress"><i class="fa fa-map-marker"></i> <span data-i18n="knxUltimate-config.ets.tunnel_interface_individual_address"></span></label>
                    <input type="text" id="node-config-input-tunnelInterfaceIndividualAddress" style="width:150px;" data-i18n="[placeholder]knxUltimate-config.ets.tunnel_interface_individual_address_placeholder"/>
                </div>
                <div class="form-row" id="rowTunnelUserPassword" style="display:none;">
                    <label for="node-config-input-tunnelUserPassword"><i class="fa fa-lock"></i> <span data-i18n="knxUltimate-config.ets.tunnel_user_password"></span></label>
                    <input type="password" id="node-config-input-tunnelUserPassword" style="width:200px;" />
                </div>
            </p>   
        </div>
    </div>
    
      <br/>
<div id="tabs">
    <ul>
        <li><a href="#tabs-1"><i class="fa fa-list-ol"></i> <span data-i18n="knxUltimate-config.tabs.configuration"></span></a></li>
        <li><a href="#tabs-2"><i class="fa fa-braille"></i> <span data-i18n="knxUltimate-config.tabs.advanced"></span></a></li>
        <li><a href="#tabs-3"><i class="fa fa-code"></i> <span data-i18n="knxUltimate-config.tabs.ets_import"></span></a></li>
        <li><a href="#tabs-4"><i class="fa fa-key"></i> <span data-i18n="knxUltimate-config.tabs.utility"></span></a></li>
    </ul>
    <div id="tabs-1">
        <p>
            <div class="form-row">
                <label for="node-config-input-port" style="width: 200px">
                   <span data-i18n="knxUltimate-config.properties.ip_port"></span>
                </label>
                <input type="text" id="node-config-input-port" style="width: 100px">
            </div>
            <div class="form-row">
                <label for="node-config-input-hostProtocol" style="width: 200px">
                   <span data-i18n="knxUltimate-config.properties.ip_protocol"></span>
                </label>
                <select id="node-config-input-hostProtocol" >
                    <option value="Auto" data-i18n="knxUltimate-config.properties.proto_auto"></option>
                    <option value="TunnelUDP" data-i18n="knxUltimate-config.properties.proto_tunnel_udp"></option>
                    <option value="Multicast" data-i18n="knxUltimate-config.properties.proto_multicast"></option>
                    <option value="TunnelTCP" data-i18n="knxUltimate-config.properties.proto_tunnel_tcp"></option>
                </select>   
            </div>
            
            <div class="form-row">
                <label for="node-config-input-KNXEthInterface" style="width: 200px">
                    <i class="fa fa-wifi"></i>
                    <span data-i18n="knxUltimate-config.properties.bind_local_int"></span>
                </label>
                <select id="node-config-input-KNXEthInterface"></select>
            </div>
            <div class="form-row" id="divKNXEthInterfaceManuallyInput" style="display: none;">
                <label for="node-config-input-KNXEthInterfaceManuallyInput"><span data-i18n="knxUltimate-config.properties.iface_name"></span></label>
                <input type="text" id="node-config-input-KNXEthInterfaceManuallyInput"
                    data-i18n="[placeholder]knxUltimate-config.properties.iface_name_placeholder">
            </div>
            <div class="form-row">
                <label for="node-config-input-autoReconnect" style="width: 200px">
                    <i class="fa fa-plug"></i>
                    <span data-i18n="knxUltimate-config.properties.autoReconnect"></span>
                </label>
                <select id="node-config-input-autoReconnect">
                    <option value="yes" data-i18n="knxUltimate-config.properties.autoReconnect_yes"></option>
                    <option value="no" data-i18n="knxUltimate-config.properties.autoReconnect_no"></option>
                </select>
            </div>
        </p>
    </div>
    <div id="tabs-2">
        <p>
            
            <div class="form-row">
                <input type="checkbox" id="node-config-input-ignoreTelegramsWithRepeatedFlag"
                    style="display:inline-block; width:auto; vertical-align:top;">
                <label style="width:85%" for="node-config-input-ignoreTelegramsWithRepeatedFlag">
                    <i class="fa fa-ban"></i>
                    <span data-i18n="knxUltimate-config.advanced.ignoreTelegramsWithRepeatedFlag"></span>
                </label>
            </div>
            <div class="form-row">
                <input type="checkbox" id="node-config-input-suppressACKRequest"
                    style="display:inline-block; width:auto; vertical-align:top;">
                <label style="width:85%" for="node-config-input-suppressACKRequest">
                    <i class="fa fa-ban"></i>
                    <span data-i18n="knxUltimate-config.advanced.suppress_ack"></span>
                </label>
            </div>
            <div class="form-row">
                <label for="node-config-input-delaybetweentelegrams" style="width:auto">
                    <i class="fa fa-hourglass-start"></i>
                    <span data-i18n="knxUltimate-config.advanced.delaybetweentelegrams"></span>
                </label>
                <input type="number" id="node-config-input-delaybetweentelegrams" style="width:20%">
            </div>
            <div class="form-row">
                <label for="node-config-input-loglevel">
                    <i class="fa fa-question-circle"></i>
                    <span data-i18n="knxUltimate-config.advanced.log_level"></span>
                </label>
                <select id="node-config-input-loglevel" style="width:40%;">
                    <option value="disable" data-i18n="knxUltimate-config.advanced.select_silent"></option>
                    <option value="error" data-i18n="knxUltimate-config.advanced.select_error"></option> 
                    <option value="warn" data-i18n="knxUltimate-config.advanced.select_warning"></option>
                    <option value="info" data-i18n="knxUltimate-config.advanced.select_info"></option>
                    <option value="debug" data-i18n="knxUltimate-config.advanced.select_debug"></option>
                </select>
                <!-- <// DEBUG LEVELS: success < debug < info < warn < error < disable
                            </div> -->
            </div>
        </p>
    </div>
    <div id="tabs-3">
        <p>
            <div id="etsCSVListBox">
                <h3><span data-i18n="knxUltimate-config.properties.ets_import"></span></h3>
                <div>
                    <div class="form-row">
                        <span data-i18n="knxUltimate-config.ets.description"></span>
                    </div>
                    <div class="form-row">
                        <span style="color:red" data-i18n="[html]knxUltimate-config.ets.instruction"></span>
                    </div>
                    <div class="form-row">
                        <span style="color:red" data-i18n="[html]knxUltimate-config.ets.youtube"></span>
                    </div>
                    <div class="form-row">
                        <label for="node-config-input-stopETSImportIfNoDatapoint" style="width:250px">
                            <i class="fa fa-question-circle"></i>
                            <span data-i18n="knxUltimate-config.ets.help_ga"></span>
                        </label>
                        <select id="node-config-input-stopETSImportIfNoDatapoint" style="width:210px">
                            <option value="stop" data-i18n="knxUltimate-config.ets.import_select_stop"></option>
                            <option value="fake" data-i18n="knxUltimate-config.ets.import_select_fake"></option>
                            <option value="skip" data-i18n="knxUltimate-config.ets.import_select_skip"></option>
                        </select>
                    </div>
                    <div class="form-row">
                        <label style="width:auto" for="node-config-input-csv">
                            <i class="fa fa-th-list"></i> <span data-i18n="knxUltimate-config.ets.ga_list_label"></span>
                        </label>
                    </div>
                    <div class="form-row">
                        <div id="node-config-input-csv-editor" class="node-text-editor" style="height:300px; min-height:200px; width:100%"></div>
                        <input type="hidden" id="node-config-input-csv" data-i18n="[placeholder]knxUltimate-config.ets.ga_list_help" />
                    </div>
                </div>
            </div>                       
        </p>
    </div>
    <div id="tabs-4">
        <p>
            <div class="form-row">
                <label style="width:300px"><i class="fa fa-sign-in"></i> <span data-i18n="knxUltimate-config.utility.gather_debug"></span></label>
                <input type="