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.
719 lines (673 loc) • 29.8 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 $tabs = null;
let $requiresBridgeElems = null;
let $knxSections = null;
let $deviceName = null;
let $refreshButton = null;
let $loadingIndicator = null;
let $outputInfo = null;
let $toggleCheckbox = null;
let $statusRows = null;
let $fixedValueSection = null;
let $switchSendInput = null;
let $dimSendInput = null;
let $dptShortRelease = null;
let $dptShortReleaseStatus = null;
let $dptRepeat = null;
let $gaShortRelease = null;
let $gaShortReleaseStatus = null;
let $gaRepeat = null;
let cachedDevices = [];
let defaultDevicePlaceholder = '';
let showingNoDevicesPlaceholder = false;
let currentNode = null;
const EMPTY_SERVER_VALUES = new Set(['', 'none', '_add_', '__none__', '__null__', 'null', 'undefined']);
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 ($deviceName) {
$deviceName.off('.knxUltimateHueButton');
if ($deviceName.data('ui-autocomplete')) {
try { $deviceName.autocomplete('destroy'); } catch (error) { /* empty */ }
}
}
if ($refreshButton) {
$refreshButton.off('.knxUltimateHueButton');
}
if ($toggleCheckbox) {
$toggleCheckbox.off('.knxUltimateHueButton');
}
const autocompleteTargets = [$gaShortRelease, $gaShortReleaseStatus, $gaRepeat];
autocompleteTargets.forEach(($input) => {
if ($input) {
$input.off('.knxUltimateHueButton');
if ($input.data('ui-autocomplete')) {
try { $input.autocomplete('destroy'); } catch (error) { /* empty */ }
}
}
});
if ($switchSendInput && $switchSendInput.data('typedInput')) {
try { $switchSendInput.typedInput('destroy'); } catch (error) { /* empty */ }
}
if ($dimSendInput && $dimSendInput.data('typedInput')) {
try { $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 (!$deviceName) return;
if (hasDevices) {
if (showingNoDevicesPlaceholder) {
showingNoDevicesPlaceholder = false;
$deviceName.attr('placeholder', defaultDevicePlaceholder);
}
return;
}
const message = RED._('node-red-contrib-knx-ultimate/knxUltimateHueButton:knxUltimateHueButton.no_devices');
showingNoDevicesPlaceholder = true;
$deviceName.attr('placeholder', message);
if (($deviceName.val() || '').trim() === '') {
$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 ($loadingIndicator) $loadingIndicator.show();
$.getJSON(`KNXUltimateGetResourcesHUE?rtype=button&serverId=${hueServer.id}&_=${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 ($loadingIndicator) $loadingIndicator.hide();
}).fail(() => {
cachedDevices = [];
if (currentNode) currentNode._cachedButtonDevices = cachedDevices;
applyNoDevicesPlaceholder(false);
response([]);
});
};
const loadDptOptions = (serverId, nodeRef) => {
if (!$dptShortRelease || !$dptShortReleaseStatus || !$dptRepeat) return;
$dptShortRelease.empty();
$dptShortReleaseStatus.empty();
$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();
$dptShortRelease.append(option);
$dptShortReleaseStatus.append(optionStatus);
}
if (dpt.value.startsWith('3.007')) {
$dptRepeat.append($('<option></option>').attr('value', dpt.value).text(dpt.text));
}
});
if ($dptShortRelease.children().length) {
$dptShortRelease.val(targetShort);
}
if ($dptShortReleaseStatus.children().length) {
$dptShortReleaseStatus.val(targetShortStatus);
}
if ($dptRepeat.children().length) {
$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 = $toggleCheckbox && $toggleCheckbox.is(':checked');
if ($statusRows) {
if (toggled) {
$statusRows.show();
} else {
$statusRows.hide();
}
}
if ($fixedValueSection) {
if (toggled) {
$fixedValueSection.hide();
} else {
$fixedValueSection.show();
}
}
};
const updateTabsVisibility = () => {
if (!$tabs) return;
const hueDomValue = $('#node-input-serverHue').val();
const hueServerId = resolveServerId(hueDomValue === undefined ? (currentNode ? currentNode.serverHue : null) : hueDomValue);
const knxSelected = hasKNXServerSelected();
if (hueServerId) {
$requiresBridgeElems.show();
} else {
$requiresBridgeElems.hide();
}
if (hueServerId && knxSelected) {
$tabs.show();
$tabs.tabs('refresh');
} else {
$tabs.hide();
}
if ($outputInfo) {
if (knxSelected) {
$outputInfo.hide();
} else {
$outputInfo.show();
}
}
};
const updateKNXVisibility = () => {
const knxSelected = hasKNXServerSelected();
if (knxSelected) {
$knxSections.show();
} else {
$knxSections.hide();
}
updateTabsVisibility();
};
RED.nodes.registerType('knxUltimateHueButton', {
category: 'KNX Ultimate',
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();
$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 = $deviceName.attr('placeholder') || '';
showingNoDevicesPlaceholder = false;
$tabs.addClass('hue-vertical-tabs');
$tabs.tabs();
$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: $gaShortRelease,
$name: $('#node-input-nameshort_release'),
$dptSelect: $dptShortRelease,
filterFn: (value) => value.dpt && value.dpt.startsWith('1.'),
});
attachGroupAddressAutocomplete({
$input: $gaShortReleaseStatus,
$name: $('#node-input-nameshort_releaseStatus'),
$dptSelect: $dptShortReleaseStatus,
filterFn: (value) => value.dpt && value.dpt.startsWith('1.'),
});
attachGroupAddressAutocomplete({
$input: $gaRepeat,
$name: $('#node-input-nameDim'),
$dptSelect: $dptRepeat,
filterFn: (value) => value.dpt && value.dpt.startsWith('3.007'),
});
if ($switchSendInput) {
$switchSendInput.typedInput({
type: 'bool',
types: ['bool'],
});
const initialSwitch = node.switchSend === undefined ? true : node.switchSend;
$switchSendInput.typedInput('value', initialSwitch ? 'true' : 'false');
}
if ($dimSendInput) {
$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';
$dimSendInput.typedInput('value', initialDim || 'up');
}
const initialToggle = node.toggleValues !== false;
if ($toggleCheckbox) {
$toggleCheckbox.prop('checked', initialToggle);
$toggleCheckbox.on('change.knxUltimateHueButton', () => {
updateToggleSections();
});
}
updateToggleSections();
if ($deviceName) {
$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);
},
});
$deviceName.on('focus.knxUltimateHueButton', function () {
$(this).autocomplete('search', `${$(this).val()}exactmatch`);
});
}
if ($refreshButton) {
$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 ($deviceName) {
$deviceName.autocomplete('search', `${$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 ($loadingIndicator) $loadingIndicator.hide();
showingNoDevicesPlaceholder = false;
if ($deviceName) {
$deviceName.attr('placeholder', defaultDevicePlaceholder);
}
if (!hueServerId) {
applyNoDevicesPlaceholder(true);
}
updateTabsVisibility();
});
updateKNXVisibility();
},
oneditsave() {
try { RED.sidebar.show('info'); } catch (error) { /* empty */ }
detachHandlers();
cachedDevices = [];
this._cachedButtonDevices = [];
this.toggleValues = $toggleCheckbox ? $toggleCheckbox.is(':checked') : true;
currentNode = null;
},
oneditcancel() {
try { RED.sidebar.show('info'); } catch (error) { /* empty */ }
detachHandlers();
cachedDevices = [];
this._cachedButtonDevices = [];
currentNode = 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>