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.
378 lines (337 loc) • 22.2 kB
HTML
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
<script type="text/javascript">
RED.nodes.registerType('knxUltimateAI', {
category: "KNX Ultimate",
color: '#AD9EFF',
defaults: {
server: { type: "knxUltimate-config", required: true },
name: { value: "KNX AI", required: false },
topic: { value: "", required: false },
notifyreadrequest: { value: true },
notifyresponse: { value: true },
notifywrite: { value: true },
analysisWindowSec: { value: 60, required: true, validate: RED.validators.number() },
historyWindowSec: { value: 300, required: true, validate: RED.validators.number() },
emitIntervalSec: { value: 0, required: true, validate: RED.validators.number() },
topN: { value: 10, required: true, validate: RED.validators.number() },
maxEvents: { value: 5000, required: true, validate: RED.validators.number() },
rateWindowSec: { value: 10, required: true, validate: RED.validators.number() },
maxTelegramPerSecOverall: { value: 0, required: true, validate: RED.validators.number() },
maxTelegramPerSecPerGA: { value: 0, required: true, validate: RED.validators.number() },
flapWindowSec: { value: 30, required: true, validate: RED.validators.number() },
flapMaxChanges: { value: 0, required: true, validate: RED.validators.number() },
enablePattern: { value: true },
patternMaxLagMs: { value: 1500, required: true, validate: RED.validators.number() },
patternMinCount: { value: 8, required: true, validate: RED.validators.number() },
llmEnabled: { value: false },
llmProvider: { value: "openai_compat" },
llmBaseUrl: { value: "https://api.openai.com/v1/chat/completions" },
llmModel: { value: "gpt-4o-mini" },
llmSystemPrompt: { value: "" },
llmTemperature: { value: 0.2, required: false, validate: RED.validators.number() },
llmMaxTokens: { value: 600, required: false, validate: RED.validators.number() },
llmTimeoutMs: { value: 30000, required: false, validate: RED.validators.number() },
llmMaxEventsInPrompt: { value: 120, required: false, validate: RED.validators.number() },
llmIncludeRaw: { value: false },
llmIncludeFlowContext: { value: true },
llmMaxFlowNodesInPrompt: { value: 80, required: false, validate: RED.validators.number() },
llmIncludeDocsSnippets: { value: true },
llmDocsLanguage: { value: "it" },
llmDocsMaxSnippets: { value: 5, required: false, validate: RED.validators.number() },
llmDocsMaxChars: { value: 3000, required: false, validate: RED.validators.number() }
},
credentials: {
llmApiKey: { type: "password" }
},
inputs: 1,
outputs: 3,
outputLabels: function (i) {
switch (i) {
case 0: return RED._('knxUltimateAI.outputs.summary');
case 1: return RED._('knxUltimateAI.outputs.anomalies');
case 2: return RED._('knxUltimateAI.outputs.assistant');
}
},
icon: "node-eye-icon.svg",
label: function () {
return (this.name || "KNX AI");
},
paletteLabel: "KNX AI",
oneditprepare: function () {
try { RED.sidebar.show("help"); } catch (error) { }
$("#knx-ai-accordion").accordion({
header: "h3",
heightStyle: "content",
collapsible: true,
active: 0
});
const toggleLLM = () => {
const enabled = $("#node-input-llmEnabled").is(":checked");
if (enabled) {
$("#knx-ai-llm-settings").show();
} else {
$("#knx-ai-llm-settings").hide();
}
};
$("#node-input-llmEnabled").on("change", toggleLLM);
toggleLLM();
const toggleProvider = () => {
const provider = $("#node-input-llmProvider").val();
if (provider === "ollama") {
$("#knx-ai-apikey-row").hide();
$("#knx-ai-ollama-warning").show();
} else {
$("#knx-ai-apikey-row").show();
$("#knx-ai-ollama-warning").hide();
}
};
$("#node-input-llmProvider").on("change", toggleProvider);
toggleProvider();
const nodeId = this.id;
const setModelsStatus = (text, level) => {
const $status = $("#knx-ai-models-status");
if (!$status || $status.length === 0) return;
$status.text(text || "");
$status.removeClass("knx-ai-models-ok knx-ai-models-warn knx-ai-models-err");
if (level === "ok") $status.addClass("knx-ai-models-ok");
if (level === "warn") $status.addClass("knx-ai-models-warn");
if (level === "err") $status.addClass("knx-ai-models-err");
};
const populateModels = (models) => {
const $dl = $("#knx-ai-llmModels");
if (!$dl || $dl.length === 0) return;
$dl.empty();
(models || []).forEach(function (m) {
$("<option>").attr("value", m).appendTo($dl);
});
};
const refreshModels = () => {
const provider = $("#node-input-llmProvider").val();
const baseUrl = $("#node-input-llmBaseUrl").val() || "";
const apiKey = $("#node-input-llmApiKey").val() || "";
const payload = { nodeId: nodeId, provider: provider, baseUrl: baseUrl };
// Node-RED uses "__PWRD__" as placeholder for stored credentials: don't send it.
if (apiKey && apiKey !== "__PWRD__") payload.apiKey = apiKey;
setModelsStatus(RED._('knxUltimateAI.messages.loadingModels') || "Loading models...", "warn");
$("#knx-ai-refreshModels").prop("disabled", true);
$.ajax({
url: "knxUltimateAI/models",
type: "POST",
contentType: "application/json",
data: JSON.stringify(payload)
})
.done(function (data) {
const models = (data && data.models) ? data.models : [];
populateModels(models);
const msg = (RED._('knxUltimateAI.messages.loadedModels') || "Models loaded") + ": " + models.length;
setModelsStatus(msg, "ok");
try { RED.notify(msg, "success"); } catch (e) { }
})
.fail(function (xhr) {
let err = "Failed to load models";
try {
const resp = xhr && xhr.responseJSON;
if (resp && resp.error) err = resp.error;
} catch (e) { }
setModelsStatus(err, "err");
try { RED.notify(err, "error"); } catch (e) { }
})
.always(function () {
$("#knx-ai-refreshModels").prop("disabled", false);
});
};
$("#knx-ai-refreshModels").on("click", function (evt) {
evt.preventDefault();
refreshModels();
});
},
oneditsave: function () {
try { RED.sidebar.show("info"); } catch (error) { }
},
oneditcancel: function () {
try { RED.sidebar.show("info"); } catch (error) { }
}
})
</script>
<script type="text/html" data-template-name="knxUltimateAI">
<div class="form-row">
<b><span data-i18n="knxUltimateAI.title"></span></b>
<br/><br/>
<label for="node-input-server"><i class="fa fa-tag"></i> <span data-i18n="knxUltimateAI.properties.server"></span></label>
<input type="text" id="node-input-server">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="knxUltimateAI.properties.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]knxUltimateAI.properties.name" style="flex:1 1 240px; min-width:240px; max-width:240px;">
</div>
<div class="form-row">
<label for="node-input-topic"><i class="fa fa-tasks"></i> <span data-i18n="knxUltimateAI.properties.topic"></span></label>
<input type="text" id="node-input-topic" data-i18n="[placeholder]knxUltimateAI.properties.topic">
</div>
<div id="knx-ai-accordion">
<h3><span data-i18n="knxUltimateAI.sections.capture"></span></h3>
<div>
<div class="form-row">
<input type="checkbox" id="node-input-notifywrite" style="display:inline-block; width:auto; vertical-align:top;">
<label style="width:auto" for="node-input-notifywrite"> <span data-i18n="knxUltimateAI.properties.notifywrite"></span></label>
</div>
<div class="form-row">
<input type="checkbox" id="node-input-notifyresponse" style="display:inline-block; width:auto; vertical-align:top;">
<label style="width:auto" for="node-input-notifyresponse"> <span data-i18n="knxUltimateAI.properties.notifyresponse"></span></label>
</div>
<div class="form-row">
<input type="checkbox" id="node-input-notifyreadrequest" style="display:inline-block; width:auto; vertical-align:top;">
<label style="width:auto" for="node-input-notifyreadrequest"> <span data-i18n="knxUltimateAI.properties.notifyreadrequest"></span></label>
</div>
</div>
<h3><span data-i18n="knxUltimateAI.sections.analysis"></span></h3>
<div>
<div class="form-row">
<label style="width:290px" for="node-input-analysisWindowSec"><i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateAI.properties.analysisWindowSec"></span></label>
<input style="width:90px" type="number" id="node-input-analysisWindowSec">
</div>
<div class="form-row">
<label style="width:290px" for="node-input-historyWindowSec"><i class="fa fa-history"></i> <span data-i18n="knxUltimateAI.properties.historyWindowSec"></span></label>
<input style="width:90px" type="number" id="node-input-historyWindowSec">
</div>
<div class="form-row">
<label style="width:290px" for="node-input-maxEvents"><i class="fa fa-bars"></i> <span data-i18n="knxUltimateAI.properties.maxEvents"></span></label>
<input style="width:90px" type="number" id="node-input-maxEvents">
</div>
<div class="form-row">
<label style="width:290px" for="node-input-emitIntervalSec"><i class="fa fa-send"></i> <span data-i18n="knxUltimateAI.properties.emitIntervalSec"></span></label>
<input style="width:90px" type="number" id="node-input-emitIntervalSec">
</div>
<div class="form-row">
<label style="width:290px" for="node-input-topN"><i class="fa fa-list-ol"></i> <span data-i18n="knxUltimateAI.properties.topN"></span></label>
<input style="width:90px" type="number" id="node-input-topN">
</div>
<div class="form-row">
<input type="checkbox" id="node-input-enablePattern" style="display:inline-block; width:auto; vertical-align:top;">
<label style="width:auto" for="node-input-enablePattern"> <span data-i18n="knxUltimateAI.properties.enablePattern"></span></label>
</div>
<div class="form-row">
<label style="width:290px" for="node-input-patternMaxLagMs"><i class="fa fa-arrows-h"></i> <span data-i18n="knxUltimateAI.properties.patternMaxLagMs"></span></label>
<input style="width:120px" type="number" id="node-input-patternMaxLagMs">
</div>
<div class="form-row">
<label style="width:290px" for="node-input-patternMinCount"><i class="fa fa-filter"></i> <span data-i18n="knxUltimateAI.properties.patternMinCount"></span></label>
<input style="width:90px" type="number" id="node-input-patternMinCount">
</div>
</div>
<h3><span data-i18n="knxUltimateAI.sections.anomalies"></span></h3>
<div>
<div class="form-row">
<label style="width:290px" for="node-input-rateWindowSec"><i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateAI.properties.rateWindowSec"></span></label>
<input style="width:90px" type="number" id="node-input-rateWindowSec">
</div>
<div class="form-row">
<label style="width:290px" for="node-input-maxTelegramPerSecOverall"><i class="fa fa-tachometer"></i> <span data-i18n="knxUltimateAI.properties.maxTelegramPerSecOverall"></span></label>
<input style="width:90px" type="number" id="node-input-maxTelegramPerSecOverall">
</div>
<div class="form-row">
<label style="width:290px" for="node-input-maxTelegramPerSecPerGA"><i class="fa fa-bolt"></i> <span data-i18n="knxUltimateAI.properties.maxTelegramPerSecPerGA"></span></label>
<input style="width:90px" type="number" id="node-input-maxTelegramPerSecPerGA">
</div>
<div class="form-row">
<label style="width:290px" for="node-input-flapWindowSec"><i class="fa fa-exchange"></i> <span data-i18n="knxUltimateAI.properties.flapWindowSec"></span></label>
<input style="width:90px" type="number" id="node-input-flapWindowSec">
</div>
<div class="form-row">
<label style="width:290px" for="node-input-flapMaxChanges"><i class="fa fa-random"></i> <span data-i18n="knxUltimateAI.properties.flapMaxChanges"></span></label>
<input style="width:90px" type="number" id="node-input-flapMaxChanges">
</div>
</div>
<h3><span data-i18n="knxUltimateAI.sections.llm"></span></h3>
<div>
<div class="form-row">
<input type="checkbox" id="node-input-llmEnabled" style="display:inline-block; width:auto; vertical-align:top;">
<label style="width:auto" for="node-input-llmEnabled"> <span data-i18n="knxUltimateAI.properties.llmEnabled"></span></label>
</div>
<div id="knx-ai-llm-settings">
<div class="form-row">
<label style="width:290px" for="node-input-llmProvider"><i class="fa fa-cog"></i> <span data-i18n="knxUltimateAI.properties.llmProvider"></span></label>
<select style="width:100%" id="node-input-llmProvider">
<option value="openai_compat" data-i18n="knxUltimateAI.selectlists.llmProvider.openai_compat"></option>
<option value="ollama" data-i18n="knxUltimateAI.selectlists.llmProvider.ollama"></option>
</select>
</div>
<div class="form-tips" id="knx-ai-ollama-warning" style="display:none;">
<span data-i18n="knxUltimateAI.messages.ollamaNotSupported"></span>
</div>
<div class="form-row">
<label style="width:290px" for="node-input-llmBaseUrl"><i class="fa fa-link"></i> <span data-i18n="knxUltimateAI.properties.llmBaseUrl"></span></label>
<input type="text" id="node-input-llmBaseUrl" data-i18n="[placeholder]knxUltimateAI.placeholder.llmBaseUrl">
</div>
<div class="form-row" id="knx-ai-apikey-row">
<label style="width:290px" for="node-input-llmApiKey"><i class="fa fa-key"></i> <span data-i18n="knxUltimateAI.properties.llmApiKey"></span></label>
<input type="password" id="node-input-llmApiKey" data-i18n="[placeholder]knxUltimateAI.placeholder.llmApiKey">
</div>
<div class="form-row">
<label style="width:290px" for="node-input-llmModel"><i class="fa fa-cube"></i> <span data-i18n="knxUltimateAI.properties.llmModel"></span></label>
<input style="width:calc(100% - 110px)" type="text" id="node-input-llmModel" list="knx-ai-llmModels" data-i18n="[placeholder]knxUltimateAI.placeholder.llmModel">
<button type="button" class="red-ui-button" id="knx-ai-refreshModels" style="margin-left:6px;" title="Refresh models">
<i class="fa fa-refresh"></i> <span data-i18n="knxUltimateAI.buttons.refreshModels"></span>
</button>
</div>
<datalist id="knx-ai-llmModels"></datalist>
<div class="form-tips" id="knx-ai-models-status"></div>
<div class="form-row">
<label style="width:290px" for="node-input-llmSystemPrompt"><i class="fa fa-commenting-o"></i> <span data-i18n="knxUltimateAI.properties.llmSystemPrompt"></span></label>
<textarea style="width:100%; height:110px;" id="node-input-llmSystemPrompt" data-i18n="[placeholder]knxUltimateAI.placeholder.llmSystemPrompt"></textarea>
</div>
<div class="form-row">
<label style="width:290px" for="node-input-llmTemperature"><i class="fa fa-sliders"></i> <span data-i18n="knxUltimateAI.properties.llmTemperature"></span></label>
<input style="width:90px" type="number" id="node-input-llmTemperature">
</div>
<div class="form-row">
<label style="width:290px" for="node-input-llmMaxTokens"><i class="fa fa-align-left"></i> <span data-i18n="knxUltimateAI.properties.llmMaxTokens"></span></label>
<input style="width:90px" type="number" id="node-input-llmMaxTokens">
</div>
<div class="form-row">
<label style="width:290px" for="node-input-llmTimeoutMs"><i class="fa fa-hourglass-half"></i> <span data-i18n="knxUltimateAI.properties.llmTimeoutMs"></span></label>
<input style="width:120px" type="number" id="node-input-llmTimeoutMs">
</div>
<div class="form-row">
<label style="width:290px" for="node-input-llmMaxEventsInPrompt"><i class="fa fa-list"></i> <span data-i18n="knxUltimateAI.properties.llmMaxEventsInPrompt"></span></label>
<input style="width:90px" type="number" id="node-input-llmMaxEventsInPrompt">
</div>
<div class="form-row">
<input type="checkbox" id="node-input-llmIncludeRaw" style="display:inline-block; width:auto; vertical-align:top;">
<label style="width:auto" for="node-input-llmIncludeRaw"> <span data-i18n="knxUltimateAI.properties.llmIncludeRaw"></span></label>
</div>
<div class="form-row">
<input type="checkbox" id="node-input-llmIncludeFlowContext" style="display:inline-block; width:auto; vertical-align:top;">
<label style="width:auto" for="node-input-llmIncludeFlowContext"> <span data-i18n="knxUltimateAI.properties.llmIncludeFlowContext"></span></label>
</div>
<div class="form-row">
<label style="width:290px" for="node-input-llmMaxFlowNodesInPrompt"><i class="fa fa-sitemap"></i> <span data-i18n="knxUltimateAI.properties.llmMaxFlowNodesInPrompt"></span></label>
<input style="width:90px" type="number" id="node-input-llmMaxFlowNodesInPrompt">
</div>
<div class="form-row">
<input type="checkbox" id="node-input-llmIncludeDocsSnippets" style="display:inline-block; width:auto; vertical-align:top;">
<label style="width:auto" for="node-input-llmIncludeDocsSnippets"> <span data-i18n="knxUltimateAI.properties.llmIncludeDocsSnippets"></span></label>
</div>
<div class="form-row">
<label style="width:290px" for="node-input-llmDocsLanguage"><i class="fa fa-language"></i> <span data-i18n="knxUltimateAI.properties.llmDocsLanguage"></span></label>
<select style="width:100%" id="node-input-llmDocsLanguage">
<option value="it">Italiano</option>
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Français</option>
<option value="es">Español</option>
<option value="zh-CN">简体中文</option>
</select>
</div>
<div class="form-row">
<label style="width:290px" for="node-input-llmDocsMaxSnippets"><i class="fa fa-files-o"></i> <span data-i18n="knxUltimateAI.properties.llmDocsMaxSnippets"></span></label>
<input style="width:90px" type="number" id="node-input-llmDocsMaxSnippets">
</div>
<div class="form-row">
<label style="width:290px" for="node-input-llmDocsMaxChars"><i class="fa fa-text-width"></i> <span data-i18n="knxUltimateAI.properties.llmDocsMaxChars"></span></label>
<input style="width:110px" type="number" id="node-input-llmDocsMaxChars">
</div>
</div>
</div>
</div>
</script>