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.
927 lines (872 loc) • 92.2 kB
HTML
<!-- <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/jquery.searchableSelect.js"></script> -->
<style>
/* Monaco editor - first area (input to bus): light green */
#sendMsgToKNXCode-editor .monaco-editor,
#sendMsgToKNXCode-editor .monaco-editor-background,
#sendMsgToKNXCode-editor .monaco-editor .margin,
#sendMsgToKNXCode-editor .monaco-editor .overflow-guard,
#sendMsgToKNXCode-editor .monaco-editor .lines-content,
#sendMsgToKNXCode-editor .monaco-editor .editor-scrollable {
background-color: #e8f5e9 !important;
}
/* Monaco editor - second area (bus to output): light yellow */
#receiveMsgFromKNXCode-editor .monaco-editor,
#receiveMsgFromKNXCode-editor .monaco-editor-background,
#receiveMsgFromKNXCode-editor .monaco-editor .margin,
#receiveMsgFromKNXCode-editor .monaco-editor .overflow-guard,
#receiveMsgFromKNXCode-editor .monaco-editor .lines-content,
#receiveMsgFromKNXCode-editor .monaco-editor .editor-scrollable {
background-color: #fffde7 !important;
}
</style>
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/KNXSendSnippets.js"></script>
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/KNXReceiveSnippets.js"></script>
<script type="text/javascript">
RED.nodes.registerType('knxUltimate', {
category: "KNX Ultimate",
color: '#7dd484',
defaults: {
//buttonState: {value: true},
server: { type: "knxUltimate-config", required: true },
topic: { value: "" },
setTopicType: { value: "str" },
outputtopic: { value: "" },
dpt: { value: "" },
initialread: { value: 0 },
notifyreadrequest: { value: false },
notifyresponse: { value: false },
notifywrite: { value: true },
notifyreadrequestalsorespondtobus: { value: false },
notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized: { value: "0" },
name: { value: "" },
outputtype: { value: "write" },
outputRBE: { value: "true" },
inputRBE: { value: "false" },
formatmultiplyvalue: { value: 1 },
formatnegativevalue: { value: "leave" },
formatdecimalsvalue: { value: 999 },
passthrough: { value: "no" },
sendMsgToKNXCode: { value: "" },
receiveMsgFromKNXCode: { value: "" },
listenallga: { value: "" },
gaSecure: { value: false },
buttonEnabled: { value: true },
buttonMode: { value: "toggle" },
buttonStaticValue: { value: "" },
buttonToggleInitial: { value: "false" },
periodicSend: { value: false },
periodicSendInterval: { value: 60 }
},
inputs: 1,
outputs: 1,
icon: function () {
try {
return (this.gaSecure === true || this.gaSecure === 'true') ? "node-knx-icon-secure.svg" : "node-knx-icon.svg";
} catch (e) {
return "node-knx-icon.svg";
}
},
label: function () {
const functionSendMsgToKNXCode = (this.sendMsgToKNXCode !== undefined && this.sendMsgToKNXCode !== '') ? "f(x) " : "";
const functionreceiveMsgFromKNXCode = (this.receiveMsgFromKNXCode !== undefined && this.receiveMsgFromKNXCode !== '') ? " f(x)" : "";
return ((this.outputRBE === "true" || this.outputRBE === true) ? "|rbe| " : "") + functionSendMsgToKNXCode + (this.name || this.topic || "KNX Device") + (this.setTopicType === 'str' || this.setTopicType === undefined ? '' : ' [' + (this.setTopicType === 'listenAllGA' ? 'Universal' : this.setTopicType) + ']') + functionreceiveMsgFromKNXCode + ((this.inputRBE === "true" || this.inputRBE === true) ? " |rbe|" : "")
},
paletteLabel: "KNX DEVICE",
button: {
enabled: function () {
return !this.changed;
},
visible: function () {
const isUniversal = this.setTopicType === 'listenAllGA' || this.listenallga === true || this.listenallga === 'true';
if (isUniversal) return false;
return this.buttonEnabled === true || this.buttonEnabled === "true";
},
onclick: function () {
const node = this;
const mode = node.buttonMode || 'read';
const request = { id: node.id, mode };
if (mode === 'value') {
request.value = node.buttonStaticValue;
}
$.ajax({
type: "POST",
url: "knxUltimate/buttonAction",
data: request,
success: function (response) {
const key = mode === 'read' ? "manualReadOk" : "manualWriteOk";
const baseMessage = RED._("node-red-contrib-knx-ultimate/knxUltimate:knxUltimate." + key) || "KNX command sent";
let notifyMessage = baseMessage;
if (mode !== 'read') {
let payloadValue;
if (response && Object.prototype.hasOwnProperty.call(response, 'payload')) {
payloadValue = response.payload;
} else if (mode === 'value' && Object.prototype.hasOwnProperty.call(request, 'value')) {
payloadValue = request.value;
}
if (payloadValue !== undefined) {
let payloadText;
if (typeof payloadValue === 'object') {
try {
payloadText = JSON.stringify(payloadValue);
} catch (error) {
payloadText = String(payloadValue);
}
} else {
payloadText = String(payloadValue);
}
const payloadLabel = RED._("node-red:common.label.payload") || "payload";
notifyMessage = `${baseMessage} (${payloadLabel}: ${payloadText})`;
}
}
RED.notify(notifyMessage, "success");
},
error: function (xhr) {
let message;
if (xhr && xhr.responseJSON && xhr.responseJSON.error) {
message = xhr.responseJSON.error;
}
const key = mode === 'read' ? "manualReadError" : "manualWriteError";
RED.notify(message || (RED._("node-red-contrib-knx-ultimate/knxUltimate:knxUltimate." + key) || "Unable to send KNX command"), "error");
}
});
}
},
oneditprepare: function () {
// Go to the help panel
try {
RED.sidebar.show("help");
} catch (error) { }
try {
$("#node-input-topic").data('knxGaAutocomplete', true);
$("#node-input-knxFunctionHelperGAList").data('knxGaAutocomplete', true);
} catch (error) { }
var node = this;
if ($("#node-input-server").val() === "_ADD_") {
// Node-Red 4.0.x has a bug not selecting the default server node
try {
$("#node-input-server").prop("selectedIndex", 0);
} catch (error) {
}
}
var oNodeServer = RED.nodes.node($("#node-input-server").val()); // Store the config-node
const $buttonEnabled = $('#node-input-buttonEnabled');
const $buttonOptions = $('#knx-button-options');
const $buttonMode = $('#node-input-buttonMode');
const $buttonToggleInitial = $('#node-input-buttonToggleInitial');
const $buttonStaticValue = $('#node-input-buttonStaticValue');
const $buttonToggleRow = $('.knx-button-toggle-row');
const $buttonValueRow = $('.knx-button-value-row');
const coerceBoolean = (value) => (value === true || value === 'true');
$buttonEnabled.prop('checked', coerceBoolean(node.buttonEnabled));
$buttonMode.val(node.buttonMode || 'read');
$buttonToggleInitial.val(coerceBoolean(node.buttonToggleInitial) ? 'true' : 'false');
$buttonStaticValue.val(node.buttonStaticValue || '');
const isRawDptSelected = () => String($("#node-input-dpt").val() || '').trim().toLowerCase() === 'raw';
const refreshButtonOptions = () => {
const enabled = $buttonEnabled.prop('checked');
const rawMode = isRawDptSelected();
let mode = $buttonMode.val() || 'read';
if (rawMode && mode !== 'read') {
mode = 'read';
$buttonMode.val('read');
}
$buttonMode.find("option[value='toggle'], option[value='value']").prop('disabled', rawMode);
if (enabled) {
$buttonOptions.show();
} else {
$buttonOptions.hide();
}
$buttonToggleRow.toggle(enabled && !rawMode && mode === 'toggle');
$buttonValueRow.toggle(enabled && !rawMode && mode === 'value');
node.buttonEnabled = enabled;
node.buttonMode = mode;
node.buttonToggleInitial = $buttonToggleInitial.val();
node.buttonStaticValue = $buttonStaticValue.val();
};
const refreshRawModeUI = () => {
const rawMode = isRawDptSelected();
const isUniversal = $("#node-input-setTopicType").val() === 'listenAllGA';
if (isUniversal) {
refreshButtonOptions();
return;
}
if (rawMode) {
$("#divOutputRBE").hide();
$("#node-input-outputRBE").val("false");
$("#divInputRBE").hide();
$("#node-input-inputRBE").val("false");
$("#divPeriodicSend").hide();
$("#divPeriodicSendInterval").hide();
$("#node-input-periodicSend").prop('checked', false);
$("#divnotifyreadrequestautoreact").hide();
$("#node-input-notifyreadrequestalsorespondtobus").prop('checked', false);
$("#divFormatSectionHeader").hide();
$("#divFormatMultiply").hide();
$("#divFormatDecimals").hide();
$("#divFormatNegative").hide();
} else {
$("#divOutputRBE").show();
$("#divInputRBE").show();
$("#divPeriodicSend").show();
$("#divFormatSectionHeader").show();
$("#divFormatMultiply").show();
$("#divFormatDecimals").show();
$("#divFormatNegative").show();
if ($("#node-input-periodicSend").is(':checked')) {
$("#divPeriodicSendInterval").show();
} else {
$("#divPeriodicSendInterval").hide();
}
if ($("#node-input-notifyreadrequest").is(":checked")) {
$("#divnotifyreadrequestautoreact").show();
} else {
$("#divnotifyreadrequestautoreact").hide();
}
}
refreshButtonOptions();
};
$buttonEnabled.on('change', refreshButtonOptions);
$buttonMode.on('change', refreshButtonOptions);
$buttonToggleInitial.on('change', refreshButtonOptions);
$buttonStaticValue.on('change keyup', function () {
node.buttonStaticValue = $(this).val();
});
refreshButtonOptions();
// Secure GA cache from keyring
let secureGAs = new Set();
function refreshSecureGAs() {
try {
const sid = $("#node-input-server").val();
if (!sid || sid === '_ADD_') { secureGAs = new Set(); return; }
$.getJSON("knxUltimateKeyringDataSecureGAs?serverId=" + sid + "&_=" + new Date().getTime(), (data) => {
try {
const set = new Set();
if (Array.isArray(data)) data.forEach(ga => { if (typeof ga === 'string') set.add(ga); });
secureGAs = set;
} catch (e) { secureGAs = new Set(); }
}).fail(function () { secureGAs = new Set(); });
} catch (error) { secureGAs = new Set(); }
}
$("#tabs").tabs();
// 15/09/2020 Supergiovane, set the help sample based on Datapoint
const knxFunctionHelperItems = [
{
id: 'getGAValue',
label: 'getGAValue(address, dpt?, readIfMissing?)',
aceValue: "await getGAValue('1/1/1', '1.001', false)",
snippet: "await getGAValue('${1:1/1/1}', '${2:1.001}', ${3:false})",
doc: 'Read another group address. This helper is async, so use await to get the real value. By default, if the value is not cached, a KNX read is sent. Pass false as third parameter to use cache-only mode.'
},
{
id: 'setGAValue',
label: 'setGAValue(address, value, dpt?)',
aceValue: "setGAValue('1/1/1', true)",
snippet: "setGAValue('${1:1/1/1}', ${2:true}, '${3:1.001}')",
doc: 'Send a value to any KNX group address. The datapoint is optional when the ETS file is imported.'
},
{
id: 'self',
label: 'self(value)',
aceValue: 'self(false)',
snippet: 'self(${1:false})',
doc: 'Set this node value and forward it to the KNX bus.'
},
{
id: 'toggle',
label: 'toggle()',
aceValue: 'toggle()',
snippet: 'toggle()',
doc: 'Invert this node value and write it to the KNX bus.'
}
];
const globalScope = typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : {});
const rawDptOption = { value: 'raw', text: 'raw Raw telegram (no decode)' }
function appendRawDptOption($select) {
if (!$select || !$select.length) return
if ($select.find("option[value='raw']").length > 0) return
$select.prepend($("<option></option>")
.attr("value", rawDptOption.value)
.text(rawDptOption.text))
}
function knxUltimateDptsGetHelp(_dpt, _forceClose) {
const detailsContainer = $("#dptDetailsContainer")
if (_forceClose === true) {
detailsContainer.hide()
}
const serverId = $("#node-input-server").val()
if (serverId === "_ADD_" || serverId === '' || _dpt === null || _dpt === '') return
if (String(_dpt).trim().toLowerCase() === 'raw') {
const helplinkContainer = $("#sampleCodeEditor")
try {
if (node.sampleEditor) {
node.sampleEditor.destroy()
delete node.sampleEditor
}
} catch (error) { }
$("#example-editor").empty()
helplinkContainer.empty()
node.sampleEditor = RED.editor.createEditor({
id: 'example-editor',
mode: 'ace/mode/javascript',
value: `// KNX-Ultimate set as RAW NODE
// Incoming telegrams skip datapoint decoding.
// msg.payload will be null and the raw telegram bytes are available in msg.knx.rawValue.
// For outgoing telegrams, use msg.writeraw = Buffer.from("0730", "hex")
// and optionally msg.bitlength for 1-bit / 2-bit / 4-bit datapoints.
return msg;`
})
try {
node.sampleEditor.setReadOnly(true)
node.sampleEditor.setShowPrintMargin(false)
} catch (error) { }
$("<div>", {
class: "dpt-details-link"
})
.append($("<span>").text("RAW mode keeps the telegram undecoded and exposes the bytes in "))
.append($("<code>").text("msg.knx.rawValue"))
.append($("<span>").text("."))
.appendTo(helplinkContainer)
return
}
$.getJSON("knxUltimateDptsGetHelp?dpt=" + _dpt + "&serverId=" + serverId + "&" + { _: new Date().getTime() }, (data) => {
const helplinkContainer = $("#sampleCodeEditor")
try {
if (node.sampleEditor) {
node.sampleEditor.destroy()
delete node.sampleEditor
}
} catch (error) { }
$("#example-editor").empty()
helplinkContainer.empty()
const translate = (key, opts) => {
try {
if (typeof RED !== 'undefined' && RED._) {
return RED._(key, opts)
}
} catch (error) { }
return null
}
const noSampleText = translate('knxUltimate.dptDetails.noSample') || 'Currently, no sample payload is available.'
const createEditor = (value) => {
node.sampleEditor = RED.editor.createEditor({
id: 'example-editor',
mode: 'ace/mode/javascript',
value
})
try {
node.sampleEditor.setReadOnly(true)
node.sampleEditor.setShowPrintMargin(false)
} catch (error) { }
}
try {
const hasHelp = data.help !== 'NO'
if (hasHelp) {
createEditor(data.help)
if (data.helplink) {
const label = translate('Detail', { dpt: _dpt }) || ('More details for ' + _dpt)
helplinkContainer.html(` <i class="fa fa-question-circle"></i> <a target="_blank" href="${data.helplink}"><u>${label}</u></a>`)
}
} else {
createEditor(noSampleText)
if (data.helplink) {
const labelWiki = translate('Wiki') || 'Open wiki'
helplinkContainer.html(` <i class="fa fa-question-circle"></i> <a target="_blank" href="${data.helplink}"><u>${labelWiki}</u></a>`)
}
}
} catch (error) { }
if (detailsContainer.is(':visible') && node.sampleEditor) {
setTimeout(() => { try { node.sampleEditor.resize(true) } catch (error) { } }, 0)
}
})
}
const applyEditorOptions = (editor) => {
try {
if (!editor) return;
if (typeof editor.updateOptions === 'function') {
editor.updateOptions({ lineNumbers: 'off', minimap: { enabled: false }, scrollbar: { verticalScrollbarSize: 8, horizontalScrollbarSize: 8 } });
} else if (editor.renderer && typeof editor.renderer.setShowGutter === 'function') {
editor.renderer.setShowGutter(false);
}
if (typeof editor.setShowPrintMargin === 'function') editor.setShowPrintMargin(false);
if (typeof ace !== 'undefined' && ace.require) {
try { ace.require('ace/ext/language_tools'); } catch (error) { }
}
if (typeof editor.setOptions === 'function') {
editor.setOptions({
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
});
}
if (typeof editor.completers === 'undefined') {
editor.completers = [];
}
if (Array.isArray(editor.completers) && !editor._knxHelperCompleter) {
const aceCompletions = knxFunctionHelperItems.map(item => ({
caption: item.label,
value: item.aceValue,
snippet: item.snippet,
meta: 'KNX helper',
doc: item.doc
}));
const helperCompleter = {
getCompletions: function (_editor, _session, _pos, prefix, callback) {
const search = (prefix || '').toLowerCase();
const filtered = search
? aceCompletions.filter(entry => entry.caption.toLowerCase().startsWith(search) || entry.value.toLowerCase().startsWith(search))
: aceCompletions;
callback(null, filtered.length ? filtered : aceCompletions);
},
getDocTooltip: function (item) {
if (!item || item.docHTML || !item.doc) return;
item.docHTML = '<b>' + item.caption + '</b><hr />' + item.doc;
}
};
editor.completers.push(helperCompleter);
editor._knxHelperCompleter = helperCompleter;
}
if (typeof monaco !== 'undefined' && !globalScope.knxFunctionMonacoCompletionProvider) {
try {
const functionSuggestions = knxFunctionHelperItems.map(item => ({
label: item.label,
kind: monaco.languages.CompletionItemKind.Function,
insertText: item.snippet,
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: item.doc
}));
globalScope.knxFunctionMonacoCompletionProvider = monaco.languages.registerCompletionItemProvider('javascript', {
provideCompletionItems: () => ({ suggestions: functionSuggestions })
});
monaco.languages.typescript.javascriptDefaults.addExtraLib([
'/** Read a KNX group address. This helper is async, so use await to get the real value. By default, if the value is not cached, a KNX read is sent. Pass false as third parameter to use cache-only mode. */',
'declare function getGAValue(address: string, dptOrReadIfMissing?: string | boolean, readIfMissing?: boolean): Promise<any>;',
'/** Send a value to a KNX group address. */',
'declare function setGAValue(address: string, value: any, dpt?: string): void;',
'/** Toggle (invert) this node\'s current value on the KNX bus. */',
'declare function toggle(): void;',
].join('\n'), 'knx-ultimate-globals.d.ts');
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
noSemanticValidation: true,
noSyntaxValidation: false
});
} catch (error) { }
}
} catch (error) { }
};
const highlightEditor = (editor, active) => {
if (!editor) return;
try {
if (editor.renderer && editor.renderer.scroller) {
editor.renderer.scroller.style.backgroundColor = active ? '#e6ffe6' : '';
}
if (editor.renderer && editor.renderer.content) {
editor.renderer.content.style.backgroundColor = active ? '#e6ffe6' : '';
}
if (typeof editor.getDomNode === 'function') {
const dom = editor.getDomNode();
if (dom) dom.style.backgroundColor = active ? '#e6ffe6' : '';
} else if (editor.container) {
editor.container.style.backgroundColor = active ? '#e6ffe6' : '';
}
} catch (error) { }
};
const attachFocusHandlers = (editor) => {
if (!editor) return;
try {
if (typeof editor.on === 'function' && editor.renderer) {
editor.on('focus', () => { node.activeCodeEditor = editor; highlightEditor(editor, true); });
editor.on('blur', () => highlightEditor(editor, false));
} else if (typeof editor.onDidFocusEditorWidget === 'function') {
editor.onDidFocusEditorWidget(() => { node.activeCodeEditor = editor; highlightEditor(editor, true); });
if (typeof editor.onDidBlurEditorWidget === 'function') {
editor.onDidBlurEditorWidget(() => highlightEditor(editor, false));
}
}
} catch (error) { }
};
const sanitizeAutocompleteValue = (raw) => {
if (typeof raw !== 'string') return '';
return raw.replace(/['"]/g, '').trim();
};
const replaceLiteralAroundCursorAce = (editor, text) => {
try {
const AceRange = (typeof ace !== 'undefined' && ace.require) ? ace.require('ace/range').Range : null;
if (!AceRange) return false;
const pos = editor.getCursorPosition();
const line = editor.session.getLine(pos.row) || '';
let left = pos.column - 1;
while (left >= 0 && line[left] !== "'") left--;
if (left < 0) return false;
let right = pos.column;
while (right < line.length && line[right] !== "'") right++;
if (right >= line.length) return false;
const newLiteral = "'" + text + "'";
const range = new AceRange(pos.row, left, pos.row, right + 1);
editor.session.replace(range, newLiteral);
editor.moveCursorTo(pos.row, left + newLiteral.length);
return true;
} catch (error) { }
return false;
};
const replaceLiteralAroundCursorMonaco = (editor, text) => {
try {
const position = editor.getPosition();
if (!position) return false;
const model = typeof editor.getModel === 'function' ? editor.getModel() : null;
if (!model) return false;
const lineContent = model.getLineContent(position.lineNumber) || '';
let leftIdx = position.column - 2;
if (leftIdx >= lineContent.length) leftIdx = lineContent.length - 1;
while (leftIdx >= 0 && lineContent[leftIdx] !== "'") leftIdx--;
if (leftIdx < 0) return false;
let rightIdx = position.column - 1;
if (rightIdx < leftIdx) rightIdx = leftIdx + 1;
while (rightIdx < lineContent.length && lineContent[rightIdx] !== "'") rightIdx++;
if (rightIdx >= lineContent.length) return false;
const newLiteral = "'" + text + "'";
const range = new monaco.Range(
position.lineNumber,
leftIdx + 1,
position.lineNumber,
rightIdx + 2
);
editor.executeEdits('knxInsertGA', [{ range, text: newLiteral, forceMoveMarkers: true }]);
editor.setPosition({ lineNumber: position.lineNumber, column: leftIdx + 1 + newLiteral.length });
return true;
} catch (error) { }
return false;
};
const insertTextIntoEditor = (editor, text) => {
if (!editor || !text) return;
try {
if (editor.session && typeof editor.session.insert === 'function') {
editor.focus();
const selectionRange = editor.getSelection && typeof editor.getSelectionRange === 'function'
? editor.getSelectionRange()
: null;
if (selectionRange && !selectionRange.isEmpty()) {
editor.session.replace(selectionRange, text);
return;
}
const replaced = replaceLiteralAroundCursorAce(editor, text);
if (!replaced) {
const pos = editor.getCursorPosition();
editor.session.insert(pos, text);
}
return;
}
if (typeof editor.executeEdits === 'function' && typeof editor.getPosition === 'function' && typeof monaco !== 'undefined') {
const selection = typeof editor.getSelection === 'function' ? editor.getSelection() : null;
const hasSelection = selection && (selection.startLineNumber !== selection.endLineNumber || selection.startColumn !== selection.endColumn);
if (selection && hasSelection) {
const selectionRange = new monaco.Range(
selection.startLineNumber,
selection.startColumn,
selection.endLineNumber,
selection.endColumn
);
editor.executeEdits('knxInsertGA', [{ range: selectionRange, text, forceMoveMarkers: true }]);
const targetLine = selectionRange.startLineNumber;
const targetColumn = selectionRange.startColumn + text.length;
editor.setPosition({ lineNumber: targetLine, column: targetColumn });
editor.focus();
return;
}
const position = editor.getPosition();
if (!position) return;
let replaced = replaceLiteralAroundCursorMonaco(editor, text);
if (!replaced) {
const range = new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column);
const newColumn = position.column + text.length;
editor.executeEdits('knxInsertGA', [{ range, text, forceMoveMarkers: true }]);
editor.setPosition({ lineNumber: position.lineNumber, column: newColumn });
}
editor.focus();
}
} catch (error) { }
};
node.sendMsgToKNXCodeEditor = RED.editor.createEditor({
id: 'sendMsgToKNXCode-editor',
mode: 'ace/mode/nrjavascript',
value: node.sendMsgToKNXCode
});
applyEditorOptions(node.sendMsgToKNXCodeEditor);
node.receiveMsgFromKNXCodeEditor = RED.editor.createEditor({
id: 'receiveMsgFromKNXCode-editor',
mode: 'ace/mode/nrjavascript',
value: node.receiveMsgFromKNXCode
});
applyEditorOptions(node.receiveMsgFromKNXCodeEditor);
if (typeof monaco !== 'undefined') {
try {
if (!globalScope._knxEditorThemesDefined) {
monaco.editor.defineTheme('knx-send-theme', {
base: 'vs', inherit: true, rules: [],
colors: { 'editor.background': '#e8f5e9', 'editorGutter.background': '#e8f5e9' }
});
monaco.editor.defineTheme('knx-receive-theme', {
base: 'vs', inherit: true, rules: [],
colors: { 'editor.background': '#fffde7', 'editorGutter.background': '#fffde7' }
});
globalScope._knxEditorThemesDefined = true;
}
if (typeof node.sendMsgToKNXCodeEditor.updateOptions === 'function') {
node.sendMsgToKNXCodeEditor.updateOptions({ theme: 'knx-send-theme' });
}
if (typeof node.receiveMsgFromKNXCodeEditor.updateOptions === 'function') {
node.receiveMsgFromKNXCodeEditor.updateOptions({ theme: 'knx-receive-theme' });
}
} catch (e) { }
}
node.activeCodeEditor = null;
attachFocusHandlers(node.sendMsgToKNXCodeEditor);
attachFocusHandlers(node.receiveMsgFromKNXCodeEditor);
$("#btn-insert-knxFunctionGA").off('click').on('click', function () {
const rawValue = $("#node-input-knxFunctionHelperGAList").val();
if (!rawValue || rawValue.trim() === '') {
$("#node-input-knxFunctionHelperGAList").focus();
return;
}
const sanitizedValue = sanitizeAutocompleteValue(rawValue);
const editor = node.activeCodeEditor || node.sendMsgToKNXCodeEditor || node.receiveMsgFromKNXCodeEditor;
if (!editor) return;
if (!sanitizedValue) return;
insertTextIntoEditor(editor, sanitizedValue);
});
const $knxFunctionHelperInput = $("#node-input-knxFunctionHelperGAList");
const $knxFunctionHelperInsertButton = $("#btn-insert-knxFunctionGA");
let knxFunctionHelperDefaultPlaceholder = $knxFunctionHelperInput.attr('placeholder') || '';
if (!knxFunctionHelperDefaultPlaceholder) {
try {
knxFunctionHelperDefaultPlaceholder = RED._("node-red-contrib-knx-ultimate/knxUltimate:knxUltimate.placeholder.search") || '';
} catch (error) { }
}
const knxFunctionHelperNoEtsPlaceholder = 'To enable the search, IMPORT THE ETS FILE';
const refreshKnxFunctionHelperState = (hasEtsCsv) => {
$("#divknxFunctionHelperGAList").show();
$knxFunctionHelperInput.prop('disabled', !hasEtsCsv);
$knxFunctionHelperInsertButton.prop('disabled', !hasEtsCsv);
if (hasEtsCsv) {
$knxFunctionHelperInput.attr('placeholder', knxFunctionHelperDefaultPlaceholder);
} else {
$knxFunctionHelperInput.val('');
$knxFunctionHelperInput.attr('placeholder', knxFunctionHelperNoEtsPlaceholder);
}
};
const configureSnippetPicker = (snippets, inputSelector, datalistSelector, applySnippet) => {
const inputEl = $(inputSelector)
const datalistEl = $(datalistSelector)
if (!inputEl.length || !datalistEl.length) return
const items = Array.isArray(snippets) ? snippets.slice() : []
const translate = key => {
try {
if (typeof RED !== 'undefined' && RED._) {
return RED._(key)
}
} catch (error) { }
return null
}
const populate = () => {
datalistEl.empty()
if (!items.length) {
inputEl.val('')
inputEl.prop('disabled', true)
const emptyPlaceholder = inputEl.data('empty') || translate('knxUltimate.snippets.emptyPlaceholder') || ''
inputEl.attr('placeholder', emptyPlaceholder)
return
}
inputEl.prop('disabled', false)
const defaultPlaceholder = inputEl.data('placeholder') || translate('knxUltimate.snippets.searchPlaceholder') || ''
inputEl.attr('placeholder', defaultPlaceholder)
items.forEach(snippet => {
const opt = document.createElement('option')
opt.value = snippet.title || snippet.id || 'Snippet'
datalistEl.append(opt)
})
}
populate()
const applySelectedSnippet = value => {
if (!value) return
const snippet = items.find(sn => (sn.title || sn.id) === value)
if (!snippet) return
applySnippet(snippet)
inputEl.val('')
}
inputEl.on('change', function () {
applySelectedSnippet($(this).val())
})
inputEl.on('keydown', function (evt) {
if (evt.key === 'Enter') {
evt.preventDefault()
applySelectedSnippet($(this).val())
}
})
}
configureSnippetPicker(window.KNXSendSnippets || [], '#sendSnippetPicker', '#sendSnippetOptions', snippet => {
node.sendMsgToKNXCodeEditor.session.setValue(snippet.code || '')
})
configureSnippetPicker(window.KNXReceiveSnippets || [], '#receiveSnippetPicker', '#receiveSnippetOptions', snippet => {
node.receiveMsgFromKNXCodeEditor.session.setValue(snippet.code || '')
})
const dptDetailsContainer = $('#dptDetailsContainer')
$('#toggleDptDetails').on('click', function (evt) {
evt.preventDefault()
if (!dptDetailsContainer.length) return
dptDetailsContainer.stop(true, true).slideToggle(200, function () {
if (dptDetailsContainer.is(':visible') && node.sampleEditor) {
try { node.sampleEditor.resize(true) } catch (error) { }
}
})
})
function checkUI() {
// Backward compatibility
if (node.outputRBE === true || $("#node-input-outputRBE").val() === true) {
node.outputRBE = 'true';
$("#node-input-outputRBE").val("true")
}
if (node.outputRBE === undefined || node.outputRBE === false || $("#node-input-outputRBE").val() === false) {
node.outputRBE = 'false';
$("#node-input-outputRBE").val("false")
}
if (node.inputRBE === true || $("#node-input-inputRBE").val() === true) {
node.inputRBE = 'true';
$("#node-input-inputRBE").val("true")
}
if (node.inputRBE === undefined || node.inputRBE === false || $("#node-input-inputRBE").val() === false) {
node.inputRBE = 'false';
$("#node-input-inputRBE").val("false")
}
if (node.passthrough === undefined) {
node.passthrough = 'no';
$("#node-input-passthrough").val("no")
}
if (node.initialread === undefined || node.initialread === false) {
node.initialread = 0;
$("#node-input-initialread").val(0)
}
oNodeServer = RED.nodes.node($("#node-input-server").val());
if (oNodeServer === undefined) {
// Show the DEPLOY FIRST message
$("#divDeployFirst").show();
$("#divMain").hide();
} else {
$("#divDeployFirst").hide();
$("#divMain").show();
try {
if (typeof oNodeServer.csv !== "undefined" && oNodeServer.csv !== "") {
$("#isETSFileLoaded").val("si");
} else {
$("#isETSFileLoaded").val("no");
}
} catch (error) {
$("#isETSFileLoaded").val("no");
}
refreshKnxFunctionHelperState($("#isETSFileLoaded").val() === "si");
if (oNodeServer.knxSecureSelected) {
$("#divknxsecure").show();
} else {
$("#divknxsecure").hide();
}
if ($("#node-input-server").val() !== "_ADD_" && $("#node-input-server").val() !== '') {
refreshSecureGAs();
$.getJSON("knxUltimateDpts?serverId=" + $("#node-input-server").val() + "&_=" + new Date().getTime(), (data) => {
$("#node-input-dpt").empty();
appendRawDptOption($("#node-input-dpt"));
data.forEach(dpt => {
$("#node-input-dpt").append($("<option></option>")
.attr("value", dpt.value)
.text(dpt.text))
});
if (node.dpt === undefined || node.dpt === '') node.dpt = '1.001'
$("#node-input-dpt").val(node.dpt);
// Load help sample
knxUltimateDptsGetHelp(node.dpt, true);
refreshRawModeUI();
})
}
// Add write and response as default for existing nodes like was default before
if (node.notifywrite === undefined) {
node.notifywrite = true
node.notifyresponse = true
$("#node-input-notifywrite").prop("checked", true)
$("#node-input-notifyresponse").prop("checked", true)
}
// Add Write as default for existing clients output
if (node.outputtype === undefined) {
node.outputtype = "write"
$("#node-input-outputtype").val("write")
}
$("#node-input-notifyreadrequest").on('change', function () {
if (isRawDptSelected()) {
$("#divnotifyreadrequestautoreact").hide();
} else if ($("#node-input-notifyreadrequest").is(":checked")) {
if ($("#node-input-setTopicType").val() === "listenAllGA") {
} else {
$("#divnotifyreadrequestautoreact").show();
}
} else {
$("#divnotifyreadrequestautoreact").hide();
}
})
$("#node-input-periodicSend").on('change', function () {
if ($("#node-input-periodicSend").is(":checked")) {
$("#divPeriodicSendInterval").show()
} else {
$("#divPeriodicSendInterval").hide()
}
})
// Set the group address type
if (node.setTopicType === undefined) {
node.setTopicType = 'str';
$("#node-input-setTopicType").val('str');
}
// KNX Function helper: search for a GA and devicename
// ----------------------------------------------------------
try {
$("#node-input-knxFunctionHelperGAList").autocomplete('destroy');
$("#node-input-knxFunctionHelperGAList").removeClass(); // Rimuove eventuali classi aggiunte dall'autocompletamento
} catch (error) { }
$("#node-input-knxFunctionHelperGAList").off(); // Rimuovi tutti gli eventi associati
$("#node-input-knxFunctionHelperGAList").val(''); // Pulisce il valore del campo di input, se necessario
$("#node-input-knxFunctionHelperGAList").autocomplete({
minLength: 0,
source: function (request, response) {
$.getJSON("knxUltimatecsv?nodeID=" + oNodeServer.id + "&" + { _: new Date().getTime() }, (data) => {
response($.map(data, function (value, key) {
var sSearch = (value.ga + " (" + value.devicename + ") DPT" + value.dpt);
if (htmlUtilsfullCSVSearch(sSearch, request.term)) {
return {
label: value.ga + " # " + value.devicename + " # " + value.dpt, // Label for Display
value: value.ga + " " + value.devicename // Value
}
} else {
return null;
}
}));
});
}, select: function (event, ui) {
}
}).focus(function () {
$(this).autocomplete('search', $(this).val() + 'exactmatch');
});
// ----------------------------------------------------------
$("#node-input-setTopicType").on('change', function () {
try {
$("#divDatapointSelection").show();
$("#node-input-topic").show();
$("#node-input-topic").autocomplete('destroy');
$("#node-input-topic").off(); // Rimuovi tutti gli eventi associati
$("#node-input-topic").val(''); // Pulisce il valore del campo di input, se necessario
$("#node-input-topic").prop('disabled', false); // Assicura che il campo non sia disabilitato
$("#node-input-topic").removeClass(); // Rimuove eventuali classi aggiunte dall'autocompletamento
} catch (error) {
}
if ($("#node-input-setTopicType").val() === 'str') {
$("#node-input-topic").prop('placeholder', 'Select your GA');
// Autocomplete suggestion with ETS csv File
$("#node-input-topic").autocomplete({
minLength: 0,
source: function (request, response) {
$.getJSON("knxUltimatecsv?nodeID=" + oNodeServer.id + "&" + { _: new Date().getTime() }, (data) => {
response($.map(data, function (value, key) {
var sSearch = (value.ga + " (" + value.devicename + ") DPT" + value.dpt);