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

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.
Files changed (30) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/nodes/knxUltimateAI.html +210 -82
  3. package/nodes/knxUltimateAI.js +2554 -1660
  4. package/nodes/knxUltimateAIHomeAssistant.html +38 -0
  5. package/nodes/knxUltimateAIHomeAssistant.js +152 -0
  6. package/nodes/locales/de/knxUltimateAI.html +19 -16
  7. package/nodes/locales/de/knxUltimateAI.json +40 -31
  8. package/nodes/locales/en/knxUltimateAI.html +27 -18
  9. package/nodes/locales/en/knxUltimateAI.json +40 -31
  10. package/nodes/locales/es/knxUltimateAI.html +19 -16
  11. package/nodes/locales/es/knxUltimateAI.json +40 -31
  12. package/nodes/locales/fr/knxUltimateAI.html +19 -16
  13. package/nodes/locales/fr/knxUltimateAI.json +40 -31
  14. package/nodes/locales/it/knxUltimateAI.html +27 -18
  15. package/nodes/locales/it/knxUltimateAI.json +40 -31
  16. package/nodes/locales/zh-CN/knxUltimateAI.html +19 -16
  17. package/nodes/locales/zh-CN/knxUltimateAI.json +40 -31
  18. package/nodes/plugins/knxUltimate-cerebrum-runtime-plugin.js +79 -0
  19. package/nodes/plugins/knxUltimateAI-vue/assets/app.css +1 -1
  20. package/nodes/plugins/knxUltimateAI-vue/assets/app.js +13 -4
  21. package/nodes/utils/knxAiCamera.js +4 -4
  22. package/nodes/utils/knxAiCatalogRetrieval.js +349 -0
  23. package/nodes/utils/knxAiCerebrum.js +406 -0
  24. package/nodes/utils/knxAiChatContext.js +43 -1
  25. package/nodes/utils/knxAiHomeMemory.js +555 -13
  26. package/nodes/utils/knxAiScheduler.js +4 -1
  27. package/nodes/utils/knxAiSemanticContext.js +662 -0
  28. package/package.json +5 -3
  29. package/resources/KNXAIChatAdapterMappings.js +31 -5
  30. package/resources/hueControllerProfiles.js +7614 -7622
package/CHANGELOG.md CHANGED
@@ -6,6 +6,27 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
+ **Version 6.4.1** - September 2026<br/>
10
+
11
+ - **KNX AI — safe Cerebrum startup delivery**: corrected the RedBot Telegram envelope used by startup and other proactive messages after a Node-RED restart. Its synthetic chat context now fulfils RedBot's synchronous context contract instead of returning `undefined`, preventing `chat-platform` from raising an uncaught `sourceCode` error and terminating Node-RED. Saved RedBot adapter code is hardened at runtime as well. Schedule persistence now passes the generated Markdown filename to the atomic writer under the correct `filePath` key, removing the accompanying undefined-path warning.<br/>
12
+ - **KNX AI — Cerebrum (BETA)**: renamed the Web Assistant area to Cerebrum and introduced a passive home-automation intelligence layer that studies KNX telegrams, useful Node-RED flow events and the registered HUE, Matter and Home Assistant adapters. Cerebrum maintains bounded current-state and observation context locally, learns only repeated patterns spanning distinct days and asks the occupant for confirmation or correction through the Assistant output before a habit can become predictive. Confirmed habits remain subject to the exact ETS/DPT permissions: group addresses marked read-only are never written.<br/>
13
+ - **Cerebrum — Home Assistant adapter**: added the `Cerebrum Home Assistant` node and the shared runtime registry. When Node-RED is running as a Home Assistant add-on, Setup Doctor detects whether the required `ha-api` round trip is present and tells the user to add it when missing. Entity states, events and service capabilities are exposed to Cerebrum through bounded, vendor-neutral records without retaining raw messages, secrets or binary payloads.<br/>
14
+ - **Cerebrum — supervised startup**: every KNX AI node now asks its configured model to generate a short startup reassurance on the Assistant output. The message carries `msg.boot = true` for simple flow filtering and falls back to a localized deterministic notice if the provider is unavailable.<br/>
15
+ - **KNX AI Web — Cerebrum workspace and readable memory**: reorganized the Web navigation into Cerebrum submenus, removed the separate KNX AI node selector and bound the workspace directly to the node that opened it. Cerebrum Memory now offers an editable authoritative JSON view and a read-only simplified view; AI Chat Learning offers the equivalent editable native `.knxctx` view and localized simplified view. Copy follows the visible representation, while save, restore, download and concurrency checks continue to protect the complete authoritative files.<br/>
16
+ - **Cerebrum — complete import/export**: the Settings page now contains only import/export and its versioned backup includes AI configuration, chat learning, home memory, schedules and the readable schedules companion file, with validation and rollback-safe restoration.<br/>
17
+
18
+ **Version 6.3.32** - August 2026<br/>
19
+
20
+ - **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/>
21
+
22
+ **Version 6.3.31** - August 2026<br/>
23
+
24
+ - **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/>
25
+ - **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/>
26
+ - **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/>
27
+ - **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/>
28
+ - **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/>
29
+
9
30
  **Version 6.3.30** - August 2026<br/>
10
31
 
11
32
  - **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;
@@ -799,7 +873,7 @@
799
873
  .text(String(prompt.text))
800
874
  .on("click", function (evt) {
801
875
  evt.preventDefault();
802
- openKnxAiWebPage("assistant", String(prompt.text));
876
+ openKnxAiWebPage("cerebrum", String(prompt.text));
803
877
  })
804
878
  .appendTo($prompts);
805
879
  });
@@ -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 Cerebrum 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;
@@ -993,10 +1082,14 @@
993
1082
  const params = new URLSearchParams();
994
1083
  if (nodeId) params.set("nodeId", nodeId);
995
1084
  if (view === "chat-learning") {
996
- params.set("tab", "settings");
997
- params.set("settingsTab", "learning");
998
- } else if (view === "assistant") {
999
- params.set("tab", "assistant");
1085
+ params.set("tab", "cerebrum");
1086
+ params.set("cerebrumTab", "learning");
1087
+ } else if (view === "cerebrum-memory") {
1088
+ params.set("tab", "cerebrum");
1089
+ params.set("cerebrumTab", "memory");
1090
+ } else if (view === "cerebrum") {
1091
+ params.set("tab", "cerebrum");
1092
+ params.set("cerebrumTab", "conversation");
1000
1093
  if (prompt) params.set("prompt", String(prompt));
1001
1094
  }
1002
1095
  const accessToken = resolveAccessToken();
@@ -1016,6 +1109,11 @@
1016
1109
  openKnxAiWebPage("chat-learning");
1017
1110
  });
1018
1111
 
1112
+ $("#knx-ai-open-cerebrum-memory").on("click", function (evt) {
1113
+ evt.preventDefault();
1114
+ openKnxAiWebPage("cerebrum-memory");
1115
+ });
1116
+
1019
1117
  const setRefreshModelsBusy = (busy) => {
1020
1118
  const $button = $("#knx-ai-refreshModels");
1021
1119
  $button.prop("disabled", !!busy);
@@ -1037,9 +1135,25 @@
1037
1135
  return tokens ? tokens.toLocaleString() : "?";
1038
1136
  };
1039
1137
 
1138
+ const updateLocalContextTokenOptions = (detail) => {
1139
+ const $select = $("#node-input-llmLocalContextTokens");
1140
+ if (!$select.length) return;
1141
+ const maxContextLength = Math.max(0, Number(detail && detail.maxContextLength) || 0);
1142
+ $select.find("option[data-context-tokens]").each(function () {
1143
+ const optionTokens = Math.max(0, Number($(this).attr("data-context-tokens")) || 0);
1144
+ const unavailable = maxContextLength > 0 && optionTokens > maxContextLength;
1145
+ $(this).prop("disabled", unavailable).prop("hidden", unavailable).toggle(!unavailable);
1146
+ });
1147
+ if (maxContextLength > 0 && selectedLocalContextTokens() > maxContextLength) {
1148
+ $select.val("0");
1149
+ }
1150
+ if (lastChatContextOverview) renderChatContextOverview(lastChatContextOverview);
1151
+ };
1152
+
1040
1153
  const setLocalModelContextStatus = (detail, state, errorText) => {
1041
1154
  const $status = $("#knx-ai-local-context-status");
1042
1155
  const provider = $("#node-input-llmProvider").val();
1156
+ updateLocalContextTokenOptions(detail);
1043
1157
  if (!$status.length || (provider !== "lmstudio" && provider !== "ollama") || !detail) {
1044
1158
  $status.hide();
1045
1159
  return;
@@ -1064,13 +1178,7 @@
1064
1178
  } else if (detail.loadedContextLength > 0) {
1065
1179
  text += " · " + t("knxUltimateAI.messages.lmStudioContextCurrentlyLoaded", "currently loaded") + ": " + loadedTokens + " token";
1066
1180
  }
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
- }
1181
+ text += " · " + t("knxUltimateAI.messages.localContextBudget", "Local chat context") + ": " + selectedLocalContextLabel();
1074
1182
  $status.text(text).css("color", color).show();
1075
1183
  };
1076
1184
 
@@ -1160,6 +1268,7 @@
1160
1268
  const detail = localModelDetailsById.get(model);
1161
1269
  if (!detail || !(Number(detail.maxContextLength) > 0)) {
1162
1270
  $("#node-input-llmContextLength").val(0);
1271
+ updateLocalContextTokenOptions(null);
1163
1272
  setLocalModelContextStatus(null);
1164
1273
  return;
1165
1274
  }
@@ -1170,11 +1279,7 @@
1170
1279
  : Number(detail.maxContextLength);
1171
1280
  $("#node-input-llmContextLength").val(activeContextLength);
1172
1281
  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);
1282
+ detail.configuredContextLength = Number(detail.maxContextLength);
1178
1283
  $("#node-input-llmContextLength").val(detail.configuredContextLength);
1179
1284
  setLocalModelContextStatus(detail);
1180
1285
  return;
@@ -1187,7 +1292,8 @@
1187
1292
  const payload = {
1188
1293
  nodeId: nodeId,
1189
1294
  baseUrl: $("#node-input-llmBaseUrl").val() || LMSTUDIO_DEFAULT_CHAT_URL,
1190
- model: model
1295
+ model: model,
1296
+ localContextTokens: selectedLocalContextTokens()
1191
1297
  };
1192
1298
  const apiKey = $("#node-input-llmApiKey").val() || "";
1193
1299
  if (apiKey && apiKey !== "__PWRD__") payload.apiKey = apiKey;
@@ -1330,7 +1436,10 @@
1330
1436
  const provider = $("#node-input-llmProvider").val();
1331
1437
  const localProvider = provider === "lmstudio" || provider === "ollama";
1332
1438
  if (applyDefaults) applyProviderDefaults(provider);
1333
- $("#knx-ai-prompt-context-row").toggle(localProvider);
1439
+ $("#knx-ai-local-context-row").toggle(localProvider);
1440
+ if (localProvider) {
1441
+ updateLocalContextTokenOptions(localModelDetailsById.get(String($("#node-input-llmModel").val() || "").trim()));
1442
+ }
1334
1443
  if (provider === "ollama") {
1335
1444
  $("#knx-ai-apikey-row").hide();
1336
1445
  $("#knx-ai-ollama-install-row").show();
@@ -1347,11 +1456,9 @@
1347
1456
  };
1348
1457
  const toggleWebAccess = () => {
1349
1458
  const enabled = $("#node-input-webAccessEnabled").prop("checked") === true;
1350
- const proactive = enabled && $("#node-input-webProactiveEnabled").prop("checked") === true;
1351
1459
  $("#knx-ai-web-access-settings").toggle(enabled);
1352
- $("#knx-ai-web-proactive-interval-row").toggle(proactive);
1353
1460
  };
1354
- $("#node-input-webAccessEnabled, #node-input-webProactiveEnabled").on("change", toggleWebAccess);
1461
+ $("#node-input-webAccessEnabled").on("change", toggleWebAccess);
1355
1462
  toggleWebAccess();
1356
1463
  $("#node-input-llmProvider").on("change", function () {
1357
1464
  toggleProvider({ applyDefaults: true, autoLoadModels: true });
@@ -1365,14 +1472,10 @@
1365
1472
  $("#node-input-llmModel").on("change", function () {
1366
1473
  configureSelectedLocalModel();
1367
1474
  });
1368
- $("#node-input-llmPromptContextTokens").on("change", function () {
1475
+ $("#node-input-llmLocalContextTokens").on("change", function () {
1369
1476
  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");
1477
+ updateLocalContextTokenOptions(detail);
1478
+ if (detail) setLocalModelContextStatus(detail, detail.loadedContextLength > 0 ? "configured" : undefined);
1376
1479
  });
1377
1480
  $("#knx-ai-downloadOllamaModel").on("click", function (evt) {
1378
1481
  evt.preventDefault();
@@ -1385,6 +1488,18 @@
1385
1488
  },
1386
1489
  oneditsave: function () {
1387
1490
  try { RED.sidebar.show("info"); } catch (error) { }
1491
+ try {
1492
+ const checkboxes = $("#knx-ai-ets-ga-list input.knx-ai-ets-ga-cb");
1493
+ if (checkboxes.length > 0) {
1494
+ const exposed = [];
1495
+ const readOnly = [];
1496
+ checkboxes.each(function () { if ($(this).is(":checked")) exposed.push($(this).val()); });
1497
+ $("#knx-ai-ets-ga-list input.knx-ai-ets-ga-ro").each(function () { if ($(this).is(":checked")) readOnly.push($(this).val()); });
1498
+ this.etsExposedGAs = exposed;
1499
+ this.etsReadOnlyGAs = readOnly;
1500
+ this.etsExposeConfigured = true;
1501
+ }
1502
+ } catch (error) { }
1388
1503
  try {
1389
1504
  if (this.chatInputCodeEditor && typeof this.chatInputCodeEditor.getValue === "function") {
1390
1505
  this.chatInputCode = this.chatInputCodeEditor.getValue();
@@ -1459,6 +1574,10 @@
1459
1574
  <h4><i class="fa fa-cog"></i> <span data-i18n="knxUltimateAI.sections.quickSetup"></span></h4>
1460
1575
  <div class="knx-ai-accordion-mount" id="knx-ai-mount-assistant-setup"></div>
1461
1576
  </div>
1577
+ <div class="knx-ai-accordion-subsection">
1578
+ <h4><i class="fa fa-list"></i> <span data-i18n="knxUltimateAI.sections.etsAccess"></span></h4>
1579
+ <div class="knx-ai-accordion-mount" id="knx-ai-mount-ets-access"></div>
1580
+ </div>
1462
1581
  <div class="knx-ai-accordion-subsection">
1463
1582
  <h4><i class="fa fa-sliders"></i> <span data-i18n="knxUltimateAI.sections.advanced"></span></h4>
1464
1583
  <div class="knx-ai-accordion-mount" id="knx-ai-mount-assistant-advanced"></div>
@@ -1480,6 +1599,11 @@
1480
1599
  <button type="button" class="red-ui-button" id="knx-ai-open-chat-learning">
1481
1600
  <i class="fa fa-external-link"></i> <span data-i18n="knxUltimateAI.buttons.openChatLearning"></span>
1482
1601
  </button>
1602
+ <h4 style="margin-top:14px;"><i class="fa fa-database"></i> <span data-i18n="knxUltimateAI.sections.cerebrumMemory"></span></h4>
1603
+ <p class="knx-ai-chat-learning-link-copy" data-i18n="knxUltimateAI.messages.cerebrumMemoryOpenHint"></p>
1604
+ <button type="button" class="red-ui-button" id="knx-ai-open-cerebrum-memory">
1605
+ <i class="fa fa-external-link"></i> <span data-i18n="knxUltimateAI.buttons.openCerebrumMemory"></span>
1606
+ </button>
1483
1607
  </div>
1484
1608
  <div id="knx-ai-detected-adapters-panel">
1485
1609
  <h4><i class="fa fa-puzzle-piece"></i> <span data-i18n="knxUltimateAI.sections.detectedAdapters"></span></h4>
@@ -1584,15 +1708,19 @@
1584
1708
  </div>
1585
1709
  <input type="hidden" id="node-input-llmContextLength">
1586
1710
  <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>
1711
+ <div class="form-row" id="knx-ai-local-context-row" style="display:none;">
1712
+ <label style="width:290px" for="node-input-llmLocalContextTokens"><i class="fa fa-database"></i> <span data-i18n="knxUltimateAI.properties.llmLocalContextTokens"></span></label>
1713
+ <select style="width:100%" id="node-input-llmLocalContextTokens">
1714
+ <option value="0" data-i18n="knxUltimateAI.selectlists.localContext.maximum"></option>
1715
+ <option value="4096" data-context-tokens="4096" data-i18n="knxUltimateAI.selectlists.localContext.k4"></option>
1716
+ <option value="8192" data-context-tokens="8192" data-i18n="knxUltimateAI.selectlists.localContext.k8"></option>
1717
+ <option value="16384" data-context-tokens="16384" data-i18n="knxUltimateAI.selectlists.localContext.k16"></option>
1718
+ <option value="32768" data-context-tokens="32768" data-i18n="knxUltimateAI.selectlists.localContext.k32"></option>
1719
+ <option value="65536" data-context-tokens="65536" data-i18n="knxUltimateAI.selectlists.localContext.k64"></option>
1720
+ <option value="131072" data-context-tokens="131072" data-i18n="knxUltimateAI.selectlists.localContext.k128"></option>
1721
+ <option value="262144" data-context-tokens="262144" data-i18n="knxUltimateAI.selectlists.localContext.k256"></option>
1594
1722
  </select>
1595
- <div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.promptContextHint"></div>
1723
+ <div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.localContextHint"></div>
1596
1724
  </div>
1597
1725
  <div class="form-row">
1598
1726
  <input type="checkbox" id="node-input-llmAllowKnxCommands" style="display:inline-block; width:auto; vertical-align:top;">
@@ -1641,22 +1769,6 @@
1641
1769
  <input type="number" id="node-input-webMaxCallsPerHour" min="1" max="60" step="1" style="width:100%;">
1642
1770
  <!-- <div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.webBudgetHint"></div> -->
1643
1771
  </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
1772
  </div>
1661
1773
  <hr>
1662
1774
  <div class="form-row">
@@ -1665,6 +1777,22 @@
1665
1777
  </div>
1666
1778
  </div>
1667
1779
 
1780
+ <div id="knx-ai-tab-ets-access">
1781
+ <div class="form-row">
1782
+ <div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.etsAccessHint"></div>
1783
+ <div style="margin:8px 0 4px; display:flex; gap:6px; align-items:center;">
1784
+ <input type="text" id="knx-ai-ets-ga-filter" data-i18n="[placeholder]knxUltimateAI.messages.etsFilterPlaceholder" style="flex:1 1 auto;">
1785
+ <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>
1786
+ <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>
1787
+ <span style="border-left:1px solid #ccc; align-self:stretch;"></span>
1788
+ <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>
1789
+ <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>
1790
+ </div>
1791
+ <div id="knx-ai-ets-ga-count" style="font-size:11px; color:#666; margin:2px 0;"></div>
1792
+ <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>
1793
+ </div>
1794
+ </div>
1795
+
1668
1796
  <div id="knx-ai-tab-advanced">
1669
1797
  <div id="knx-ai-llm-advanced-settings">
1670
1798
  <div class="form-row">