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, ETS group address importer, and KNX routing between interfaces. Easy to use and highly configurable.

1,580 lines 84.3 kB
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>

<script type="text/javascript">
    const optionalNumberValidator = function (value) {
        return (value === '' || value === null || typeof value === 'undefined') ? true : RED.validators.number()(value)
    }

    RED.nodes.registerType('knxUltimate-config', {
        category: 'config',
        defaults: {
            host: { value: "224.0.23.12", required: false },
            port: { value: 3671, required: false, 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" },
            enableFlowBubbles: { value: false },
            statusUpdateThrottle: { value: "0" },
            statusDateTimeFormat: { value: "legacy" },
            statusDateTimeCustom: { value: "DD MMM HH:mm" },
            statusDateTimeLocale: { value: "" },
            serialPortPath: { value: "/dev/ttyAMA0" },
            serialBaudRate: { value: 19200, validate: optionalNumberValidator },
            serialDataBits: { value: 8 },
            serialStopBits: { value: 1 },
            serialParity: { value: "even" },
            serialRtscts: { value: false },
            serialDtr: { value: true },
            serialTimeout: { value: 1200, validate: optionalNumberValidator },
            isKBERRY: { value: true }
        },
        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);
            }
            // ################################################################

            const isSerialProtocolSelected = () => {
                try {
                    return $("#node-config-input-hostProtocol").val() === 'SerialFT12';
                } catch (e) {
                    return false;
                }
            };
            let previousIpHostValue = typeof node.host === 'string' ? node.host : '';
            try {
                const initialSerialPath = (typeof node.serialPortPath === 'undefined' || node.serialPortPath === null)
                    ? (isSerialProtocolSelected() ? (node.host || '') : '')
                    : node.serialPortPath;
                $("#node-config-input-serialPortPath").val(initialSerialPath || '');
                if ((node.hostProtocol === 'SerialFT12' || node.hostProtocol === 'serialft12') && initialSerialPath) {
                    $("#node-config-input-host").val(initialSerialPath);
                }
            } catch (e) { }
            applySerialDefaults();

            function syncSerialPortPathFromHost() {
                try {
                    const hostValue = String($("#node-config-input-host").val() || '').trim();
                    $("#node-config-input-serialPortPath").val(hostValue);
                } catch (e) { }
            }

            function applySerialDefaults() {
                $("#node-config-input-serialBaudRate").val(typeof node.serialBaudRate === 'undefined' ? 19200 : node.serialBaudRate);
                $("#node-config-input-serialDataBits").val(typeof node.serialDataBits === 'undefined' ? '8' : String(node.serialDataBits));
                $("#node-config-input-serialStopBits").val(typeof node.serialStopBits === 'undefined' ? '1' : String(node.serialStopBits));
                $("#node-config-input-serialParity").val(typeof node.serialParity === 'undefined' ? 'even' : String(node.serialParity));
                $("#node-config-input-serialRtscts").prop('checked', (typeof node.serialRtscts === 'undefined') ? false : !!node.serialRtscts);
                $("#node-config-input-serialDtr").prop('checked', (typeof node.serialDtr === 'undefined') ? true : !!node.serialDtr);
                $("#node-config-input-serialTimeout").val(typeof node.serialTimeout === 'undefined' ? 1200 : node.serialTimeout);
                try {
                    let isKBERRY = node.isKBERRY;
                    if (typeof isKBERRY === 'undefined' || isKBERRY === null || isKBERRY === '') {
                        isKBERRY = true;
                    } else if (typeof isKBERRY === 'string') {
                        isKBERRY = isKBERRY.toLowerCase() === 'true';
                    } else {
                        isKBERRY = !!isKBERRY;
                    }
                    $("#node-config-input-isKBERRY").val(isKBERRY ? 'true' : 'false');
                } catch (e) { }
            }

            const syncStatusDateTimeRows = () => {
                try {
                    const mode = $("#node-config-input-statusDateTimeFormat").val();
                    const showCustom = mode === 'custom';
                    $("#knxUltimate-statusDateTimeCustom-row").toggle(showCustom);
                    $("#knxUltimate-statusDateTimeLocale-row").toggle(showCustom);
                } catch (e) { }
            }

            $("#node-config-input-statusDateTimeFormat").on("change", syncStatusDateTimeRows);
            syncStatusDateTimeRows();

            function updateProtocolSections() {
                const isSerial = isSerialProtocolSelected();
                $("#rowPort").toggle(!isSerial);
                $("#rowEthernetInterface").toggle(!isSerial);
                if (isSerial) {
                    $("#divKNXEthInterfaceManuallyInput").hide();
                } else {
                    try { updateEthernetManualVisibility($("#node-config-input-KNXEthInterface").val()); } catch (e) { }
                }
                $("#serialAdvancedContainer").toggle(isSerial);
                if (isSerial) {
                    syncSerialPortPathFromHost();
                }
            }

            // 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 {
                    if (isSerialProtocolSelected()) return;
                    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();
            }

            function updateSecureModeAvailability() {
                try {
                    const hostProtocol = $("#node-config-input-hostProtocol").val();
                    const $mode = $("#node-config-input-secureCredentialsMode");
                    if (!$mode || $mode.length === 0) return;
                    if (hostProtocol === 'SerialFT12') {
                        // Secure credential source is only meaningful for IP transports.
                        // When using Serial FT1.2 keep the selector disabled to simplify the UI.
                        if ($mode.val() !== 'keyring') {
                            $mode.val('keyring');
                        }
                        $mode.prop('disabled', true);
                    } else {
                        $mode.prop('disabled', false);
                    }
                } catch (e) { }
            }

            // 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 isSerial = (hostProtocol === 'SerialFT12');
                    const mode = getSecureCredentialsMode();
                    const keyringMode = (mode === 'keyring');
                    const manualMode = (mode === 'manual' || mode === 'combined');
                    const showTunnelIA = isSecure && !isMulticast && keyringMode && !isSerial;
                    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.
                    // For Serial FT1.2 these fields are not applicable (they apply only to IP tunnelling),
                    // so keep them hidden when a serial protocol is selected.
                    const showManualTunnelFields = isSecure && manualMode && !isSerial;
                    $("#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');
                    const isSerial = (hostProtocol === 'SerialFT12');
                    if (isSecure && isMulticast && !isSerial) {
                        $("#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 + Serial ports + Refresh capability
            let hostEntries = []
            function listGateways() {
                return new Promise((resolve) => {
                    $.getJSON("knxUltimateDiscoverKNXGateways", { _: new Date().getTime() }, (data) => {
                        resolve(Array.isArray(data) ? data : [])
                    }).fail(() => resolve([]))
                })
            }
            function listSerialPorts() {
                return new Promise((resolve) => {
                    $.getJSON("knxUltimateSerialInterfaces", { _: new Date().getTime() }, (data) => {
                        resolve(Array.isArray(data) ? data : [])
                    }).fail(() => resolve([]))
                })
            }
            function createGatewayEntries(data) {
                const entries = []
                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'
                    entries.push({
                        label: `${name || ''} -> ${ip || ''}:${port || ''} phys addr:${addrLabel}`,
                        value: ip || '',
                        type: 'gateway',
                        gateway: {
                            ip,
                            port,
                            name,
                            address,
                            security,
                            proto,
                            isSecure
                        }
                    })
                }
                return entries
            }
            function createSerialEntries(data) {
                const entries = []
                data.forEach((port) => {
                    if (!port || !port.path) return
                    const info = []
                    if (port.manufacturer) info.push(port.manufacturer)
                    if (port.serialNumber) info.push(port.serialNumber)
                    if (port.vendorId && port.productId) info.push(`${port.vendorId}:${port.productId}`)
                    const descriptor = info.length ? info.join(' / ') : 'n/a'
                    entries.push({
                        label: `[Serial] ${port.path} -> ${descriptor}`,
                        value: port.path,
                        type: 'serial',
                        serial: port
                    })
                })
                return entries
            }
            function refreshHostEntries(openDropdown) {
                const startedAt = Date.now()
                $("#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)
                Promise.all([listGateways(), listSerialPorts()]).then(([gateways, serials]) => {
                    const gatewayEntries = createGatewayEntries(gateways)
                    const serialEntries = createSerialEntries(serials)
                    hostEntries = gatewayEntries.concat(serialEntries)
                    const finalize = () => {
                        $("#interfaces-count").text(`${hostEntries.length} ` + node._('knxUltimate-config.properties.interfaces_found'))
                        try {
                            $("#node-config-input-host").autocomplete('option', 'source', hostEntries)
                            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)
                }).catch(() => {
                    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: hostEntries,
                select: function (event, ui) {
                    const entry = ui.item || {}
                    if (entry.type === 'serial') {
                        const info = entry.serial || {}
                        const path = info.path || entry.value || ''
                        const descriptorParts = []
                        if (info.manufacturer && info.manufacturer !== 'n/a') descriptorParts.push(info.manufacturer)
                        if (info.serialNumber && info.serialNumber !== 'n/a') descriptorParts.push(`#${info.serialNumber}`)
                        if (info.vendorId && info.productId) descriptorParts.push(`${info.vendorId}:${info.productId}`)
                        const descriptor = descriptorParts.length > 0 ? descriptorParts.join(' ') : path
                        $("#node-config-input-host").val(path)
                        $("#node-config-input-name").val(descriptor)
                        syncSerialPortPathFromHost()
                        try { updateSecureModeAvailability(); } catch (e) { }
                        try {
                            const $sel = $("#node-config-input-hostProtocol")
                            $sel.val('SerialFT12')
                            $sel.prop('disabled', true)
                        } catch (e) { }
                        applySerialDefaults()
                        updateProtocolSections()
                        updateTunnelIAVisibility()
                        updatePhysAddrVisibility()
                        updateSecureMulticastHint()
                        blinkBackgroundArray(["#node-config-input-host", "#node-config-input-name"])
                        return false
                    }
                    const gateway = entry.gateway || {}
                    $("#node-config-input-host").val(gateway.ip || '')
                    $("#node-config-input-port").val(gateway.port || '')
                    $("#node-config-input-name").val(gateway.name || "")
                    try { updateSecureModeAvailability(); } catch (e) { }
                    try {
                        const $sel = $("#node-config-input-hostProtocol")
                        if (gateway.proto) {
                            $sel.val(gateway.proto)
                            $sel.prop('disabled', true)
                        }
                    } catch (e) { }
                    updateProtocolSections()
                    updateTunnelIAVisibility()
                    updatePhysAddrVisibility()
                    updateSecureMulticastHint()
                    const address = gateway.address
                    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'
                    }
                    let rnd = 200 + Math.floor(Math.random() * 55)
                    try {
                        const gwLast = parseInt(gwIaParts[2])
                        if (!isNaN(gwLast) && gwLast >= 200 && gwLast <= 254 && rnd === gwLast) {
                            rnd = (gwLast < 254 ? gwLast + 1 : 200)
                        }
                    } catch (e) { }
                    $("#node-config-input-physAddr").val(`${prefix}.${rnd}`)
                    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) {
                    const entry = ui.item || {}
                    if (entry.type === 'serial') {
                        $("#node-config-input-host").val(entry.serial?.path || entry.value || '')
                    } else {
                        $("#node-config-input-host").val(entry.gateway?.ip || entry.value || '')
                    }
                    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) {
                if (item.type === 'serial') {
                    return $("<li>")
                        .append(`<div style="color:#005a9c;"><b>[Serial]</b> ${item.serial?.path || item.value || ''} -> ${item.label.replace('[Serial] ', '')}</div>`)
                        .appendTo(ul)
                }
                const gw = item.gateway || {}
                const colorStyle = gw.isSecure ? 'color: green;' : ''
                const proto = gw.proto || ''
                const addrLabel = (typeof gw.address === 'string' && gw.address !== '' && gw.address !== 'undefined') ? gw.address : 'n/a'
                return $("<li>")
                    .append(`<div style="${colorStyle}"><b>${gw.ip || ''}:${gw.port || ''}</b> [${proto}] -> ${gw.name || ''} - ${node._('knxUltimate-config.properties.address')}:${addrLabel}${gw.isSecure ? ' (' + node._('knxUltimate-config.properties.secure_knx_label') + ')' : ''}</div>`)
                    .appendTo(ul)
            }
            $("#node-config-input-serialPortPath").on('input', function () {
                syncSerialPortPathFromHost()
            });
            try { updateProtocolSections(); } catch (e) { }
            // Initial discovery
            refreshHostEntries(false);
            // Refresh button handler
            try {
                $("#refresh-interfaces").on('click', function (e) {
                    try { e.preventDefault(); e.stopPropagation(); } catch (_) { }
                    refreshHostEntries(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 () {
                    updateProtocolSections();
                    updateTunnelIAVisibility();
                    updatePhysAddrVisibility();
                    updateSecureMulticastHint();
                    const isSerial = isSerialProtocolSelected();
                    if (isSerial) {
                        applySerialDefaults();
                        syncSerialPortPathFromHost();
                        return;
                    }
                    try {
                        const ipNow = String($("#node-config-input-host").val() || '').trim();
                        if (!$("#node-config-input-physAddr").val()) {
                            const gwEntry = hostEntries.find((entry) => entry.type === 'gateway' && entry.gateway && entry.gateway.ip === ipNow);
                            suggestPhysAddrFromGatewayIA(gwEntry ? gwEntry.gateway.address : null);
                        }
                    } 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;
                if (isSerialProtocolSelected()) 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 () {
                    if (isSerialProtocolSelected()) {
                        syncSerialPortPathFromHost();
                        return;
                    }
                    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) {
                            const entry = hostEntries.find((item) => item.type === 'gateway' && item.gateway && item.gateway.ip === ipTyped);
                            const gwIA = entry ? entry.gateway.address : null;
                            const proto = entry ? entry.gateway.proto : null;
                            // 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) { }
            try { updateSecureModeAvailability(); } catch (e) { }


            // NOTE: Do not auto-delete persisted GA cache on editor open.
            // Users can explicitly clear it from the Utility tab.

            // 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);
            });

            $("#clearpersistga").click(function () {
                try {
                    const confirmText = node._("knxUltimate-config.utility.clear_persist_confirm");
                    const okText = node._("knxUltimate-config.utility.clear_persist_ok");
                    const cancelText = node._("knxUltimate-config.utility.clear_persist_cancel");
                    const doneText = node._("knxUltimate-config.utility.clear_persist_done");
                    const failText = node._("knxUltimate-config.utility.clear_persist_fail");

                    var dlg = RED.notify(confirmText, {
                        modal: true,
                        fixed: true,
                        type: "warning",
                        buttons: [
                            {
                                text: cancelText,
                                click: function () { dlg.close(); }
                            },
                            {
                                text: okText,
                                class: "primary",
                                click: function () {
                                    dlg.close();
                                    $.getJSON("deletePersistGAFile?serverId=" + node.id, (data) => {
                                        RED.notify(doneText, { type: "success", timeout: 2500 });
                                    }).fail(function (jqXHR, textStatus, errorThrown) {
                                        const err = (errorThrown || textStatus || "").toString();
                                        RED.notify(failText + (err ? (": " + err) : ""), { type: "error", timeout: 5000 });
                                    });
                                }
                            }
                        ]
                    });
                } catch (e) { }
            });



        },
        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-config-input-hostProtocol").val() === 'SerialFT12') {
                    $("#node-config-input-serialPortPath").val($("#node-config-input-host").val());
                }
            } catch (e) { }
            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" id="rowName">
            <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" id="rowHost">
            <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" id="rowHostInfo">
            <span id="interfaces-count" style="font-size: 12px; color:#377e00; margin-top: 2px;"></span>
        </div>
        <input type="hidden" id="node-config-input-serialPortPath">
        <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-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>&nbsp;<span data-i18n="knxUltimate-config.properties.unsecureKNX"></span></a></li>
          <li><a href="#SecureKNX"><i class="fa fa-shield"></i>&nbsp;<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 style="color:red" 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>
                        <option value="combined" data-i18n="knxUltimate-config.ets.secure_credentials_mode_combined"></option>
                    </select>
                </div>
                <div id="secureKeyringFields">
                <div class="form-row form-row-no-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="rowTunnelUserId" style="display:none;">
                    <label for="node-config-input-tunnelUserId"><i class="fa fa-id-badge"></i> <span data-i18n="knxUltimate-config.ets.tunnel_user_id"></span></label>
                    <input type="text" id="node-config-input-tunnelUserId" style="width:150px;" />
                </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="text" 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" id="rowPort">
                <label for="node-config-input-port">
                   <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">
                   <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>
                    <option value="SerialFT12" data-i18n="knxUltimate-config.properties.proto_serial_ft12"></option>
                </select>   
            </div>
            <div id="serialAdvancedContainer" style="display:none;">
                <div class="form-row">
                    <label for="node-config-input-isKBERRY"><i class="fa fa-microchip"></i> <span data-i18n="knxUltimate-config.properties.serial_mode"></span></label>
                    <select id="node-config-input-isKBERRY" style="width: 200px">
                        <option value="true" data-i18n="knxUltimate-config.properties.serial_mode_kberry"></option>
                        <option value="false" data-i18n="knxUltimate-config.properties.serial_mode_standard"></option>
                    </select>
                </div>
                <div class="form-row">
                    <label for="node-config-input-serialBaudRate"><i class="fa fa-tachometer"></i> <span data-i18n="knxUltimate-config.properties.serial_baud"></span></label>
                    <input type="number" id="node-config-input-serialBaudRate" style="width: 120px">
                </div>
                <div class="form-row">
                    <label for="node-config-input-serialDataBits"><i class="fa fa-ellipsis-h"></i> <span data-i18n="knxUltimate-config.properties.serial_data_bits"></span></label>
                    <select id="node-config-input-serialDataBits" style="width: 120px">
                        <option value="8">8</option>
                        <option value="7">7</option>
                        <option value="6">6</option>
                        <option value="5">5</option>
                    </select>
                </div>
                <div class="form-row">
                    <label for="node-config-input-serialStopBits"><i class="fa fa-arrows-h"></i> <span data-i18n="knxUltimate-config.properties.serial_stop_bits"></span></label>
                    <select id="node-config-input-serialStopBits" style="width: 120px">
                        <option value="1">1</option>
                        <option value="2">2</option>
                    </select>
                </div>
                <div class="form-row">
                    <label for="node-config-input-serialParity"><i class="fa fa-balance-scale"></i> <span data-i18n="knxUltimate-config.properties.serial_parity"></span></label>
                    <select id="node-config-input-serialParity" style="width: 120px">
                        <option value="even" data-i18n="knxUltimate-config.properties.serial_parity_even"></option>
                        <option value="odd" data-i18n="knxUltimate-config.properties.serial_parity_odd"></option>
                        <option value="none" data-i18n="knxUltimate-config.properties.serial_parity_none"></option>
                    </select>
                </div>
                <div class="form-row form-row-checkbox">
                    <input type="checkbox" id="node-config-input-serialRtscts" style="display:inline-block; width:auto; vertical-align:top;">
                    <label for="node-config-input-serialRtscts"><i class="fa fa-random"></i> <span data-i18n="knxUltimate-config.properties.serial_rtscts"></span></label>
                </div>
                <div class="form-row form-row-checkbox">
                    <input type="checkbox" id="node-config-input-serialDtr" style="display:inline-block; width:auto; vertical-align:top;">
                    <label for="node-config-input-serialDtr"><i class="fa fa-battery-full"></i> <span data-i18n="knxUltimate-config.properties.serial_dtr"></span></label>
                </div>
                <div class="form-row">
                    <label for="node-config-input-serialTimeout"><i class="fa fa-clock-o"></i> <span data-i18n="knxUltimate-config.properties.serial_timeout"></span></label>
                    <input type="number" id="node-config-input-serialTimeout" style="width: 120px">
                </div>
            </div>
            

            <div class="form-row" id="rowEthernetInterface">
                <label for="node-config-input-KNXEthInterface">
                    <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">
                    <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 form-row-checkbox">
                <input type="checkbox" id="node-config-input-ignoreTelegramsWithRepeatedFlag"
                    style="display:inline-block; width:auto; vertical-align:top;">
                <label 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 form-row-checkbox">
                <input type="checkbox" id="node-config-input-suppressACKRequest"
                    style="display:inline-block; width:auto; vertical-align:top;">
                <label 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 form-row-checkbox">
                <input type="checkbox" id="node-config-input-enableFlowBubbles"
                    style="display:inline-block; width:auto; vertical-align:top;">
                <label for="node-config-input-enableFlowBubbles">
                    <i class="fa fa-commenting-o"></i>
                    <span data-i18n="knxUltimate-config.advanced.enable_flow_bubbles"></span>
                </label>
            </div>
            <div class="form-tips" data-i18n="knxUltimate-config.advanced.enable_flow_bubbles_help"></div>


            <div class="form-row">
                <label for="node-config-input-delaybetweentelegrams">
                    <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>

	            <div class="form-row">
	                <label>          <span style="font-weight:bold">Node Status</span>
	      </label>
	            </div>

	            <div class="form-row">
	                <label for="node-config-input-statusUpdateThrottle">
	                    <i class="fa fa-clock-o"></i>
	                    <span data-i18n="knxUltimate-config.advanced.status_throttle"></span>
	                </label>
                <select id="node-config-input-statusUpdateThrottle" style="width:40%;">
                    <option value="0" data-i18n="knxUltimate-config.advanced.status_throttle_none"></option>
                    <option value="1" data-i18n="knxUltimate-config.advanced.status_throttle_1s"></option>
                    <option value="3" data-i18n="knxUltimate-config.advanced.status_throttle_3s"></option>
                    <option value="5" data-i18n="knxUltimate-config.advanced.status_throttle_5s"></option>
                    <option value="10" data-i18n="knxUltimate-config.advanced.status_throttle_10s"></option>
                    <option value="30" data-i18n="knxUltimate-config.advanced.status_throttle_30s"></option>
                </select>
            </div>

            <div class="form-row">
                <label for="node-config-input-statusDateTimeFormat">
                    <i class="fa fa-calendar"></i>
                    <span data-i18n="knxUltimate-config.advanced.status_datetime_format"></span>
                </label>
                <select id="node-config-input-statusDateTimeFormat" style="width:40%;">
                    <option value="legacy" data-i18n="knxUltimate-config.advanced.status_datetime_format_legacy"></option>
                    <option value="iso" data-i18n="knxUltimate-config.advanced.status_datetime_format_iso"></option>
                    <option value="isoNoSeconds" data-i18n="knxUltimate-config.advanced.status_datetime_format_iso_nosec"></option>
                    <option value="custom" data-i18n="knxUltimate-config.advanced.status_datetime_format_custom"></option>
                </select>
            </div>

            <div class="form-row" id="knxUltimate-statusDateTimeCustom-row">
                <label for="node-config-input-statusDateTimeCustom">
                    <i class="fa fa-pencil"></i>
                    <span data-i18n="knxUltimate-config.advanced.status_datetime_custom"></span>
                </label>
                <input type="text" id="node-config-input-statusDateTimeCustom" style="width:70%" data-i18n="[placeholder]knxUltimate-config.advanced.status_datetime_custom_placeholder">
            </div>

            <div class="form-row" id="knxUltimate-statusDateTimeLocale-row">
                <label for="node-config-input-statusDateTimeLocale">
                    <i class="fa fa-globe"></i>
                    <span data-i18n="knxUltimate-config.advanced.status_datetime_locale"></span>
                </label>
                <input type="text" id="node-config-input-statusDateTimeLocale" style="width:70%; min-width:220px" data-i18n="[placeholder]knxUltimate-config.advanced.status_datetime_locale_placeholder">
            </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.youtube"></span>
                    </div>
                    <div class="form-row">
                        <label for="node-config-input-stopETSImportIfNoDatapoint">
                            <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 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><i class="fa fa-sign-in"></i> <span data-i18n="knxUltimate-config.utility.gather_debug"></span></label>
	                <input type="button" id="getinfocam" class="ui-button ui-corner-all ui-widget"
	                    style="background-color:#AEE1FF;width:150px" data-i18n="[value]knxUltimate-config.utility.read_button">
	            </div>
	            <div class="form-row">
	                <label><i class="fa fa-sign-in"></i> <span data-i18n="knxUltimate-config.utility.get_all_used_ga"></span></label>
	                <input type="button" id="getallgaused" class="ui-button ui-corner-all ui-widget"
	                    style="background-color:#AEE1FF;width:150px" data-i18n="[value]knxUltimate-config.utility.read_button">
	            </div>

                <div class="form-row">
                    <label><i class="fa fa-trash"></i> <span data-i18n="knxUltimate-config.utility.clear_persist_label"></span></label>
                    <input type="button" id="clearpersistga" class="ui-button ui-corner-all ui-widget"
                        style="background-color:#FFD2D2;width:150px" data-i18n="[value]knxUltimate-config.utility.clear_persist_button">
                </div>
	            
	            <div class="form-row" id="divDebugText" style="display: none;">                
	                <textarea rows="10" id="debugText" style="width:100%"></textarea>
	            </div>
	        </p>
	    </div>
    </div>

    </div>
  
</script>

<script type="text/html" data-help-name="knxUltimate-config">
    <div data-i18n="knxUltimate-config.help.intro"></div>
    <h3 data-i18n="knxUltimate-config.help.flow_bubbles_title"></h3>
    <ul>
        <li data-i18n="knxUltimate-config.help.flow_bubbles_enable"></li>
        <li data-i18n="knxUltimate-config.help.flow_bubbles_scope"></li>
        <li data-i18n="knxUltimate-config.help.flow_bubbles_tooltip"></li>
        <li data-i18n="knxUltimate-config.help.flow_bubbles_stale"></li>
    </ul>
</script>