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.
216 lines (203 loc) • 8.17 kB
JavaScript
// 31/03/2020 Search Helper
function htmlUtilsfullCSVSearch (sourceText, searchString) {
let aSearchWords = []
if (searchString.toLowerCase().includes('exactmatch')) {
// Find only the strict exact match of group address. For example, if the string is 0/0/2exactmatch, i return only the item in the csv having
// group address 0/0/2 (and not also, for example 0/0/20)
// I can have also an exact string like '0/0/1exactmatch 1.000', (GA or any text, plus the datapoint) and i must take it into consideration
const aSearchStrings = searchString.split(' ')
for (let index = 0; index < aSearchStrings.length; index++) {
const element = aSearchStrings[index]
if (element.includes('exactmatch')) {
aSearchWords.push(element.replace('exactmatch', ' ')) // The last ' ' allow to return the exact match
} else {
aSearchWords.push(element) // The last ' ' allow to return the exact match
}
}
} else {
aSearchWords = searchString.toLowerCase().split(' ')
}
// This searches for all words in a string
let i = 0
for (let index = 0; index < aSearchWords.length; index++) {
if (sourceText.toLowerCase().indexOf(aSearchWords[index]) > -1) i += 1
}
return i == aSearchWords.length
}
// 2025-09 Secure KNX helpers for GA autocompletes
// Cache for secure GA lists per serverId
window.__knxSecureGAsCache = window.__knxSecureGAsCache || {}
function KNX_fetchSecureGAs (serverId) {
return new Promise((resolve) => {
try {
if (window.__knxSecureGAsCache[serverId] instanceof Set) {
resolve(window.__knxSecureGAsCache[serverId])
return
}
$.getJSON('knxUltimateKeyringDataSecureGAs?serverId=' + serverId + '&_=' + new Date().getTime(), (data) => {
try {
const set = new Set()
if (Array.isArray(data)) data.forEach(ga => { if (typeof ga === 'string') set.add(ga) })
window.__knxSecureGAsCache[serverId] = set
resolve(set)
} catch (e) { resolve(new Set()) }
}).fail(function () { resolve(new Set()) })
} catch (e) { resolve(new Set()) }
})
}
function KNX_enableSecureFormatting ($input, serverId) {
try {
KNX_fetchSecureGAs(serverId).then((secureSet) => {
try {
const inst = $input.autocomplete('instance')
if (!inst) return
inst._renderItem = function (ul, item) {
// Try to detect GA from item.ga or from item.value string
let ga = item.ga
if (!ga && typeof item.value === 'string') {
const m = item.value.match(/\b\d{1,2}\/\d{1,3}\/\d{1,3}\b/)
if (m) ga = m[0]
}
const isSecure = ga ? secureSet.has(ga) : false
const colorStyle = isSecure ? 'color: green;' : ''
const shield = isSecure ? '<i class="fa fa-shield"></i> ' : ''
const label = (typeof item.label === 'string') ? item.label : (item.value || '')
return $('<li>').append(`<div style="${colorStyle}">${shield}${label}</div>`).appendTo(ul)
}
} catch (e) { }
})
} catch (e) { }
}
// Expose helpers
window.htmlUtilsfullCSVSearch = htmlUtilsfullCSVSearch
window.KNX_fetchSecureGAs = KNX_fetchSecureGAs
window.KNX_enableSecureFormatting = KNX_enableSecureFormatting
// 2025-09: Make DPT selects searchable via jQuery UI Autocomplete
function KNX_makeSelectSearchable ($select) {
try {
if (!($select && $select.length)) return
const id = $select.attr('id') || ('knx-dpt-' + Math.random().toString(36).slice(2))
$select.attr('id', id)
const prevCount = $select.data('knx-options-count')
const curCount = $select.find('option').length
const already = $select.data('knx-searchable') === true
const needsSync = already && prevCount !== curCount
function buildSource () {
const items = []
$select.find('option').each(function () {
const v = $(this).attr('value')
const t = $(this).text()
items.push({ label: t, value: v })
})
return items
}
function syncFromSelect ($input) {
try {
const val = $select.val()
const txt = ($select.find('option:selected').text()) || ''
if ($input) { $input.val(txt) }
// refresh source
if ($input && $input.data('ui-autocomplete')) {
$input.autocomplete('option', 'source', buildSource())
}
$select.data('knx-options-count', curCount)
} catch (e) { }
}
if (!already) {
// Create input next to select
const width = $select.outerWidth() || 200
const $input = $('<input type="text" class="knx-dpt-combobox" autocomplete="off" />')
.attr('id', id + '-search')
.css('width', width + 'px')
$select.after($input)
// Hide original select but keep in DOM for value binding
$select.hide()
// Init autocomplete
$input.autocomplete({
minLength: 0,
source: buildSource(),
select: function (event, ui) {
try { event.preventDefault() } catch (e) {}
$input.val(ui.item.label)
$select.val(ui.item.value).trigger('change')
},
focus: function (event, ui) {
try { event.preventDefault() } catch (e) {}
$input.val(ui.item.label)
}
}).on('focus click', function () {
// Always show full list on click/focus
try { $(this).autocomplete('search', '') } catch (e) {}
})
// Initial sync
syncFromSelect($input)
// When select value changes programmatically, keep input in sync
$select.on('change', function () { syncFromSelect($input) })
$select.data('knx-searchable', true)
} else if (needsSync) {
// Only refresh source and text
const $input = $('#' + id + '-search')
syncFromSelect($input)
}
} catch (e) { }
}
// Auto-enhance any select whose id contains 'dpt'
(function () {
if (window.__knx_dptObserverSetup) return; window.__knx_dptObserverSetup = true
function enhance (root) {
try {
$(root).find('select[id*="dpt"]').each(function () { KNX_makeSelectSearchable($(this)) })
} catch (e) {}
}
try {
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (m) {
if (!m.addedNodes) return
m.addedNodes.forEach(function (n) {
if (n.nodeType !== 1) return
// If a select is added
if (n.matches && n.matches('select[id*="dpt"]')) { enhance(n); return }
// If options are added to an existing select
if (n.matches && n.matches('option')) {
const sel = n.closest && n.closest('select')
if (sel && sel.id && sel.id.indexOf('dpt') > -1) { KNX_makeSelectSearchable($(sel)) }
return
}
enhance(n)
})
})
})
observer.observe(document.body, { childList: true, subtree: true })
// Initial pass
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { enhance(document.body) })
} else { enhance(document.body) }
} catch (e) {}
})()
window.KNX_makeSelectSearchable = KNX_makeSelectSearchable;
// Ensure autocomplete lists open immediately on focus/click
(function ($) {
if (!$.ui || !$.ui.autocomplete) return
const _create = $.ui.autocomplete.prototype._create
$.ui.autocomplete.prototype._create = function () {
_create.call(this)
const self = this
const showAll = (val) => {
if (self.options.disabled) return
const term = (typeof val === 'string') ? val : self.element.val() || ''
const minLen = self.options.minLength || 0
const query = term.length >= minLen ? term : ''
try {
self.search(query)
} catch (error) { }
}
this.element.on('focus.knxAutocomplete click.knxAutocomplete', function () {
clearTimeout(self._knxAutoTimer)
self._knxAutoTimer = setTimeout(function () { showAll() }, 0)
})
this.element.on('mousedown.knxAutocomplete', function () {
clearTimeout(self._knxAutoTimer)
self._knxAutoTimer = setTimeout(function () { showAll('') }, 0)
})
}
})(jQuery)