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, KNX AI for diagnosticsand KNX routing between interfaces. Easy to use and highly configurable.
266 lines (250 loc) • 9.78 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 {
if (!$input || !$input.length) return
try {
$input.data('knxGaAutocomplete', true)
} catch (e) { /* empty */ }
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
function KNX_cleanAutocompleteTerm (term) {
if (typeof term !== 'string') return ''
return term.replace(/exactmatch/gi, ' ').replace(/\s+/g, ' ').trim()
}
window.KNX_cleanAutocompleteTerm = KNX_cleanAutocompleteTerm
// 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)
const isDptCombobox = $(this).hasClass('knx-dpt-combobox')
self._knxAutoTimer = setTimeout(function () {
// DPT comboboxes must keep the full dropdown open even when an existing value is clicked.
showAll(isDptCombobox ? '' : undefined)
}, 0)
})
this.element.on('mousedown.knxAutocomplete', function () {
clearTimeout(self._knxAutoTimer)
self._knxAutoTimer = setTimeout(function () { showAll('') }, 0)
})
}
const _normalize = $.ui.autocomplete.prototype._normalize
$.ui.autocomplete.prototype._normalize = function (items) {
let normalized
try {
normalized = _normalize.call(this, items)
} catch (error) {
normalized = []
}
if (Array.isArray(normalized) && normalized.length > 0) {
return normalized
}
const $element = this.element
if (!$element || !$element.length) {
return Array.isArray(normalized) ? normalized : []
}
const isGaAutocomplete = !!$element.data('knxGaAutocomplete')
if (!isGaAutocomplete) {
return Array.isArray(normalized) ? normalized : []
}
const rawTerm = (typeof this.term === 'string') ? this.term : ''
const cleanedTerm = KNX_cleanAutocompleteTerm(rawTerm)
if (!cleanedTerm) {
return Array.isArray(normalized) ? normalized : []
}
return [{
label: `${cleanedTerm} # # `,
value: cleanedTerm,
ga: cleanedTerm,
devicename: '',
dpt: '',
isSecure: false,
__knxFallback: true
}]
}
})(jQuery)