UNPKG

node-red-contrib-knx-ultimate

Version:

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

917 lines (870 loc) 50.9 kB
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script> <script type="text/javascript"> RED.nodes.registerType('knxUltimate-config', { category: 'config', defaults: { host: { value: "224.0.23.12", required: true }, port: { value: 3671, required: true, validate: RED.validators.number() }, // the KNX physical address we'd like to use physAddr: { value: "15.15.22", required: false }, hostProtocol: { value: "Auto", required: false }, // TunnelUDP/TunnelTCP/Multicast // enable this option to suppress the acknowledge flag with outgoing L_Data.req requests. LoxOne needs this suppressACKRequest: { value: false }, csv: { value: "", required: false }, KNXEthInterface: { value: "Auto" }, KNXEthInterfaceManuallyInput: { value: "" }, stopETSImportIfNoDatapoint: { value: "fake" }, loglevel: { value: "error" }, name: { value: "KNX Gateway" }, delaybetweentelegrams: { value: 25, required: false, validate: RED.validators.number() }, ignoreTelegramsWithRepeatedFlag: { value: false, required: false }, keyringFileXML: { value: "" }, knxSecureSelected: { value: false }, secureCredentialsMode: { value: "keyring" }, tunnelIASelection: { value: "Auto" }, tunnelIA: { value: "" }, tunnelInterfaceIndividualAddress: { value: "" }, 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>&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 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="