node-red-contrib-knx-ultimate 6.3.30 → 6.3.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,18 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
+ **Version 6.3.32** - August 2026<br/>
10
+
11
+ - **KNX AI**: AI-first chat with complete cloud ETS context, selectable local windows and guided retrieval; selected ETS access is now the sole read/write authority, with no neutral role.<br/>
12
+
13
+ **Version 6.3.31** - August 2026<br/>
14
+
15
+ - **KNX AI — adaptive prompts for 8K local models**: the conversational prompt now reserves response space from the active model window before every request. For an 8K model it uses a compact tool contract and automatically bounds the newest conversation turns, KNX/adapter events, home memory, Web results, schedules, Function source, retrieved ETS objects and camera metadata. The complete selected ETS catalog remains local and available through deterministic retrieval; it is not copied wholesale into the prompt. The prompt-debug file records the final bounded system/user text actually sent to the model.<br/>
16
+ - **KNX AI — model-guided local ETS retrieval**: the conversational model now receives `catalogActions` for deterministic searches over the complete selected ETS catalog held inside the node instead of receiving the full catalog in every prompt. Bounded multi-pass retrieval supports ranked search across exact GAs, ETS names, aliases, hierarchy, areas, semantics, DPTs and value labels; accent-insensitive and typo-tolerant matches; exact-address lookup; area discovery/browsing; and related command/status discovery. No socket or second classifier is used: LM Studio returns structured requests, KNX AI executes them locally and calls the same model again with a deduplicated subset of at most 24 objects. Commands and GA-role learning are accepted only for retrieved objects and still pass the complete local ETS/DPT/access validation. The final prompt debug file exposes the retrieval summaries and exact retrieved subset.<br/>
17
+ - **KNX AI — complete ETS with deduplicated operational context**: removed the 4K/8K/16K/unlimited context-size selector and its persisted runtime property while keeping the complete selected ETS catalog locally searchable from every conversation. The implicit KNX and adapter archive range is now 20 minutes instead of 24 hours; an explicit user time range still takes precedence. Packaged help, README, wiki, examples and changelog text are no longer embedded in prompts and can instead be retrieved from public GitHub through the request-driven Web tool. Retrieved ETS metadata, archive rows and the current request each have one canonical prompt location; duplicated archive summaries, ETS-derived area lists, per-GA live snapshots and the always-on Node-RED project inventory were removed. Complete Function source is supplied only for explicit Function-code review requests. A node-specific, locally protected `knxai-last-chat-prompt-<node-id>.txt` debug file is overwritten before every conversational model call with the exact system/user prompt text and its estimated size, excluding API keys and HTTP headers; its path appears in the editor context card. Ollama receives the model's full declared window as `num_ctx`; KNX AI never retries an oversized request by compacting its input.<br/>
18
+ - **KNX AI — explicit ETS catalog access and read-only policy**: added the same filterable group-address selector used by IoT Bridge’s MQTT profile, with Select all/none and bulk or per-row Read only controls. Every selected ETS object is available to conversational retrieval, while Flow Builder keeps its complete selected design inventory. Excluded objects are absent from AI tools, while read-only objects remain searchable and readable but are rejected locally for every write even if their inferred or learned role is command. Complete ETS names, hierarchy paths and duplicate-address aliases are preserved in the local catalog and retrieved object records. There is intentionally no backward-compatibility migration: an existing node without a newly saved explicit ETS selection exposes no group addresses until it is opened, configured, saved and deployed.<br/>
19
+ - **KNX AI — request-driven Web access with clarification-first tool use**: removed the fixed proactive Web polling option, interval selector, background timer, synthetic recipient routing and related Setup Doctor state. During a normal conversation the model now decides semantically whether the current request needs fresh public information; when a missing goal, scope, place, time window or desired outcome would materially change the answer or query, it must ask one concise clarification and emit no tool action until the user answers. Explicit one-time or recurring Web checks remain available through the user-authorized natural-language scheduler and share the existing hourly budget, bounded research loop, verified citations and security controls. Editor strings, help, wiki documentation and tests were aligned in EN, IT, DE, FR, ES and zh-CN.<br/>
20
+
9
21
  **Version 6.3.30** - August 2026<br/>
10
22
 
11
23
  - **KNX AI — two-way RedBot Telegram voice chat**: the `RedBot / node-red-contrib-chatbot (Telegram)` preset now accepts RedBot's native inbound `audio` payload and its already-downloaded OGG/Opus `Buffer`, applies the same 20 MB and five-minute safeguards without downloading the file a second time, and transcribes it through the selected OpenAI-compatible provider. Successful requests receive a native RedBot `audio` reply with the localized AI-generated-voice disclosure and text caption; KNX confirmations deliberately remain RedBot `inline-buttons` text messages because its Telegram voice sender cannot attach that keyboard to `sendVoice`. Existing saved RedBot input/output mappings are upgraded at runtime. The separate `windkh/node-red-contrib-telegrambot` voice contract and its tests remain unchanged.<br/>
@@ -480,12 +480,13 @@
480
480
  llmModel: { value: "gpt-5.4" },
481
481
  llmReasoningEffort: { value: "default" },
482
482
  llmContextLength: { value: 0 },
483
- llmPromptContextTokens: { value: 16384 },
483
+ llmLocalContextTokens: { value: 0 },
484
484
  llmAllowKnxCommands: { value: false },
485
485
  llmRequireCommandConfirmation: { value: true },
486
+ etsExposedGAs: { value: [] },
487
+ etsReadOnlyGAs: { value: [] },
488
+ etsExposeConfigured: { value: false },
486
489
  webAccessEnabled: { value: true },
487
- webProactiveEnabled: { value: true },
488
- webProactiveIntervalMinutes: { value: 5 },
489
490
  webMaxCallsPerHour: { value: 40 },
490
491
  chatAdapterPreset: { value: "none" },
491
492
  chatInputCode: { value: "" },
@@ -529,6 +530,7 @@
529
530
  if ($mount.length && $section.length) $mount.append($section);
530
531
  };
531
532
  mountTabSection("#knx-ai-mount-assistant-setup", "#knx-ai-tab-llm-connection");
533
+ mountTabSection("#knx-ai-mount-ets-access", "#knx-ai-tab-ets-access");
532
534
  mountTabSection("#knx-ai-mount-assistant-advanced", "#knx-ai-tab-advanced");
533
535
  mountTabSection("#knx-ai-mount-chat-adapter", "#knx-ai-tab-chat-adapter");
534
536
  mountTabSection("#knx-ai-mount-home-intelligence", "#knx-ai-tab-home-intelligence");
@@ -572,6 +574,78 @@
572
574
  return value;
573
575
  };
574
576
 
577
+ // Same ETS access selector used by the MQTT profile of IoT Bridge.
578
+ try {
579
+ const savedSet = new Set(Array.isArray(node.etsExposedGAs) ? node.etsExposedGAs : []);
580
+ const readOnlySet = new Set(Array.isArray(node.etsReadOnlyGAs) ? node.etsReadOnlyGAs : []);
581
+ const exposeConfigured = node.etsExposeConfigured === true;
582
+
583
+ const updateEtsCount = () => {
584
+ const total = $("#knx-ai-ets-ga-list input.knx-ai-ets-ga-cb").length;
585
+ const selected = $("#knx-ai-ets-ga-list input.knx-ai-ets-ga-cb:checked").length;
586
+ $("#knx-ai-ets-ga-count").text(selected + " / " + total + " " + t("knxUltimateAI.messages.etsSelected", "selected"));
587
+ };
588
+
589
+ const buildEtsGaList = () => {
590
+ const list = $("#knx-ai-ets-ga-list").empty();
591
+ const serverId = String($("#node-input-server").val() || "").trim();
592
+ if (!serverId) {
593
+ list.append($("<div/>").css("color", "#999").text(t("knxUltimateAI.messages.etsNoGateway", "Select a KNX gateway.")));
594
+ updateEtsCount();
595
+ return;
596
+ }
597
+ $.getJSON("knxUltimatecsv?nodeID=" + encodeURIComponent(serverId), function (data) {
598
+ list.empty();
599
+ if (!Array.isArray(data) || data.length === 0) {
600
+ list.append($("<div/>").css("color", "#999").text(t("knxUltimateAI.messages.etsNoGa", "No group addresses found. Import the ETS list in the KNX gateway.")));
601
+ updateEtsCount();
602
+ return;
603
+ }
604
+ data.forEach(function (item) {
605
+ if (!item || !item.ga) return;
606
+ const row = $("<div/>").addClass("knx-ai-ets-ga-row").css({
607
+ display: "flex", gap: "8px", alignItems: "center", padding: "3px 2px",
608
+ width: "100%", whiteSpace: "nowrap", cursor: "pointer", borderBottom: "1px solid #f3f3f3"
609
+ }).appendTo(list);
610
+ const include = $("<input/>", { type: "checkbox", class: "knx-ai-ets-ga-cb", value: item.ga, style: "width:auto; margin:0; flex:0 0 auto;" }).appendTo(row);
611
+ include.prop("checked", exposeConfigured ? savedSet.has(item.ga) : true);
612
+ $("<span/>").css({ flex: "0 0 auto", fontFamily: "monospace", minWidth: "64px" }).text(item.ga).appendTo(row);
613
+ const name = String(item.devicename || item.ga);
614
+ $("<span/>").addClass("knx-ai-ets-ga-name").css({ flex: "1 1 auto", minWidth: "0", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "#444" }).text(name).attr("title", name).appendTo(row);
615
+ $("<span/>").css({ flex: "0 0 auto", color: "#888", fontSize: "11px" }).text("DPT" + (item.dpt || "")).appendTo(row);
616
+ const readOnlyLabel = $("<label/>").css({ flex: "0 0 auto", display: "flex", alignItems: "center", gap: "3px", margin: "0", fontSize: "11px", color: "#888", cursor: "pointer" }).appendTo(row);
617
+ const readOnly = $("<input/>", { type: "checkbox", class: "knx-ai-ets-ga-ro", value: item.ga, style: "width:auto; margin:0;" }).appendTo(readOnlyLabel);
618
+ readOnly.prop("checked", readOnlySet.has(item.ga));
619
+ $("<span/>").text(t("knxUltimateAI.messages.etsReadOnly", "Read only")).appendTo(readOnlyLabel);
620
+ readOnlyLabel.on("click", function (event) { event.stopPropagation(); });
621
+ row.on("click", function (event) {
622
+ if (event.target === include[0] || event.target === readOnly[0]) return;
623
+ include.prop("checked", !include.prop("checked"));
624
+ updateEtsCount();
625
+ });
626
+ });
627
+ $("#knx-ai-ets-ga-list input.knx-ai-ets-ga-cb").on("change", updateEtsCount);
628
+ updateEtsCount();
629
+ }).fail(function () {
630
+ list.empty().append($("<div/>").css("color", "#c0392b").text(t("knxUltimateAI.messages.etsCsvError", "Unable to load the group address list from the gateway.")));
631
+ updateEtsCount();
632
+ });
633
+ };
634
+
635
+ $("#knx-ai-ets-ga-all").on("click", function (event) { event.preventDefault(); $("#knx-ai-ets-ga-list .knx-ai-ets-ga-row:visible").find("input.knx-ai-ets-ga-cb").prop("checked", true); updateEtsCount(); });
636
+ $("#knx-ai-ets-ga-none").on("click", function (event) { event.preventDefault(); $("#knx-ai-ets-ga-list .knx-ai-ets-ga-row:visible").find("input.knx-ai-ets-ga-cb").prop("checked", false); updateEtsCount(); });
637
+ $("#knx-ai-ets-ga-ro-all").on("click", function (event) { event.preventDefault(); $("#knx-ai-ets-ga-list .knx-ai-ets-ga-row:visible").find("input.knx-ai-ets-ga-ro").prop("checked", true); });
638
+ $("#knx-ai-ets-ga-ro-none").on("click", function (event) { event.preventDefault(); $("#knx-ai-ets-ga-list .knx-ai-ets-ga-row:visible").find("input.knx-ai-ets-ga-ro").prop("checked", false); });
639
+ $("#knx-ai-ets-ga-filter").on("input", function () {
640
+ const term = String($(this).val() || "").toLowerCase();
641
+ $("#knx-ai-ets-ga-list .knx-ai-ets-ga-row").each(function () {
642
+ $(this).toggle($(this).text().toLowerCase().indexOf(term) !== -1);
643
+ });
644
+ });
645
+ $("#node-input-server").on("change.knxAiEtsAccess", buildEtsGaList);
646
+ buildEtsGaList();
647
+ } catch (error) { }
648
+
575
649
  const applyChatEditorOptions = (editor) => {
576
650
  try {
577
651
  if (!editor) return;
@@ -808,6 +882,21 @@
808
882
  $content.show();
809
883
  };
810
884
 
885
+ let lastChatContextOverview = null;
886
+ const selectedLocalContextTokens = () => Math.max(0, Number($("#node-input-llmLocalContextTokens").val()) || 0);
887
+ const formatCompactContextTokens = (value) => {
888
+ const tokens = Math.max(0, Number(value) || 0);
889
+ if (!tokens) return "?";
890
+ if (tokens >= 1024 && tokens % 1024 === 0) return (tokens / 1024) + "K";
891
+ return tokens.toLocaleString();
892
+ };
893
+ const selectedLocalContextLabel = () => {
894
+ const selectedTokens = selectedLocalContextTokens();
895
+ return selectedTokens
896
+ ? formatCompactContextTokens(selectedTokens)
897
+ : t("knxUltimateAI.selectlists.localContext.maximum", "Maximum");
898
+ };
899
+
811
900
  const renderChatContextOverview = (overview) => {
812
901
  const $status = $("#knx-ai-chat-context-status");
813
902
  const $content = $("#knx-ai-chat-context-content");
@@ -817,12 +906,14 @@
817
906
  const $contextLimit = $("#knx-ai-chat-context-limit");
818
907
  const $lastPromptUsage = $("#knx-ai-chat-context-last-prompt");
819
908
  if (!overview || typeof overview !== "object") {
909
+ lastChatContextOverview = null;
820
910
  $content.hide();
821
911
  $status
822
912
  .text(t("knxUltimateAI.messages.chatContextUnavailable", "The chat context summary is temporarily unavailable."))
823
913
  .show();
824
914
  return;
825
915
  }
916
+ lastChatContextOverview = overview;
826
917
 
827
918
  const addRow = function ($list, title, description, pathText, badge) {
828
919
  const $row = $("<div></div>").addClass("knx-ai-detected-adapter");
@@ -839,12 +930,6 @@
839
930
  $row.appendTo($list);
840
931
  };
841
932
 
842
- const formatCompactTokens = function (value) {
843
- const tokens = Math.max(0, Number(value) || 0);
844
- if (!tokens) return "?";
845
- if (tokens >= 1024 && tokens % 1024 === 0) return (tokens / 1024) + "K";
846
- return tokens.toLocaleString();
847
- };
848
933
  const formatBytes = function (value) {
849
934
  const bytes = Math.max(0, Number(value) || 0);
850
935
  if (bytes >= (1024 * 1024)) return (bytes / (1024 * 1024)).toLocaleString(undefined, { maximumFractionDigits: 1 }) + " MB";
@@ -853,24 +938,27 @@
853
938
  };
854
939
 
855
940
  const contextLimit = overview.contextLimit && typeof overview.contextLimit === "object" ? overview.contextLimit : {};
856
- const currentProvider = String(contextLimit.provider || $("#node-input-llmProvider").val() || "").toLowerCase();
857
- let contextLimitTokens = Math.max(0, Number(contextLimit.tokens) || 0);
858
- const contextIsProviderManaged = String(contextLimit.mode || "") === "provider-managed";
859
- if (!contextLimitTokens && !contextIsProviderManaged && (currentProvider === "lmstudio" || currentProvider === "ollama")) {
860
- const modelContextTokens = Math.max(0, Number($("#node-input-llmContextLength").val()) || 16384);
861
- const selectedContextValue = String($("#node-input-llmPromptContextTokens").val() || "16384");
862
- const selectedContextTokens = selectedContextValue === "0"
863
- ? 0
864
- : Math.min(16384, Math.max(4096, Number(selectedContextValue) || 16384));
865
- contextLimitTokens = selectedContextTokens === 0
866
- ? modelContextTokens
867
- : Math.min(modelContextTokens, selectedContextTokens);
941
+ const deployedProvider = String(contextLimit.provider || "").toLowerCase();
942
+ const currentProvider = String($("#node-input-llmProvider").val() || deployedProvider).toLowerCase();
943
+ let contextLimitTokens = currentProvider === deployedProvider
944
+ ? Math.max(0, Number(contextLimit.tokens) || 0)
945
+ : 0;
946
+ const localProvider = currentProvider === "lmstudio" || currentProvider === "ollama";
947
+ if (localProvider) {
948
+ const modelContextTokens = contextLimitTokens || Math.max(0, Number($("#node-input-llmContextLength").val()) || 0);
949
+ const selectedTokens = selectedLocalContextTokens();
950
+ contextLimitTokens = selectedTokens > 0
951
+ ? (modelContextTokens > 0 ? Math.min(modelContextTokens, selectedTokens) : selectedTokens)
952
+ : modelContextTokens;
868
953
  }
869
954
  const contextLimitValue = contextLimitTokens
870
- ? formatCompactTokens(contextLimitTokens) + " " + t("knxUltimateAI.messages.chatContextTokens", "tokens")
955
+ ? formatCompactContextTokens(contextLimitTokens) + " " + t("knxUltimateAI.messages.chatContextTokens", "tokens")
871
956
  : t("knxUltimateAI.messages.chatContextProviderManaged", "managed by the selected provider/model");
957
+ const localBudgetValue = localProvider
958
+ ? " · " + t("knxUltimateAI.messages.localContextBudget", "Local chat context") + ": " + selectedLocalContextLabel()
959
+ : "";
872
960
  $contextLimit
873
- .text(t("knxUltimateAI.messages.chatContextLimitLabel", "Maximum operational context") + ": " + contextLimitValue)
961
+ .text(t("knxUltimateAI.messages.chatContextLimitLabel", "Maximum operational context") + ": " + contextLimitValue + localBudgetValue)
874
962
  .attr("title", contextLimitTokens ? contextLimitTokens.toLocaleString() + " tokens" : contextLimitValue);
875
963
 
876
964
  const promptUsage = overview.lastPromptUsage && typeof overview.lastPromptUsage === "object" ? overview.lastPromptUsage : null;
@@ -878,8 +966,8 @@
878
966
  const exactInputTokens = Math.max(0, Number(promptUsage.exactInputTokens) || 0);
879
967
  const estimatedInputTokens = Math.max(0, Number(promptUsage.estimatedInputTokens) || 0);
880
968
  const tokenText = exactInputTokens
881
- ? formatCompactTokens(exactInputTokens) + " " + t("knxUltimateAI.messages.chatContextExactInputTokens", "input tokens measured by the provider")
882
- : "~" + formatCompactTokens(estimatedInputTokens) + " " + t("knxUltimateAI.messages.chatContextEstimatedInputTokens", "estimated input tokens");
969
+ ? formatCompactContextTokens(exactInputTokens) + " " + t("knxUltimateAI.messages.chatContextExactInputTokens", "input tokens measured by the provider")
970
+ : "~" + formatCompactContextTokens(estimatedInputTokens) + " " + t("knxUltimateAI.messages.chatContextEstimatedInputTokens", "estimated input tokens");
883
971
  const imageText = Number(promptUsage.imageCount) > 0
884
972
  ? " · " + Number(promptUsage.imageCount).toLocaleString() + " " + t("knxUltimateAI.messages.chatContextImages", "images")
885
973
  : "";
@@ -895,9 +983,9 @@
895
983
  }
896
984
 
897
985
  const sourceLabels = {
898
- knxTraffic: t("knxUltimateAI.messages.chatContextSourceKnxTraffic", "Live KNX summary, anomalies, topology and selected telegrams."),
899
- adapterHistory: t("knxUltimateAI.messages.chatContextSourceAdapterHistory", "Persistent history of automatically detected adapter events."),
900
- etsProject: t("knxUltimateAI.messages.chatContextSourceEtsProject", "ETS semantics and the full Node-RED project inventory."),
986
+ knxTraffic: t("knxUltimateAI.messages.chatContextSourceKnxTraffic", "Derived KNX analysis plus the latest exact events from the default 20-minute or explicit interval, automatically bounded for the active local-model window."),
987
+ adapterHistory: t("knxUltimateAI.messages.chatContextSourceAdapterHistory", "Latest exact adapter events from the same interval, automatically bounded for the active local-model window."),
988
+ etsProject: t("knxUltimateAI.messages.chatContextSourceEtsProject", "Complete selected semantic ETS catalog for cloud models. Local models receive the full catalog when it fits; otherwise they receive a window-sized manifest and exact model-requested details. Function source only for explicit code review."),
901
989
  memoryEducation: t("knxUltimateAI.messages.chatContextSourceMemoryEducation", "Session context, AI Education, bounded home memory and active plans."),
902
990
  cameras: t("knxUltimateAI.messages.chatContextSourceCameras", "Detected cameras and their available capabilities.")
903
991
  };
@@ -916,7 +1004,8 @@
916
1004
  homeMemory: t("knxUltimateAI.messages.chatContextFileHomeMemory", "Bounded learned home memory only; AI Education remains in the node property."),
917
1005
  schedules: t("knxUltimateAI.messages.chatContextFileSchedules", "Authoritative structured plans and reminders for this node."),
918
1006
  schedulesReadable: t("knxUltimateAI.messages.chatContextFileSchedulesReadable", "Human-readable plans and reminders generated from the scheduler state."),
919
- assistantConfig: t("knxUltimateAI.messages.chatContextFileAssistantConfig", "Persistent web Assistant configuration and semantic areas for this node.")
1007
+ assistantConfig: t("knxUltimateAI.messages.chatContextFileAssistantConfig", "Persistent web Assistant configuration and semantic areas for this node."),
1008
+ lastChatPrompt: t("knxUltimateAI.messages.chatContextFileLastChatPrompt", "Temporary local copy of the latest system and user messages sent to the chat model; overwritten on every chat request.")
920
1009
  };
921
1010
  (Array.isArray(overview.files) ? overview.files : []).forEach(function (item) {
922
1011
  if (!item || !item.name || !item.path) return;
@@ -1037,9 +1126,25 @@
1037
1126
  return tokens ? tokens.toLocaleString() : "?";
1038
1127
  };
1039
1128
 
1129
+ const updateLocalContextTokenOptions = (detail) => {
1130
+ const $select = $("#node-input-llmLocalContextTokens");
1131
+ if (!$select.length) return;
1132
+ const maxContextLength = Math.max(0, Number(detail && detail.maxContextLength) || 0);
1133
+ $select.find("option[data-context-tokens]").each(function () {
1134
+ const optionTokens = Math.max(0, Number($(this).attr("data-context-tokens")) || 0);
1135
+ const unavailable = maxContextLength > 0 && optionTokens > maxContextLength;
1136
+ $(this).prop("disabled", unavailable).prop("hidden", unavailable).toggle(!unavailable);
1137
+ });
1138
+ if (maxContextLength > 0 && selectedLocalContextTokens() > maxContextLength) {
1139
+ $select.val("0");
1140
+ }
1141
+ if (lastChatContextOverview) renderChatContextOverview(lastChatContextOverview);
1142
+ };
1143
+
1040
1144
  const setLocalModelContextStatus = (detail, state, errorText) => {
1041
1145
  const $status = $("#knx-ai-local-context-status");
1042
1146
  const provider = $("#node-input-llmProvider").val();
1147
+ updateLocalContextTokenOptions(detail);
1043
1148
  if (!$status.length || (provider !== "lmstudio" && provider !== "ollama") || !detail) {
1044
1149
  $status.hide();
1045
1150
  return;
@@ -1064,13 +1169,7 @@
1064
1169
  } else if (detail.loadedContextLength > 0) {
1065
1170
  text += " · " + t("knxUltimateAI.messages.lmStudioContextCurrentlyLoaded", "currently loaded") + ": " + loadedTokens + " token";
1066
1171
  }
1067
- if (provider === "lmstudio" || provider === "ollama") {
1068
- const selectedContextValue = String($("#node-input-llmPromptContextTokens").val() || "16384");
1069
- const selectedBudget = selectedContextValue === "0"
1070
- ? t("knxUltimateAI.selectlists.promptContext.unlimited", "No KNX AI limit (model context)")
1071
- : (Math.max(4096, Number(selectedContextValue) || 16384) / 1024) + "K";
1072
- text += " · " + t("knxUltimateAI.messages.localContextBudget", "KNX AI context budget") + ": " + selectedBudget;
1073
- }
1172
+ text += " · " + t("knxUltimateAI.messages.localContextBudget", "Local chat context") + ": " + selectedLocalContextLabel();
1074
1173
  $status.text(text).css("color", color).show();
1075
1174
  };
1076
1175
 
@@ -1160,6 +1259,7 @@
1160
1259
  const detail = localModelDetailsById.get(model);
1161
1260
  if (!detail || !(Number(detail.maxContextLength) > 0)) {
1162
1261
  $("#node-input-llmContextLength").val(0);
1262
+ updateLocalContextTokenOptions(null);
1163
1263
  setLocalModelContextStatus(null);
1164
1264
  return;
1165
1265
  }
@@ -1170,11 +1270,7 @@
1170
1270
  : Number(detail.maxContextLength);
1171
1271
  $("#node-input-llmContextLength").val(activeContextLength);
1172
1272
  if (provider === "ollama") {
1173
- const selectedContextValue = String($("#node-input-llmPromptContextTokens").val() || "16384");
1174
- const selectedContextTokens = selectedContextValue === "0"
1175
- ? Number(detail.maxContextLength)
1176
- : Math.min(16384, Math.max(4096, Number(selectedContextValue) || 16384));
1177
- detail.configuredContextLength = Math.min(Number(detail.maxContextLength), selectedContextTokens);
1273
+ detail.configuredContextLength = Number(detail.maxContextLength);
1178
1274
  $("#node-input-llmContextLength").val(detail.configuredContextLength);
1179
1275
  setLocalModelContextStatus(detail);
1180
1276
  return;
@@ -1187,7 +1283,8 @@
1187
1283
  const payload = {
1188
1284
  nodeId: nodeId,
1189
1285
  baseUrl: $("#node-input-llmBaseUrl").val() || LMSTUDIO_DEFAULT_CHAT_URL,
1190
- model: model
1286
+ model: model,
1287
+ localContextTokens: selectedLocalContextTokens()
1191
1288
  };
1192
1289
  const apiKey = $("#node-input-llmApiKey").val() || "";
1193
1290
  if (apiKey && apiKey !== "__PWRD__") payload.apiKey = apiKey;
@@ -1330,7 +1427,10 @@
1330
1427
  const provider = $("#node-input-llmProvider").val();
1331
1428
  const localProvider = provider === "lmstudio" || provider === "ollama";
1332
1429
  if (applyDefaults) applyProviderDefaults(provider);
1333
- $("#knx-ai-prompt-context-row").toggle(localProvider);
1430
+ $("#knx-ai-local-context-row").toggle(localProvider);
1431
+ if (localProvider) {
1432
+ updateLocalContextTokenOptions(localModelDetailsById.get(String($("#node-input-llmModel").val() || "").trim()));
1433
+ }
1334
1434
  if (provider === "ollama") {
1335
1435
  $("#knx-ai-apikey-row").hide();
1336
1436
  $("#knx-ai-ollama-install-row").show();
@@ -1347,11 +1447,9 @@
1347
1447
  };
1348
1448
  const toggleWebAccess = () => {
1349
1449
  const enabled = $("#node-input-webAccessEnabled").prop("checked") === true;
1350
- const proactive = enabled && $("#node-input-webProactiveEnabled").prop("checked") === true;
1351
1450
  $("#knx-ai-web-access-settings").toggle(enabled);
1352
- $("#knx-ai-web-proactive-interval-row").toggle(proactive);
1353
1451
  };
1354
- $("#node-input-webAccessEnabled, #node-input-webProactiveEnabled").on("change", toggleWebAccess);
1452
+ $("#node-input-webAccessEnabled").on("change", toggleWebAccess);
1355
1453
  toggleWebAccess();
1356
1454
  $("#node-input-llmProvider").on("change", function () {
1357
1455
  toggleProvider({ applyDefaults: true, autoLoadModels: true });
@@ -1365,14 +1463,10 @@
1365
1463
  $("#node-input-llmModel").on("change", function () {
1366
1464
  configureSelectedLocalModel();
1367
1465
  });
1368
- $("#node-input-llmPromptContextTokens").on("change", function () {
1466
+ $("#node-input-llmLocalContextTokens").on("change", function () {
1369
1467
  const detail = localModelDetailsById.get(String($("#node-input-llmModel").val() || "").trim());
1370
- if (!detail) return;
1371
- if ($("#node-input-llmProvider").val() === "ollama") {
1372
- configureSelectedLocalModel();
1373
- return;
1374
- }
1375
- setLocalModelContextStatus(detail, detail.loadedContextLength > 0 ? "configured" : "inactive");
1468
+ updateLocalContextTokenOptions(detail);
1469
+ if (detail) setLocalModelContextStatus(detail, detail.loadedContextLength > 0 ? "configured" : undefined);
1376
1470
  });
1377
1471
  $("#knx-ai-downloadOllamaModel").on("click", function (evt) {
1378
1472
  evt.preventDefault();
@@ -1385,6 +1479,18 @@
1385
1479
  },
1386
1480
  oneditsave: function () {
1387
1481
  try { RED.sidebar.show("info"); } catch (error) { }
1482
+ try {
1483
+ const checkboxes = $("#knx-ai-ets-ga-list input.knx-ai-ets-ga-cb");
1484
+ if (checkboxes.length > 0) {
1485
+ const exposed = [];
1486
+ const readOnly = [];
1487
+ checkboxes.each(function () { if ($(this).is(":checked")) exposed.push($(this).val()); });
1488
+ $("#knx-ai-ets-ga-list input.knx-ai-ets-ga-ro").each(function () { if ($(this).is(":checked")) readOnly.push($(this).val()); });
1489
+ this.etsExposedGAs = exposed;
1490
+ this.etsReadOnlyGAs = readOnly;
1491
+ this.etsExposeConfigured = true;
1492
+ }
1493
+ } catch (error) { }
1388
1494
  try {
1389
1495
  if (this.chatInputCodeEditor && typeof this.chatInputCodeEditor.getValue === "function") {
1390
1496
  this.chatInputCode = this.chatInputCodeEditor.getValue();
@@ -1459,6 +1565,10 @@
1459
1565
  <h4><i class="fa fa-cog"></i> <span data-i18n="knxUltimateAI.sections.quickSetup"></span></h4>
1460
1566
  <div class="knx-ai-accordion-mount" id="knx-ai-mount-assistant-setup"></div>
1461
1567
  </div>
1568
+ <div class="knx-ai-accordion-subsection">
1569
+ <h4><i class="fa fa-list"></i> <span data-i18n="knxUltimateAI.sections.etsAccess"></span></h4>
1570
+ <div class="knx-ai-accordion-mount" id="knx-ai-mount-ets-access"></div>
1571
+ </div>
1462
1572
  <div class="knx-ai-accordion-subsection">
1463
1573
  <h4><i class="fa fa-sliders"></i> <span data-i18n="knxUltimateAI.sections.advanced"></span></h4>
1464
1574
  <div class="knx-ai-accordion-mount" id="knx-ai-mount-assistant-advanced"></div>
@@ -1584,15 +1694,19 @@
1584
1694
  </div>
1585
1695
  <input type="hidden" id="node-input-llmContextLength">
1586
1696
  <div id="knx-ai-local-context-status" style="display:none;"></div>
1587
- <div class="form-row" id="knx-ai-prompt-context-row" style="display:none;">
1588
- <label style="width:290px" for="node-input-llmPromptContextTokens"><i class="fa fa-database"></i> <span data-i18n="knxUltimateAI.properties.llmPromptContextTokens"></span></label>
1589
- <select style="width:100%" id="node-input-llmPromptContextTokens">
1590
- <option value="4096" data-i18n="knxUltimateAI.selectlists.promptContext.small"></option>
1591
- <option value="8192" data-i18n="knxUltimateAI.selectlists.promptContext.medium"></option>
1592
- <option value="16384" data-i18n="knxUltimateAI.selectlists.promptContext.full"></option>
1593
- <option value="0" data-i18n="knxUltimateAI.selectlists.promptContext.unlimited"></option>
1697
+ <div class="form-row" id="knx-ai-local-context-row" style="display:none;">
1698
+ <label style="width:290px" for="node-input-llmLocalContextTokens"><i class="fa fa-database"></i> <span data-i18n="knxUltimateAI.properties.llmLocalContextTokens"></span></label>
1699
+ <select style="width:100%" id="node-input-llmLocalContextTokens">
1700
+ <option value="0" data-i18n="knxUltimateAI.selectlists.localContext.maximum"></option>
1701
+ <option value="4096" data-context-tokens="4096" data-i18n="knxUltimateAI.selectlists.localContext.k4"></option>
1702
+ <option value="8192" data-context-tokens="8192" data-i18n="knxUltimateAI.selectlists.localContext.k8"></option>
1703
+ <option value="16384" data-context-tokens="16384" data-i18n="knxUltimateAI.selectlists.localContext.k16"></option>
1704
+ <option value="32768" data-context-tokens="32768" data-i18n="knxUltimateAI.selectlists.localContext.k32"></option>
1705
+ <option value="65536" data-context-tokens="65536" data-i18n="knxUltimateAI.selectlists.localContext.k64"></option>
1706
+ <option value="131072" data-context-tokens="131072" data-i18n="knxUltimateAI.selectlists.localContext.k128"></option>
1707
+ <option value="262144" data-context-tokens="262144" data-i18n="knxUltimateAI.selectlists.localContext.k256"></option>
1594
1708
  </select>
1595
- <div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.promptContextHint"></div>
1709
+ <div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.localContextHint"></div>
1596
1710
  </div>
1597
1711
  <div class="form-row">
1598
1712
  <input type="checkbox" id="node-input-llmAllowKnxCommands" style="display:inline-block; width:auto; vertical-align:top;">
@@ -1641,22 +1755,6 @@
1641
1755
  <input type="number" id="node-input-webMaxCallsPerHour" min="1" max="60" step="1" style="width:100%;">
1642
1756
  <!-- <div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.webBudgetHint"></div> -->
1643
1757
  </div>
1644
- <div class="form-row">
1645
- <input type="checkbox" id="node-input-webProactiveEnabled" style="display:inline-block; width:auto; vertical-align:top;">
1646
- <label style="width:auto" for="node-input-webProactiveEnabled">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.webProactiveEnabled"></span></label>
1647
- <div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.webProactiveHint"></div>
1648
- </div>
1649
- <div class="form-row" id="knx-ai-web-proactive-interval-row">
1650
- <label style="width:290px" for="node-input-webProactiveIntervalMinutes"><i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateAI.properties.webProactiveIntervalMinutes"></span></label>
1651
- <select style="width:100%" id="node-input-webProactiveIntervalMinutes">
1652
- <option value="5" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.5"></option>
1653
- <option value="10" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.10"></option>
1654
- <option value="15" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.15"></option>
1655
- <option value="30" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.30"></option>
1656
- <option value="60" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.60"></option>
1657
- <option value="180" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.180"></option>
1658
- </select>
1659
- </div>
1660
1758
  </div>
1661
1759
  <hr>
1662
1760
  <div class="form-row">
@@ -1665,6 +1763,22 @@
1665
1763
  </div>
1666
1764
  </div>
1667
1765
 
1766
+ <div id="knx-ai-tab-ets-access">
1767
+ <div class="form-row">
1768
+ <div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.etsAccessHint"></div>
1769
+ <div style="margin:8px 0 4px; display:flex; gap:6px; align-items:center;">
1770
+ <input type="text" id="knx-ai-ets-ga-filter" data-i18n="[placeholder]knxUltimateAI.messages.etsFilterPlaceholder" style="flex:1 1 auto;">
1771
+ <button type="button" id="knx-ai-ets-ga-all" class="ui-button ui-corner-all ui-widget" style="width:auto;" data-i18n="knxUltimateAI.buttons.etsSelectAll"></button>
1772
+ <button type="button" id="knx-ai-ets-ga-none" class="ui-button ui-corner-all ui-widget" style="width:auto;" data-i18n="knxUltimateAI.buttons.etsSelectNone"></button>
1773
+ <span style="border-left:1px solid #ccc; align-self:stretch;"></span>
1774
+ <button type="button" id="knx-ai-ets-ga-ro-all" class="ui-button ui-corner-all ui-widget" style="width:auto;" data-i18n="[title]knxUltimateAI.messages.etsReadOnlyBulk;knxUltimateAI.buttons.etsReadOnlyAll"></button>
1775
+ <button type="button" id="knx-ai-ets-ga-ro-none" class="ui-button ui-corner-all ui-widget" style="width:auto;" data-i18n="[title]knxUltimateAI.messages.etsReadOnlyBulk;knxUltimateAI.buttons.etsReadOnlyNone"></button>
1776
+ </div>
1777
+ <div id="knx-ai-ets-ga-count" style="font-size:11px; color:#666; margin:2px 0;"></div>
1778
+ <div id="knx-ai-ets-ga-list" style="max-height:220px; overflow:auto; border:1px solid #ccc; padding:6px; border-radius:4px; background:#fff;"></div>
1779
+ </div>
1780
+ </div>
1781
+
1668
1782
  <div id="knx-ai-tab-advanced">
1669
1783
  <div id="knx-ai-llm-advanced-settings">
1670
1784
  <div class="form-row">