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.
772 lines (710 loc) • 32.6 kB
HTML
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/11f26b4500.js"></script>
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
<script type="text/javascript">
(function () {
let ui = null;
let cachedDevices = [];
let defaultDevicePlaceholder = '';
let showingNoDevicesPlaceholder = false;
let currentNode = null;
const EMPTY_SERVER_VALUES = new Set(['', 'none', '_add_', '__none__', '__null__', 'null', 'undefined']);
const coerceBool = (value, defaultValue = false) => {
if (value === undefined || value === null) return defaultValue;
if (typeof value === 'boolean') return value;
if (typeof value === 'number') return value !== 0;
if (typeof value === 'string') {
const cleaned = value.trim().toLowerCase();
if (cleaned === '' || cleaned === '0' || cleaned === 'false' || cleaned === 'no' || cleaned === 'off') return false;
if (cleaned === '1' || cleaned === 'true' || cleaned === 'yes' || cleaned === 'on') return true;
}
return Boolean(value);
};
const ensureVerticalTabsStyle = () => {
if ($('#knxUltimateHueButtonVerticalTabs').length) return;
const style = `
<style id="knxUltimateHueButtonVerticalTabs">
.hue-vertical-tabs.ui-tabs.ui-widget.ui-widget-content.ui-corner-all {
display: flex;
border: none;
padding: 0;
}
.hue-vertical-tabs > ul.ui-tabs-nav {
flex: 0 0 144px;
border-right: 1px solid #ccc;
border-left: none;
border-top: none;
border-bottom: none;
padding: 0.5em 0.3em;
}
.hue-vertical-tabs > ul.ui-tabs-nav li {
float: none;
width: 100%;
margin: 0 0 2px 0;
}
.hue-vertical-tabs > ul.ui-tabs-nav li a {
display: block;
width: 100%;
white-space: nowrap;
position: relative;
border-bottom: none !important;
}
.hue-vertical-tabs > ul.ui-tabs-nav li.ui-tabs-active {
border-bottom: none !important;
}
.hue-vertical-tabs > ul.ui-tabs-nav li.ui-tabs-active a::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 50%;
height: 3px;
background: currentColor;
}
.hue-vertical-tabs .ui-tabs-panel {
flex: 1;
padding: 0.8em 1em;
box-sizing: border-box;
border: none;
background: transparent;
}
.hue-vertical-tabs .form-row {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 4px;
}
.hue-vertical-tabs .form-row > dt {
flex: 1 1 auto;
margin: 0;
}
.hue-vertical-tabs .hue-form-tip {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
margin-left: 0 !important;
max-width: none;
color: #1b7d33;
margin-bottom: 6px;
padding: 6px 10px;
box-sizing: border-box;
}
.hue-vertical-tabs .hue-form-tip .fa {
color: forestgreen;
flex: 0 0 auto;
}
.hue-vertical-tabs .hue-form-tip span {
flex: 1 1 auto;
min-width: 0;
white-space: normal;
}
</style>`;
$('head').append(style);
};
const detachHandlers = () => {
$('#node-input-server').off('.knxUltimateHueButton');
$('#node-input-serverHue').off('.knxUltimateHueButton');
if (ui?.deviceName) {
ui.deviceName.off('.knxUltimateHueButton');
if (ui.deviceName.data('ui-autocomplete')) {
try { ui.deviceName.autocomplete('destroy'); } catch (error) { /* empty */ }
}
}
if (ui?.refreshButton) {
ui.refreshButton.off('.knxUltimateHueButton');
}
if (ui?.toggleCheckbox) {
ui.toggleCheckbox.off('.knxUltimateHueButton');
}
const autocompleteTargets = [ui?.gaShortRelease, ui?.gaShortReleaseStatus, ui?.gaRepeat];
autocompleteTargets.forEach(($input) => {
if ($input) {
$input.off('.knxUltimateHueButton');
if ($input.data('ui-autocomplete')) {
try { $input.autocomplete('destroy'); } catch (error) { /* empty */ }
}
}
});
if (ui?.switchSendInput && ui.switchSendInput.data('typedInput')) {
try { ui.switchSendInput.typedInput('destroy'); } catch (error) { /* empty */ }
}
if (ui?.dimSendInput && ui.dimSendInput.data('typedInput')) {
try { ui.dimSendInput.typedInput('destroy'); } catch (error) { /* empty */ }
}
};
const ensureConfigSelection = (selector) => {
if ($(selector).val() !== '_ADD_') return;
try { $(selector).prop('selectedIndex', 0); } catch (error) { /* empty */ }
};
const resolveServerId = (value) => {
if (value === undefined || value === null) return null;
if (value === false) return null;
if (typeof value === 'string') {
const trimmed = value.trim();
if (trimmed === '') return null;
if (EMPTY_SERVER_VALUES.has(trimmed.toLowerCase())) return null;
return trimmed;
}
const asString = String(value).trim();
if (asString === '' || EMPTY_SERVER_VALUES.has(asString.toLowerCase())) return null;
return value;
};
const getKnxServer = (allowFallback = true) => {
const resolved = resolveServerId($('#node-input-server').val());
if (resolved) return RED.nodes.node(resolved);
if (!allowFallback) return null;
const fallback = resolveServerId(currentNode ? currentNode.server : null);
return fallback ? RED.nodes.node(fallback) : null;
};
const getHueServer = (allowFallback = true) => {
const resolved = resolveServerId($('#node-input-serverHue').val());
if (resolved) return RED.nodes.node(resolved);
if (!allowFallback) return null;
const fallback = resolveServerId(currentNode ? currentNode.serverHue : null);
return fallback ? RED.nodes.node(fallback) : null;
};
const hasKnxSelection = () => {
const resolved = resolveServerId($('#node-input-server').val());
if (resolved) return true;
if ($('#node-input-server').length) return false;
return resolveServerId(currentNode ? currentNode.server : null) !== null;
};
const hasHueSelection = () => {
const resolved = resolveServerId($('#node-input-serverHue').val());
if (resolved) return true;
if ($('#node-input-serverHue').length) return false;
return resolveServerId(currentNode ? currentNode.serverHue : null) !== null;
};
const applyNoDevicesPlaceholder = (hasDevices) => {
if (!ui?.deviceName) return;
if (hasDevices) {
if (showingNoDevicesPlaceholder) {
showingNoDevicesPlaceholder = false;
ui.deviceName.attr('placeholder', defaultDevicePlaceholder);
}
return;
}
const message = RED._('node-red-contrib-knx-ultimate/knxUltimateHueButton:knxUltimateHueButton.no_devices');
showingNoDevicesPlaceholder = true;
ui.deviceName.attr('placeholder', message);
if ((ui.deviceName.val() || '').trim() === '') {
ui.deviceName.val('');
}
};
const filterDevices = (devices, term) => {
const cleaned = (term || '').replace(/exactmatch/gi, '').trim();
return $.map(devices, (value) => {
const sSearch = value.name;
if (cleaned === '' || htmlUtilsfullCSVSearch(sSearch, cleaned)) {
return {
hueDevice: value.id,
value: value.name,
deviceObject: value.deviceObject || value,
};
}
return null;
});
};
const fetchDevices = (hueServer, term, response, { forceRefresh = false } = {}) => {
if (!hueServer) {
applyNoDevicesPlaceholder(true);
response([]);
return;
}
if (!forceRefresh && cachedDevices.length > 0) {
applyNoDevicesPlaceholder(cachedDevices.length > 0);
response(filterDevices(cachedDevices, term));
return;
}
if (ui?.loadingIndicator) ui.loadingIndicator.show();
const refreshQuery = forceRefresh ? '&forceRefresh=1' : '';
$.getJSON(`KNXUltimateGetResourcesHUE?rtype=button&serverId=${encodeURIComponent(hueServer.id)}${refreshQuery}&_=${Date.now()}`, (data) => {
const listCandidates = Array.isArray(data) ? data : (Array.isArray(data?.devices) ? data.devices : []);
cachedDevices = listCandidates.map((value) => {
if (value.deviceObject) return value;
return {
id: value.id || value.rid,
name: value.name || value.metadata?.name || '',
deviceObject: value,
};
});
if (currentNode) currentNode._cachedButtonDevices = cachedDevices;
applyNoDevicesPlaceholder(cachedDevices.length > 0);
response(filterDevices(cachedDevices, term));
}).always(() => {
if (ui?.loadingIndicator) ui.loadingIndicator.hide();
}).fail(() => {
cachedDevices = [];
if (currentNode) currentNode._cachedButtonDevices = cachedDevices;
applyNoDevicesPlaceholder(false);
response([]);
});
};
const loadDptOptions = (serverId, nodeRef) => {
if (!ui?.dptShortRelease || !ui?.dptShortReleaseStatus || !ui?.dptRepeat) return;
ui.dptShortRelease.empty();
ui.dptShortReleaseStatus.empty();
ui.dptRepeat.empty();
const validId = resolveServerId(serverId);
if (!validId) {
return;
}
$.getJSON(`knxUltimateDpts?serverId=${validId}`, (data) => {
const referenceNode = nodeRef || currentNode || {};
const targetShort = referenceNode.dptshort_release || '1.001';
const targetShortStatus = referenceNode.dptshort_releaseStatus || referenceNode.dptshort_release || '1.001';
const targetRepeat = referenceNode.dptrepeat || '3.007';
data.forEach((dpt) => {
if (dpt.value.startsWith('1.')) {
const option = $('<option></option>').attr('value', dpt.value).text(dpt.text);
const optionStatus = option.clone();
ui.dptShortRelease.append(option);
ui.dptShortReleaseStatus.append(optionStatus);
}
if (dpt.value.startsWith('3.007')) {
ui.dptRepeat.append($('<option></option>').attr('value', dpt.value).text(dpt.text));
}
});
if (ui.dptShortRelease.children().length) {
ui.dptShortRelease.val(targetShort);
}
if (ui.dptShortReleaseStatus.children().length) {
ui.dptShortReleaseStatus.val(targetShortStatus);
}
if (ui.dptRepeat.children().length) {
ui.dptRepeat.val(targetRepeat);
}
});
};
const attachGroupAddressAutocomplete = ({ $input, $name, $dptSelect, filterFn }) => {
if (!$input || !$input.length) return;
$input.autocomplete({
minLength: 0,
source(request, response) {
const rawValue = $('#node-input-server').val();
const serverId = resolveServerId(rawValue === undefined ? (currentNode ? currentNode.server : null) : rawValue);
const server = serverId ? RED.nodes.node(serverId) : null;
if (!server) { response([]); return; }
$.getJSON(`knxUltimatecsv?nodeID=${server.id}`, (data) => {
const matches = [];
data.forEach((value) => {
if (filterFn && !filterFn(value)) return;
const sSearch = `${value.ga} (${value.devicename}) DPT${value.dpt}`;
if (htmlUtilsfullCSVSearch(sSearch, request.term)) {
matches.push({
label: `${value.ga} # ${value.devicename} # ${value.dpt}`,
value: value.ga,
});
}
});
response(matches);
});
},
select(event, ui) {
let sDevName = ui.item.label.split('#')[1]?.trim() || '';
try {
sDevName = sDevName.substr(sDevName.indexOf(')') + 1).trim();
} catch (error) { /* empty */ }
if ($name) $name.val(sDevName);
if ($dptSelect) {
const dptLabel = ui.item.label.split('#')[2]?.trim();
const optVal = dptLabel ? $dptSelect.find(`option:contains('${dptLabel}')`).attr('value') : undefined;
if (optVal !== undefined && optVal !== null) {
$dptSelect.val(optVal).trigger('change');
} else {
$dptSelect.trigger('change');
}
}
},
});
$input.on('focus.knxUltimateHueButton', function () {
$(this).autocomplete('search', `${$(this).val()}exactmatch`);
});
try {
const serverId = resolveServerId($('#node-input-server').val() || (currentNode ? currentNode.server : null));
const server = serverId ? RED.nodes.node(serverId) : null;
if (server && server.id) KNX_enableSecureFormatting($input, server.id);
} catch (error) { /* empty */ }
};
const hasKNXServerSelected = () => {
let domValue = $('#node-input-server').val();
if (domValue === undefined && currentNode) domValue = currentNode.server;
const knxServerId = resolveServerId(domValue);
return Boolean(knxServerId);
};
const updateToggleSections = () => {
const toggled = ui?.toggleCheckbox ? ui.toggleCheckbox.is(':checked') : false;
if (ui?.statusRows) {
if (toggled) {
ui.statusRows.show();
} else {
ui.statusRows.hide();
}
}
if (ui?.fixedValueSection) {
if (toggled) {
ui.fixedValueSection.hide();
} else {
ui.fixedValueSection.show();
}
}
};
const updateTabsVisibility = () => {
if (!ui?.tabs) return;
const hueDomValue = $('#node-input-serverHue').val();
const hueServerId = resolveServerId(hueDomValue === undefined ? (currentNode ? currentNode.serverHue : null) : hueDomValue);
const knxSelected = hasKNXServerSelected();
if (hueServerId) {
ui.requiresBridgeElems?.show();
} else {
ui.requiresBridgeElems?.hide();
}
if (hueServerId && knxSelected) {
ui.tabs.show();
ui.tabs.tabs('refresh');
} else {
ui.tabs.hide();
}
if (ui?.outputInfo) {
if (knxSelected) {
ui.outputInfo.hide();
} else {
ui.outputInfo.show();
}
}
};
const updateKNXVisibility = () => {
const knxSelected = hasKNXServerSelected();
if (knxSelected) {
ui?.knxSections?.show();
} else {
ui?.knxSections?.hide();
}
updateTabsVisibility();
};
RED.nodes.registerType('knxUltimateHueButton', {
category: 'KNX Ultimate HUE',
color: '#C0C7E9',
defaults: {
server: { type: 'knxUltimate-config', required: false },
serverHue: { type: 'hue-config', required: true },
name: { value: '' },
nameDim: { value: '' },
GArepeat: { value: '' },
dptrepeat: { value: '3.007' },
nameshort_release: { value: '' },
GAshort_release: { value: '' },
dptshort_release: { value: '1.001' },
nameshort_releaseStatus: { value: '' },
GAshort_releaseStatus: { value: '' },
dptshort_releaseStatus: { value: '1.001' },
toggleValues: { value: true },
hueDevice: { value: '' },
switchSend: { value: true },
dimSend: { value: 'up' },
},
inputs: 0,
outputs: 1,
icon: 'node-hue-icon.svg',
label() {
return this.name || 'Hue Button';
},
paletteLabel: 'Hue Button',
oneditprepare() {
try { RED.sidebar.show('help'); } catch (error) { /* empty */ }
const node = this;
currentNode = node;
ensureConfigSelection('#node-input-serverHue');
ensureVerticalTabsStyle();
ui = {
tabs: $('#hue-button-tabs'),
requiresBridgeElems: $('.hue-requires-bridge'),
knxSections: $('.hue-knx-section'),
deviceName: $('#node-input-name'),
refreshButton: $('.hue-refresh-devices'),
loadingIndicator: $('.hue-devices-loading'),
outputInfo: $('.hue-output-info'),
toggleCheckbox: $('#node-input-toggleValues'),
statusRows: $('.hue-status-row'),
fixedValueSection: $('.hue-fixed-values'),
switchSendInput: $('#node-input-switchSend'),
dimSendInput: $('#node-input-dimSend'),
dptShortRelease: $('#node-input-dptshort_release'),
dptShortReleaseStatus: $('#node-input-dptshort_releaseStatus'),
dptRepeat: $('#node-input-dptrepeat'),
gaShortRelease: $('#node-input-GAshort_release'),
gaShortReleaseStatus: $('#node-input-GAshort_releaseStatus'),
gaRepeat: $('#node-input-GArepeat'),
};
cachedDevices = Array.isArray(node._cachedButtonDevices) ? node._cachedButtonDevices : [];
node._cachedButtonDevices = cachedDevices;
defaultDevicePlaceholder = ui.deviceName.attr('placeholder') || '';
showingNoDevicesPlaceholder = false;
ui.tabs.addClass('hue-vertical-tabs');
ui.tabs.tabs();
ui.tabs.find('li').removeClass('ui-corner-top').addClass('ui-corner-left');
const initialServerDomValue = $('#node-input-server').val();
const initialServerId = initialServerDomValue === undefined ? node.server : initialServerDomValue;
loadDptOptions(initialServerId, node);
attachGroupAddressAutocomplete({
$input: ui.gaShortRelease,
$name: $('#node-input-nameshort_release'),
$dptSelect: ui.dptShortRelease,
filterFn: (value) => value.dpt && value.dpt.startsWith('1.'),
});
attachGroupAddressAutocomplete({
$input: ui.gaShortReleaseStatus,
$name: $('#node-input-nameshort_releaseStatus'),
$dptSelect: ui.dptShortReleaseStatus,
filterFn: (value) => value.dpt && value.dpt.startsWith('1.'),
});
attachGroupAddressAutocomplete({
$input: ui.gaRepeat,
$name: $('#node-input-nameDim'),
$dptSelect: ui.dptRepeat,
filterFn: (value) => value.dpt && value.dpt.startsWith('3.007'),
});
if (ui.switchSendInput) {
ui.switchSendInput.typedInput({
type: 'bool',
types: ['bool'],
});
const initialSwitch = coerceBool(node.switchSend, true);
ui.switchSendInput.typedInput('value', initialSwitch ? 'true' : 'false');
}
if (ui.dimSendInput) {
ui.dimSendInput.typedInput({
type: 'direction',
types: [{
value: 'direction',
options: [
{ value: 'up', label: RED._('node-red-contrib-knx-ultimate/knxUltimateHueButton:knxUltimateHueButton.dim_up') || 'Up' },
{ value: 'down', label: RED._('node-red-contrib-knx-ultimate/knxUltimateHueButton:knxUltimateHueButton.dim_down') || 'Down' },
{ value: 'stop', label: RED._('node-red-contrib-knx-ultimate/knxUltimateHueButton:knxUltimateHueButton.dim_stop') || 'Stop' },
],
}],
});
const initialDim = typeof node.dimSend === 'string' ? node.dimSend : 'up';
ui.dimSendInput.typedInput('value', initialDim || 'up');
}
// If the stored value is missing/legacy, default to false so UI matches runtime truthiness.
const initialToggle = coerceBool(node.toggleValues, false);
if (ui.toggleCheckbox) {
ui.toggleCheckbox.prop('checked', initialToggle);
ui.toggleCheckbox.on('change.knxUltimateHueButton', () => {
updateToggleSections();
});
}
updateToggleSections();
if (ui.deviceName) {
ui.deviceName.autocomplete({
minLength: 0,
source(request, response) {
const hueDomValue = $('#node-input-serverHue').val();
const hueServerId = resolveServerId(hueDomValue === undefined ? node.serverHue : hueDomValue);
const hueServer = hueServerId ? RED.nodes.node(hueServerId) : null;
if (!hueServer) { response([]); return; }
fetchDevices(hueServer, request.term, response);
},
select(event, ui) {
$('#node-input-hueDevice').val(ui.item.hueDevice);
},
});
ui.deviceName.on('focus.knxUltimateHueButton', function () {
$(this).autocomplete('search', `${$(this).val()}exactmatch`);
});
}
if (ui.refreshButton) {
ui.refreshButton.on('click.knxUltimateHueButton', () => {
cachedDevices = [];
node._cachedButtonDevices = cachedDevices;
const hueDomValue = $('#node-input-serverHue').val();
const hueServerId = resolveServerId(hueDomValue === undefined ? node.serverHue : hueDomValue);
const hueServer = hueServerId ? RED.nodes.node(hueServerId) : null;
if (!hueServer) return;
fetchDevices(hueServer, '', () => {
if (ui?.deviceName) {
ui.deviceName.autocomplete('search', `${ui.deviceName.val()}exactmatch`);
}
}, { forceRefresh: true });
});
}
$('#node-input-server').on('change.knxUltimateHueButton', function () {
const serverId = $(this).val();
loadDptOptions(serverId, node);
updateKNXVisibility();
});
$('#node-input-serverHue').on('change.knxUltimateHueButton', function () {
const hueServerId = resolveServerId($(this).val());
cachedDevices = [];
node._cachedButtonDevices = cachedDevices;
if (ui?.loadingIndicator) ui.loadingIndicator.hide();
showingNoDevicesPlaceholder = false;
if (ui?.deviceName) {
ui.deviceName.attr('placeholder', defaultDevicePlaceholder);
}
if (!hueServerId) {
applyNoDevicesPlaceholder(true);
}
updateTabsVisibility();
});
updateKNXVisibility();
},
oneditsave() {
try { RED.sidebar.show('info'); } catch (error) { /* empty */ }
// Persist values explicitly because typedInput/checkbox widgets are not always serialised reliably by Node-RED.
this.toggleValues = ui?.toggleCheckbox ? ui.toggleCheckbox.is(':checked') : coerceBool(this.toggleValues, false);
const switchSendRaw = ui?.switchSendInput ? ui.switchSendInput.typedInput('value') : this.switchSend;
this.switchSend = (switchSendRaw === true || String(switchSendRaw).toLowerCase() === 'true') ? 'true' : 'false';
if (ui?.dimSendInput) {
const dimRaw = ui.dimSendInput.typedInput('value');
this.dimSend = typeof dimRaw === 'string' && dimRaw !== '' ? dimRaw : (this.dimSend || 'up');
}
detachHandlers();
cachedDevices = [];
this._cachedButtonDevices = [];
currentNode = null;
ui = null;
},
oneditcancel() {
try { RED.sidebar.show('info'); } catch (error) { /* empty */ }
detachHandlers();
cachedDevices = [];
this._cachedButtonDevices = [];
currentNode = null;
ui = null;
},
});
}());
</script>
<script type="text/html" data-template-name="knxUltimateHueButton">
<div class="form-row">
<label for="node-input-server">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAKnRFWHRDcmVhdGlvbiBUaW1lAEZyIDYgQXVnIDIwMTAgMjE6NTI6MTkgKzAxMDD84aS8AAAAB3RJTUUH3gYYCicNV+4WIQAAAAlwSFlzAAALEgAACxIB0t1+/AAAAARnQU1BAACxjwv8YQUAAACUSURBVHjaY2CgFZg5c+Z/ZEyWAZ8+f/6/ZsWs/xoamqMGkGrA6Wla/1+fVARjEBuGsSoGmY4eZSCNL59d/g8DIDbIAHR14OgFGQByKjIGKX5+6/T///8gGMQGiV1+/B0Fg70GIkD+RMYgxf/O5/7//2MSmAZhkBi6OrgB6Bg5DGB4ajr3f2xqsYYLSDE2THJUDg0AAAqyDVd4tp4YAAAAAElFTkSuQmCC" />
<span data-i18n="common.knx_gw"></span>
</label>
<input type="text" id="node-input-server">
</div>
<div class="form-row">
<label for="node-input-serverHue">
<img src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAEKADAAQAAAABAAAAEAAAAAA0VXHyAAABFUlEQVQ4EZWSsWoCQRCG1yiENEFEi6QSkjqWWoqFoBYJ+Br6JHkMn8Iibd4ihQpaJIhWNkry/ZtdGZY78Qa+m39nZ+dm9s4550awglNBluS/gVtAX6KgDclf68w2OThgfR9iT/jnoEv4TtByDThWTCDKW4SSZTf/zj9/eZbN+izTDuKGimu0vPF8B/YN8aC8LmcOj/AAn9CFTEs70Js/oGqy79C69bqJ5XbQI2kGO5N8QL9D08S8zBtBF5ZaVsznpCMoqJnVdjTpb1Db0fwIWmQV6BLXzFOYgA6/gDVfQN9bBWp2J2hdWDPoBV5FrKnAJutHikk/CHHR8i7x4iG7qQ720IYvu3GFbpHjx3pFrOFYkA354z/5bkK826phyAAAAABJRU5ErkJggg=="/>
<span data-i18n="common.hue_bridge"></span>
</label>
<input type="text" id="node-input-serverHue">
</div>
<div class="form-row hue-requires-bridge">
<label for="node-input-name">
<i class="fa fa-tag"></i> <span data-i18n="knxUltimateHueButton.hue_sensor"></span>
</label>
<input type="text" id="node-input-name" placeholder="Hue button" style="flex:1 1 240px; min-width:240px; max-width:240px;">
<button type="button" class="red-ui-button hue-refresh-devices" style="margin-left:6px; color:#1b7d33; border-color:#1b7d33;">
<i class="fa fa-sync"></i>
</button>
<span class="hue-devices-loading" style="margin-left:6px; display:none; color:#1b7d33;">
<i class="fa fa-circle-notch fa-spin"></i>
</span>
</div>
<div id="hue-button-tabs">
<ul>
<li><a href="#hue-button-tab-switch"><i class="fa fa-toggle-on"></i> <span data-i18n="knxUltimateHueButton.tabs.switch"></span></a></li>
<li><a href="#hue-button-tab-dim"><i class="fa fa-sun"></i> <span data-i18n="knxUltimateHueButton.tabs.dim"></span></a></li>
<li><a href="#hue-button-tab-behaviour"><i class="fa fa-gear"></i> <span data-i18n="knxUltimateHueButton.tabs.behaviour"></span></a></li>
</ul>
<div id="hue-button-tab-switch">
<div class="form-tips hue-form-tip hue-knx-section">
<i class="fa fa-circle-info"></i>
<span data-i18n="knxUltimateHueButton.switch_info"></span>
</div>
<div class="form-row hue-knx-section">
<label for="node-input-GAshort_release" style="width:70px;"><span data-i18n="common.ga"></span></label>
<input type="text" id="node-input-GAshort_release" style="width:80px; text-align:left;" placeholder="1/1/1">
<label for="node-input-dptshort_release" style="width:40px; text-align:right;"><span data-i18n="common.dpt"></span></label>
<select id="node-input-dptshort_release" style="width:120px;"></select>
<label for="node-input-nameshort_release" style="width:50px; text-align:right;"><span data-i18n="common.name"></span></label>
<input type="text" id="node-input-nameshort_release" style="flex:1 1 140px; min-width:120px; text-align:left;" placeholder="Switch action">
</div>
<div class="form-row hue-knx-section hue-status-row">
<label for="node-input-GAshort_releaseStatus" style="width:70px;"><span data-i18n="knxUltimateHueButton.switch_status"></span></label>
<input type="text" id="node-input-GAshort_releaseStatus" style="width:80px; text-align:left;" placeholder="1/1/2">
<label for="node-input-dptshort_releaseStatus" style="width:40px; text-align:right;"><span data-i18n="common.dpt"></span></label>
<select id="node-input-dptshort_releaseStatus" style="width:120px;"></select>
<label for="node-input-nameshort_releaseStatus" style="width:50px; text-align:right;"><span data-i18n="common.name"></span></label>
<input type="text" id="node-input-nameshort_releaseStatus" style="flex:1 1 140px; min-width:120px; text-align:left;" placeholder="Switch status">
</div>
</div>
<div id="hue-button-tab-dim">
<div class="form-tips hue-form-tip hue-knx-section">
<i class="fa fa-circle-info"></i>
<span data-i18n="knxUltimateHueButton.dim_info"></span>
</div>
<div class="form-row hue-knx-section">
<label for="node-input-GArepeat" style="width:70px;"><span data-i18n="common.ga"></span></label>
<input type="text" id="node-input-GArepeat" style="width:80px; text-align:left;" placeholder="1/1/3">
<label for="node-input-dptrepeat" style="width:40px; text-align:right;"><span data-i18n="common.dpt"></span></label>
<select id="node-input-dptrepeat" style="width:120px;"></select>
<label for="node-input-nameDim" style="width:50px; text-align:right;"><span data-i18n="common.name"></span></label>
<input type="text" id="node-input-nameDim" style="flex:1 1 140px; min-width:120px; text-align:left;" placeholder="Dim action">
</div>
</div>
<div id="hue-button-tab-behaviour">
<div class="form-tips hue-form-tip">
<i class="fa fa-circle-info"></i>
<span data-i18n="knxUltimateHueButton.behaviour_info"></span>
</div>
<div class="form-row">
<input type="checkbox" id="node-input-toggleValues" style="width:auto;">
<label for="node-input-toggleValues" style="flex:1 1 auto;">
<span data-i18n="knxUltimateHueButton.toggle_values"></span>
</label>
</div>
<div class="form-row hue-status-row" style="margin-left:24px;">
<span data-i18n="knxUltimateHueButton.toggle_values_hint"></span>
</div>
<div class="hue-fixed-values" style="margin-top:8px;">
<div class="form-row">
<label for="node-input-switchSend" style="width:130px;"><span data-i18n="knxUltimateHueButton.switch_send"></span></label>
<input type="text" id="node-input-switchSend" style="width:160px;">
</div>
<div class="form-row">
<label for="node-input-dimSend" style="width:130px;"><span data-i18n="knxUltimateHueButton.dim_send"></span></label>
<input type="text" id="node-input-dimSend" style="width:160px;">
</div>
</div>
</div>
</div>
<div class="form-tips hue-form-tip hue-output-info" style="display:none;">
<i class="fa fa-circle-info"></i>
<span data-i18n="knxUltimateHueButton.output_info"></span>
</div>
<input type="hidden" id="node-input-hueDevice">
</script>
<script type="text/markdown" data-help-name="knxUltimateHueButton">
<p>The Hue Button node maps Hue button events to KNX group addresses and exposes the same events on its flow output via <code>button.button_report.event</code>.</p>
Start typing in the GA field (name or Group Address) to link the KNX GA; devices appear while you type.
**General**
|Property|Description|
|--|--|
| KNX GW | Select the KNX gateway to be used |
| Hue Bridge | Select the Hue Bridge to be used |
| Hue Button | Hue button to be used (autocomplete while typing) |
**Switch**
|Property|Description|
|--|--|
| Switch | GA triggered by <code>short\_release</code> (quick press/release). |
| Status GA | Optional feedback GA when <em>Toggle values</em> is enabled to keep the internal toggle state aligned with other actuators. |
**Dim**
|Property|Description|
|--|--|
| Dim | GA used during <code>long\_press</code>/<code>repeat</code> events for dimming (typically DPT 3.007). |
**Behaviour**
|Property|Description|
|--|--|
| Toggle values on each event | If enabled, the node alternates between <code>true/false</code> and up/down dimming payloads. |
| Switch payload | Payload sent to KNX/flow when Toggle values is disabled. |
| Dim payload | Direction sent to KNX/flow when Toggle values is disabled. |
### Outputs
1. Standard output
: `msg.payload` carries the boolean (or dim object) sent to KNX; `msg.event` is the Hue event string (e.g. `short_release`, `repeat`).
### Details
`msg.event` mirrors `button.button_report.event`. The original Hue event is exposed in `msg.rawEvent`. Use the optional Status GA to keep the toggle state in sync with wall switches or other controllers.
</script>