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, and KNX routing between interfaces. Easy to use and highly configurable.
326 lines (287 loc) • 16.6 kB
HTML
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
<script type="text/javascript">
RED.nodes.registerType('knxUltimateStaircase', {
category: 'KNX Ultimate',
color: '#C7E9C0',
defaults: {
server: { type: 'knxUltimate-config', required: true },
name: { value: '' },
outputtopic: { value: '' },
gaTrigger: { value: '', required: true },
nameTrigger: { value: '' },
dptTrigger: { value: '1.001' },
gaOutput: { value: '', required: true },
nameOutput: { value: '' },
dptOutput: { value: '1.001' },
gaStatus: { value: '' },
nameStatus: { value: '' },
dptStatus: { value: '1.001' },
gaOverride: { value: '' },
nameOverride: { value: '' },
dptOverride: { value: '1.001' },
gaBlock: { value: '' },
nameBlock: { value: '' },
dptBlock: { value: '1.001' },
timerSeconds: { value: 120, required: true, validate: RED.validators.number() },
extendMode: { value: 'restart' },
triggerOffCancels: { value: 'yes' },
preWarnEnable: { value: false },
preWarnSeconds: { value: 15 },
preWarnMode: { value: 'status' },
preWarnFlashMs: { value: 400 },
blockAction: { value: 'off' },
emitEvents: { value: false }
},
inputs: 1,
outputs: 1,
icon: 'node-knx-icon.svg',
label: function () {
return this.name || 'KNX Staircase';
},
paletteLabel: 'KNX Staircase',
oneditprepare: function () {
const node = this;
const $knxServerInput = $('#node-input-server');
try { RED.sidebar.show('help'); } catch (error) { /* ignore */ }
const KNX_EMPTY_VALUES = new Set(['', '_ADD_', '__NONE__', 'none']);
const resolveKnxServerValue = () => {
const domValue = $knxServerInput.val();
if (domValue !== undefined && domValue !== null && domValue !== '') return domValue;
if (node.server !== undefined && node.server !== null && node.server !== '') return node.server;
return '';
};
const hasKnxServerSelected = () => {
const val = resolveKnxServerValue();
return !(val === undefined || val === null || KNX_EMPTY_VALUES.has(String(val)));
};
const KNX_GA_CACHE = node._knxGaCache || (node._knxGaCache = new Map());
const fetchGroupAddresses = (serverId) => {
if (!serverId) return Promise.resolve([]);
if (KNX_GA_CACHE.has(serverId)) return Promise.resolve(KNX_GA_CACHE.get(serverId));
return new Promise((resolve) => {
$.getJSON(`knxUltimatecsv?nodeID=${serverId}&_=${Date.now()}`, (data) => {
const list = Array.isArray(data) ? data : [];
KNX_GA_CACHE.set(serverId, list);
resolve(list);
}).fail(() => resolve([]));
});
};
const getGroupAddress = (gaSelector, nameSelector, dptSelector, prefixes) => {
const $gaInput = $(gaSelector);
const $nameInput = $(nameSelector);
const $dptInput = $(dptSelector);
if (!$gaInput.length) return;
const ensureAutocomplete = () => {
const sourceFn = (request, response) => {
if (!hasKnxServerSelected()) {
response([]);
return;
}
const serverId = resolveKnxServerValue();
fetchGroupAddresses(serverId).then((data) => {
const items = [];
data.forEach((entry) => {
const dpt = entry.dpt || '';
const allowed = prefixes.some((prefix) => prefix === '' || dpt.startsWith(prefix));
if (!allowed) return;
const devName = entry.devicename || '';
const searchStr = `${entry.ga} (${devName}) DPT${dpt}`;
if (!htmlUtilsfullCSVSearch(searchStr, request.term || '')) return;
items.push({
label: `${entry.ga} # ${devName} # ${dpt}`,
value: entry.ga,
ga: entry.ga
});
});
response(items);
});
};
if ($gaInput.data('knx-ga-initialised')) {
$gaInput.autocomplete('option', 'source', sourceFn);
} else {
$gaInput
.autocomplete({
minLength: 0,
source: sourceFn,
select: (event, ui) => {
let deviceName = '';
try {
deviceName = ui.item.label.split('#')[1].trim();
deviceName = deviceName.replace(/^\)/, '').trim();
} catch (error) { deviceName = ''; }
if ($nameInput.length) {
if (deviceName && deviceName !== '') {
$nameInput.val(deviceName);
} else if (!$nameInput.val()) {
$nameInput.val('');
}
}
try {
const parts = ui.item.label.split('#');
const dptFromLabel = parts.length >= 3 ? parts[2].trim() : '';
if (dptFromLabel !== '') {
$dptInput.val(dptFromLabel);
}
} catch (error) { /* ignore */ }
}
})
.on('focus.knxUltimateStaircase click.knxUltimateStaircase', function () {
const currentValue = $(this).val() || '';
try { $(this).autocomplete('search', `${currentValue} exactmatch`); } catch (error) { /* ignore */ }
});
$gaInput.data('knx-ga-initialised', true);
}
try {
if (hasKnxServerSelected()) {
const srv = RED.nodes.node(resolveKnxServerValue());
if (srv && srv.id) KNX_enableSecureFormatting($gaInput, srv.id);
}
} catch (error) { /* ignore */ }
};
ensureAutocomplete();
};
const BINARY_PREFIX = ['1.'];
const ANY_ANALOG_PREFIX = ['1.', '2.', '5.', '6.', '7.', '8.', '9.', '12.', '13.', '14.', '16.', '20.'];
const refreshKnxBindings = () => {
if (!hasKnxServerSelected()) {
return;
}
getGroupAddress('#node-input-gaTrigger', '#node-input-nameTrigger', '#node-input-dptTrigger', BINARY_PREFIX);
getGroupAddress('#node-input-gaOutput', '#node-input-nameOutput', '#node-input-dptOutput', ANY_ANALOG_PREFIX);
getGroupAddress('#node-input-gaStatus', '#node-input-nameStatus', '#node-input-dptStatus', ANY_ANALOG_PREFIX);
getGroupAddress('#node-input-gaOverride', '#node-input-nameOverride', '#node-input-dptOverride', BINARY_PREFIX);
getGroupAddress('#node-input-gaBlock', '#node-input-nameBlock', '#node-input-dptBlock', BINARY_PREFIX);
};
$knxServerInput.on('change', () => {
KNX_GA_CACHE.clear();
refreshKnxBindings();
});
refreshKnxBindings();
const $preWarnRows = $('.knx-staircase-prewarn');
const $flashRow = $('.knx-staircase-prewarn-flash');
function syncPreWarnMode() {
const enabled = $('#node-input-preWarnEnable').is(':checked');
$preWarnRows.toggle(enabled);
if (!enabled) {
$flashRow.hide();
return;
}
const mode = $('#node-input-preWarnMode').val();
$flashRow.toggle(mode === 'flash');
}
$('#node-input-preWarnEnable').on('change', syncPreWarnMode);
$('#node-input-preWarnMode').on('change', syncPreWarnMode);
syncPreWarnMode();
}
});
</script>
<script type="text/html" data-template-name="knxUltimateStaircase">
<div class="form-row" style="display:flex; align-items:center;">
<label for="node-input-server" style="width:180px"><i class="fa fa-circle-o"></i> <span data-i18n="knxUltimateStaircase.node-input-server"></span></label>
<input type="text" id="node-input-server" style="flex:1">
</div>
<div class="form-row" style="display:flex; align-items:center;">
<label for="node-input-name" style="width:180px"><i class="fa fa-tag"></i> <span data-i18n="knxUltimateStaircase.node-input-name"></span></label>
<input type="text" id="node-input-name" style="flex:1">
</div>
<div class="form-row" style="display:flex; align-items:center;">
<label for="node-input-outputtopic" style="width:180px"><i class="fa fa-comment"></i> <span data-i18n="knxUltimateStaircase.node-input-outputtopic"></span></label>
<input type="text" id="node-input-outputtopic" style="flex:1" placeholder="events/staircase" data-i18n="[placeholder]knxUltimateStaircase.placeholders.outputtopic">
</div>
<hr>
<div class="form-row" style="margin:4px 0 2px;">
<span style="font-weight:bold;" data-i18n="knxUltimateStaircase.section_commands"></span>
</div>
<div class="form-row" style="display:flex; align-items:center; gap:8px;">
<label for="node-input-gaOutput" style="width:180px"><i class="fa fa-lightbulb-o"></i> <span data-i18n="knxUltimateStaircase.output"></span></label>
<input type="text" id="node-input-gaOutput" style="width:160px" placeholder="1/1/2" data-i18n="[placeholder]knxUltimateStaircase.placeholders.ga">
<input type="text" id="node-input-nameOutput" style="flex:1" placeholder="Staircase actuator" data-i18n="[placeholder]knxUltimateStaircase.placeholders.outputName">
<label for="node-input-dptOutput" style="width:60px; text-align:right">DPT</label>
<input type="text" id="node-input-dptOutput" style="width:160px" readonly>
</div>
<div class="form-row" style="display:flex; align-items:center; gap:8px;">
<label for="node-input-gaStatus" style="width:180px"><i class="fa fa-info-circle"></i> <span data-i18n="knxUltimateStaircase.status"></span></label>
<input type="text" id="node-input-gaStatus" style="width:160px" placeholder="1/1/3" data-i18n="[placeholder]knxUltimateStaircase.placeholders.ga">
<input type="text" id="node-input-nameStatus" style="flex:1" placeholder="Status LED" data-i18n="[placeholder]knxUltimateStaircase.placeholders.statusName">
<label for="node-input-dptStatus" style="width:60px; text-align:right">DPT</label>
<input type="text" id="node-input-dptStatus" style="width:160px" readonly>
</div>
<div style="border-top:1px solid #ccc; margin:10px 0 6px;"></div>
<div class="form-row" style="margin:4px 0 2px;">
<span style="font-weight:bold;" data-i18n="knxUltimateStaircase.section_inputs"></span>
</div>
<div class="form-row" style="display:flex; align-items:center; gap:8px;">
<label for="node-input-gaTrigger" style="width:180px"><i class="fa fa-bolt"></i> <span data-i18n="knxUltimateStaircase.trigger"></span></label>
<input type="text" id="node-input-gaTrigger" style="width:160px" placeholder="1/1/1" data-i18n="[placeholder]knxUltimateStaircase.placeholders.ga">
<input type="text" id="node-input-nameTrigger" style="flex:1" placeholder="Living room switch" data-i18n="[placeholder]knxUltimateStaircase.placeholders.triggerName">
<label for="node-input-dptTrigger" style="width:60px; text-align:right">DPT</label>
<input type="text" id="node-input-dptTrigger" style="width:160px" readonly>
</div>
<div class="form-row" style="display:flex; align-items:center; gap:8px;">
<label for="node-input-gaOverride" style="width:180px"><i class="fa fa-toggle-on"></i> <span data-i18n="knxUltimateStaircase.override"></span></label>
<input type="text" id="node-input-gaOverride" style="width:160px" placeholder="1/1/4" data-i18n="[placeholder]knxUltimateStaircase.placeholders.ga">
<input type="text" id="node-input-nameOverride" style="flex:1" placeholder="Maintenance override" data-i18n="[placeholder]knxUltimateStaircase.placeholders.overrideName">
<label for="node-input-dptOverride" style="width:60px; text-align:right">DPT</label>
<input type="text" id="node-input-dptOverride" style="width:160px" readonly>
</div>
<div class="form-row" style="display:flex; align-items:center; gap:8px;">
<label for="node-input-gaBlock" style="width:180px"><i class="fa fa-hand-paper-o"></i> <span data-i18n="knxUltimateStaircase.block"></span></label>
<input type="text" id="node-input-gaBlock" style="width:160px" placeholder="1/1/5" data-i18n="[placeholder]knxUltimateStaircase.placeholders.ga">
<input type="text" id="node-input-nameBlock" style="flex:1" placeholder="Block command" data-i18n="[placeholder]knxUltimateStaircase.placeholders.blockName">
<label for="node-input-dptBlock" style="width:60px; text-align:right">DPT</label>
<input type="text" id="node-input-dptBlock" style="width:160px" readonly>
</div>
<hr>
<div class="form-row" style="display:flex; align-items:center;">
<label for="node-input-timerSeconds" style="width:180px"><i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateStaircase.node-input-timerSeconds"></span></label>
<input type="number" id="node-input-timerSeconds" style="width:140px" min="1">
</div>
<div class="form-row" style="display:flex; align-items:center;">
<label for="node-input-extendMode" style="width:180px"><i class="fa fa-repeat"></i> <span data-i18n="knxUltimateStaircase.node-input-extendMode"></span></label>
<select id="node-input-extendMode" style="flex:1">
<option value="restart" data-i18n="knxUltimateStaircase.extend_restart"></option>
<option value="extend" data-i18n="knxUltimateStaircase.extend_extend"></option>
<option value="ignore" data-i18n="knxUltimateStaircase.extend_ignore"></option>
</select>
</div>
<div class="form-row" style="display:flex; align-items:center;">
<label for="node-input-triggerOffCancels" style="width:180px"><i class="fa fa-power-off"></i> <span data-i18n="knxUltimateStaircase.node-input-triggerOffCancels"></span></label>
<select id="node-input-triggerOffCancels" style="flex:1">
<option value="yes" data-i18n="knxUltimateStaircase.opt_yes"></option>
<option value="no" data-i18n="knxUltimateStaircase.opt_no"></option>
</select>
</div>
<div class="form-row" style="display:flex; align-items:center;">
<label for="node-input-blockAction" style="width:180px"><i class="fa fa-ban"></i> <span data-i18n="knxUltimateStaircase.node-input-blockAction"></span></label>
<select id="node-input-blockAction" style="flex:1">
<option value="off" data-i18n="knxUltimateStaircase.block_off"></option>
<option value="keep" data-i18n="knxUltimateStaircase.block_keep"></option>
</select>
</div>
<hr>
<div class="form-row" style="display:flex; align-items:center;">
<label for="node-input-preWarnEnable" style="width:180px"><i class="fa fa-exclamation-triangle"></i> <span data-i18n="knxUltimateStaircase.node-input-preWarnEnable"></span></label>
<input type="checkbox" id="node-input-preWarnEnable" style="width:auto">
</div>
<div class="form-row knx-staircase-prewarn" style="display:flex; align-items:center;">
<label for="node-input-preWarnSeconds" style="width:180px"><i class="fa fa-hourglass-end"></i> <span data-i18n="knxUltimateStaircase.node-input-preWarnSeconds"></span></label>
<input type="number" id="node-input-preWarnSeconds" style="width:140px" min="1">
</div>
<div class="form-row knx-staircase-prewarn" style="display:flex; align-items:center;">
<label for="node-input-preWarnMode" style="width:180px"><i class="fa fa-bullhorn"></i> <span data-i18n="knxUltimateStaircase.node-input-preWarnMode"></span></label>
<select id="node-input-preWarnMode" style="flex:1">
<option value="status" data-i18n="knxUltimateStaircase.prewarn_status"></option>
<option value="flash" data-i18n="knxUltimateStaircase.prewarn_flash"></option>
</select>
</div>
<div class="form-row knx-staircase-prewarn knx-staircase-prewarn-flash" style="display:flex; align-items:center;">
<label for="node-input-preWarnFlashMs" style="width:180px"><i class="fa fa-lightbulb-o"></i> <span data-i18n="knxUltimateStaircase.node-input-preWarnFlashMs"></span></label>
<input type="number" id="node-input-preWarnFlashMs" style="width:140px" min="50">
</div>
<hr>
<div class="form-row" style="display:flex; align-items:center;">
<label for="node-input-emitEvents" style="width:180px"><i class="fa fa-sign-out"></i> <span data-i18n="knxUltimateStaircase.node-input-emitEvents"></span></label>
<input type="checkbox" id="node-input-emitEvents" style="width:auto">
</div>
<br/><br/><br/><br/>
</script>