node-red-contrib-knx-ultimate 4.3.5 → 4.3.7
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 +13 -0
- package/nodes/knxUltimate-config.js +45 -10
- package/nodes/knxUltimate.html +71 -35
- package/nodes/knxUltimateAI.html +87 -106
- package/nodes/knxUltimateAI.js +345 -23
- package/nodes/locales/de/knxUltimateAI.html +18 -11
- package/nodes/locales/de/knxUltimateAI.json +9 -12
- package/nodes/locales/en/knxUltimateAI.html +18 -11
- package/nodes/locales/en/knxUltimateAI.json +9 -12
- package/nodes/locales/es/knxUltimateAI.html +18 -11
- package/nodes/locales/es/knxUltimateAI.json +9 -12
- package/nodes/locales/fr/knxUltimateAI.html +18 -11
- package/nodes/locales/fr/knxUltimateAI.json +9 -12
- package/nodes/locales/it/knxUltimateAI.html +18 -11
- package/nodes/locales/it/knxUltimateAI.json +9 -12
- package/nodes/locales/zh-CN/knxUltimateAI.html +18 -11
- package/nodes/locales/zh-CN/knxUltimateAI.json +9 -12
- package/package.json +1 -1
package/nodes/knxUltimateAI.js
CHANGED
|
@@ -875,6 +875,106 @@ const readJsonFileSafe = (filePath, fallbackValue) => {
|
|
|
875
875
|
}
|
|
876
876
|
}
|
|
877
877
|
|
|
878
|
+
const formatArchiveDayKey = (ts) => {
|
|
879
|
+
try {
|
|
880
|
+
return new Date(ts).toISOString().slice(0, 10)
|
|
881
|
+
} catch (error) {
|
|
882
|
+
return new Date().toISOString().slice(0, 10)
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const collectArchiveDayKeysBetween = ({ fromTs, toTs }) => {
|
|
887
|
+
const out = []
|
|
888
|
+
const start = Number(fromTs || 0)
|
|
889
|
+
const end = Number(toTs || 0)
|
|
890
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return out
|
|
891
|
+
const cursor = new Date(start)
|
|
892
|
+
cursor.setUTCHours(0, 0, 0, 0)
|
|
893
|
+
const endDay = new Date(end)
|
|
894
|
+
endDay.setUTCHours(0, 0, 0, 0)
|
|
895
|
+
while (cursor.getTime() <= endDay.getTime()) {
|
|
896
|
+
out.push(cursor.toISOString().slice(0, 10))
|
|
897
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1)
|
|
898
|
+
}
|
|
899
|
+
return out
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const startOfLocalDayMs = (ts) => {
|
|
903
|
+
const d = new Date(ts)
|
|
904
|
+
d.setHours(0, 0, 0, 0)
|
|
905
|
+
return d.getTime()
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const endOfLocalDayMs = (ts) => {
|
|
909
|
+
const d = new Date(ts)
|
|
910
|
+
d.setHours(23, 59, 59, 999)
|
|
911
|
+
return d.getTime()
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const parseQuestionTimeRange = (question, nowTs = Date.now()) => {
|
|
915
|
+
const text = String(question || '').trim().toLowerCase()
|
|
916
|
+
if (!text) return null
|
|
917
|
+
|
|
918
|
+
const exactDates = Array.from(text.matchAll(/\b(\d{4}-\d{2}-\d{2})\b/g)).map(match => match[1])
|
|
919
|
+
if (exactDates.length >= 2) {
|
|
920
|
+
const start = new Date(`${exactDates[0]}T00:00:00`).getTime()
|
|
921
|
+
const end = new Date(`${exactDates[1]}T23:59:59.999`).getTime()
|
|
922
|
+
if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {
|
|
923
|
+
return { fromTs: start, toTs: end, label: `${exactDates[0]}..${exactDates[1]}`, explicit: true }
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (exactDates.length === 1) {
|
|
927
|
+
const start = new Date(`${exactDates[0]}T00:00:00`).getTime()
|
|
928
|
+
const end = new Date(`${exactDates[0]}T23:59:59.999`).getTime()
|
|
929
|
+
if (Number.isFinite(start) && Number.isFinite(end)) {
|
|
930
|
+
return { fromTs: start, toTs: end, label: exactDates[0], explicit: true }
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const dayStart = startOfLocalDayMs(nowTs)
|
|
935
|
+
const dayEnd = endOfLocalDayMs(nowTs)
|
|
936
|
+
const yesterdayStart = dayStart - (24 * 60 * 60 * 1000)
|
|
937
|
+
const yesterdayEnd = dayStart - 1
|
|
938
|
+
|
|
939
|
+
if (/\b(stamattina|this morning)\b/.test(text)) {
|
|
940
|
+
return { fromTs: dayStart, toTs: Math.min(dayEnd, dayStart + (12 * 60 * 60 * 1000) - 1), label: 'this morning', explicit: true }
|
|
941
|
+
}
|
|
942
|
+
if (/\b(oggi pomeriggio|this afternoon)\b/.test(text)) {
|
|
943
|
+
return { fromTs: dayStart + (12 * 60 * 60 * 1000), toTs: Math.min(dayEnd, dayStart + (18 * 60 * 60 * 1000) - 1), label: 'this afternoon', explicit: true }
|
|
944
|
+
}
|
|
945
|
+
if (/\b(stasera|this evening|tonight)\b/.test(text)) {
|
|
946
|
+
return { fromTs: dayStart + (18 * 60 * 60 * 1000), toTs: dayEnd, label: 'this evening', explicit: true }
|
|
947
|
+
}
|
|
948
|
+
if (/\b(oggi|today)\b/.test(text)) {
|
|
949
|
+
return { fromTs: dayStart, toTs: dayEnd, label: 'today', explicit: true }
|
|
950
|
+
}
|
|
951
|
+
if (/\b(ieri|yesterday)\b/.test(text)) {
|
|
952
|
+
return { fromTs: yesterdayStart, toTs: yesterdayEnd, label: 'yesterday', explicit: true }
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const lastDaysMatch = text.match(/\b(?:last|ultimi)\s+(\d{1,3})\s+(?:day|days|giorno|giorni)\b/)
|
|
956
|
+
if (lastDaysMatch) {
|
|
957
|
+
const days = Math.max(1, Number(lastDaysMatch[1] || 1))
|
|
958
|
+
return {
|
|
959
|
+
fromTs: nowTs - (days * 24 * 60 * 60 * 1000),
|
|
960
|
+
toTs: nowTs,
|
|
961
|
+
label: `last ${days} days`,
|
|
962
|
+
explicit: true
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (/\b(this week|questa settimana)\b/.test(text)) {
|
|
967
|
+
const d = new Date(nowTs)
|
|
968
|
+
const dow = d.getDay()
|
|
969
|
+
const offset = dow === 0 ? 6 : dow - 1
|
|
970
|
+
d.setHours(0, 0, 0, 0)
|
|
971
|
+
d.setDate(d.getDate() - offset)
|
|
972
|
+
return { fromTs: d.getTime(), toTs: nowTs, label: 'this week', explicit: true }
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return null
|
|
976
|
+
}
|
|
977
|
+
|
|
878
978
|
const normalizeAreaOverridePayload = (payload) => {
|
|
879
979
|
const p = payload && typeof payload === 'object' ? payload : {}
|
|
880
980
|
const normalized = {}
|
|
@@ -2602,13 +2702,14 @@ module.exports = function (RED) {
|
|
|
2602
2702
|
return catalog
|
|
2603
2703
|
}
|
|
2604
2704
|
|
|
2605
|
-
const
|
|
2705
|
+
const buildKnxUltimateProjectInventory = () => {
|
|
2606
2706
|
const tabById = new Map()
|
|
2607
2707
|
const gatewaysById = new Map()
|
|
2608
|
-
const
|
|
2708
|
+
const flowNodes = []
|
|
2609
2709
|
|
|
2610
2710
|
try {
|
|
2611
2711
|
if (typeof RED.nodes.eachNode !== 'function') return ''
|
|
2712
|
+
const gaRe = /\b\d{1,3}\/\d{1,3}\/\d{1,3}\b/g
|
|
2612
2713
|
|
|
2613
2714
|
// First pass: collect tabs + gateways
|
|
2614
2715
|
RED.nodes.eachNode((n) => {
|
|
@@ -2627,11 +2728,11 @@ module.exports = function (RED) {
|
|
|
2627
2728
|
}
|
|
2628
2729
|
})
|
|
2629
2730
|
|
|
2630
|
-
// Second pass: collect
|
|
2731
|
+
// Second pass: collect all flow nodes that may help the LLM understand KNX logic.
|
|
2631
2732
|
RED.nodes.eachNode((n) => {
|
|
2632
2733
|
if (!n || typeof n !== 'object') return
|
|
2633
2734
|
const type = String(n.type || '')
|
|
2634
|
-
if (
|
|
2735
|
+
if (type === 'tab' || type === 'subflow' || type === 'knxUltimate-config') return
|
|
2635
2736
|
|
|
2636
2737
|
const tabId = String(n.z || '')
|
|
2637
2738
|
const tabLabel = tabById.get(tabId) || ''
|
|
@@ -2640,6 +2741,16 @@ module.exports = function (RED) {
|
|
|
2640
2741
|
const server = String(n.server || '')
|
|
2641
2742
|
const gw = gatewaysById.get(server) || null
|
|
2642
2743
|
|
|
2744
|
+
const gaRefs = new Set()
|
|
2745
|
+
extractGAsFromValue({ value: n, outSet: gaRefs, gaRe, maxItems: 24 })
|
|
2746
|
+
const gaList = Array.from(gaRefs.values()).slice(0, 24)
|
|
2747
|
+
|
|
2748
|
+
const shortenSnippet = (value, maxLen = 140) => {
|
|
2749
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim()
|
|
2750
|
+
if (!text) return ''
|
|
2751
|
+
return text.length > maxLen ? `${text.slice(0, Math.max(0, maxLen - 3))}...` : text
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2643
2754
|
const entry = {
|
|
2644
2755
|
tabLabel,
|
|
2645
2756
|
type,
|
|
@@ -2648,7 +2759,12 @@ module.exports = function (RED) {
|
|
|
2648
2759
|
gatewayId: server,
|
|
2649
2760
|
gatewayName: gw ? gw.name : '',
|
|
2650
2761
|
topic: n.topic !== undefined ? String(n.topic) : '',
|
|
2651
|
-
dpt: n.dpt !== undefined ? String(n.dpt) : ''
|
|
2762
|
+
dpt: n.dpt !== undefined ? String(n.dpt) : '',
|
|
2763
|
+
gaRefs: gaList,
|
|
2764
|
+
payload: n.payload !== undefined ? shortenSnippet(n.payload, 80) : '',
|
|
2765
|
+
payloadType: n.payloadType !== undefined ? String(n.payloadType) : '',
|
|
2766
|
+
outputTopic: n.outputtopic !== undefined ? String(n.outputtopic) : '',
|
|
2767
|
+
setTopicType: n.setTopicType !== undefined ? String(n.setTopicType) : ''
|
|
2652
2768
|
}
|
|
2653
2769
|
|
|
2654
2770
|
if (type === 'knxUltimate') {
|
|
@@ -2669,17 +2785,31 @@ module.exports = function (RED) {
|
|
|
2669
2785
|
entry.gaRewriteRules = n.gaRewriteRules !== undefined ? String(n.gaRewriteRules) : ''
|
|
2670
2786
|
entry.rewriteSource = n.rewriteSource === true || n.rewriteSource === 'true'
|
|
2671
2787
|
entry.srcRewriteRules = n.srcRewriteRules !== undefined ? String(n.srcRewriteRules) : ''
|
|
2672
|
-
}
|
|
2673
|
-
|
|
2674
|
-
|
|
2788
|
+
} else if (type === 'function') {
|
|
2789
|
+
entry.funcSnippet = shortenSnippet(n.func, 220)
|
|
2790
|
+
} else if (type === 'change') {
|
|
2791
|
+
entry.rulesSnippet = shortenSnippet(safeStringify(n.rules), 180)
|
|
2792
|
+
} else if (type === 'inject') {
|
|
2793
|
+
entry.injectOnce = n.once === true || n.once === 'true'
|
|
2794
|
+
entry.repeat = n.repeat !== undefined ? String(n.repeat) : ''
|
|
2795
|
+
entry.crontab = n.crontab !== undefined ? String(n.crontab) : ''
|
|
2796
|
+
} else if (type === 'template') {
|
|
2797
|
+
entry.templateSnippet = shortenSnippet(n.template, 180)
|
|
2798
|
+
} else if (type === 'switch') {
|
|
2799
|
+
entry.rulesSnippet = shortenSnippet(safeStringify(n.rules), 180)
|
|
2800
|
+
} else if (type === 'api-current-state' || type === 'server-state-changed') {
|
|
2801
|
+
entry.entityId = n.entityid !== undefined ? String(n.entityid) : ''
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
flowNodes.push(entry)
|
|
2675
2805
|
})
|
|
2676
2806
|
} catch (error) {
|
|
2677
2807
|
return ''
|
|
2678
2808
|
}
|
|
2679
2809
|
|
|
2680
|
-
if (!
|
|
2810
|
+
if (!flowNodes.length && !gatewaysById.size) return ''
|
|
2681
2811
|
|
|
2682
|
-
const sorted =
|
|
2812
|
+
const sorted = flowNodes
|
|
2683
2813
|
.sort((a, b) => {
|
|
2684
2814
|
const at = (a.tabLabel || '').localeCompare(b.tabLabel || '')
|
|
2685
2815
|
if (at !== 0) return at
|
|
@@ -2687,13 +2817,12 @@ module.exports = function (RED) {
|
|
|
2687
2817
|
if (an !== 0) return an
|
|
2688
2818
|
return (a.type || '').localeCompare(b.type || '')
|
|
2689
2819
|
})
|
|
2690
|
-
.slice(0, Math.max(0, Number(maxNodes) || 0))
|
|
2691
2820
|
|
|
2692
2821
|
const shorten = (id) => (id && id.length > 8) ? id.slice(0, 8) : id
|
|
2693
2822
|
const safeLine = (s) => String(s || '').replace(/\s+/g, ' ').trim()
|
|
2694
2823
|
|
|
2695
2824
|
const lines = []
|
|
2696
|
-
lines.push('Node-RED
|
|
2825
|
+
lines.push('Node-RED project inventory:')
|
|
2697
2826
|
|
|
2698
2827
|
if (gatewaysById.size) {
|
|
2699
2828
|
lines.push(`Gateways (knxUltimate-config): ${gatewaysById.size}`)
|
|
@@ -2707,7 +2836,7 @@ module.exports = function (RED) {
|
|
|
2707
2836
|
if (gatewaysById.size > 20) lines.push('- ...')
|
|
2708
2837
|
}
|
|
2709
2838
|
|
|
2710
|
-
lines.push(`
|
|
2839
|
+
lines.push(`Project nodes: ${flowNodes.length}`)
|
|
2711
2840
|
for (const n of sorted) {
|
|
2712
2841
|
const parts = []
|
|
2713
2842
|
if (n.tabLabel) parts.push(`[${safeLine(n.tabLabel)}]`)
|
|
@@ -2716,6 +2845,7 @@ module.exports = function (RED) {
|
|
|
2716
2845
|
if (n.name) parts.push(`name="${safeLine(n.name)}"`)
|
|
2717
2846
|
if (n.gatewayName) parts.push(`gw="${safeLine(n.gatewayName)}"`)
|
|
2718
2847
|
if (!n.gatewayName && n.gatewayId) parts.push(`gwId=${shorten(n.gatewayId)}`)
|
|
2848
|
+
if (Array.isArray(n.gaRefs) && n.gaRefs.length) parts.push(`gaRefs="${safeLine(n.gaRefs.join(','))}"`)
|
|
2719
2849
|
|
|
2720
2850
|
if (n.type === 'knxUltimate') {
|
|
2721
2851
|
if (n.topic) parts.push(`topic=${safeLine(n.topic)}`)
|
|
@@ -2733,8 +2863,26 @@ module.exports = function (RED) {
|
|
|
2733
2863
|
if (n.gaRewriteRules) parts.push(`gaRewriteRules="${safeLine(n.gaRewriteRules)}"`)
|
|
2734
2864
|
if (n.rewriteSource) parts.push('rewriteSource=true')
|
|
2735
2865
|
if (n.srcRewriteRules) parts.push(`srcRewriteRules="${safeLine(n.srcRewriteRules)}"`)
|
|
2866
|
+
} else if (n.type === 'function') {
|
|
2867
|
+
if (n.funcSnippet) parts.push(`func="${safeLine(n.funcSnippet)}"`)
|
|
2868
|
+
} else if (n.type === 'change' || n.type === 'switch') {
|
|
2869
|
+
if (n.rulesSnippet) parts.push(`rules="${safeLine(n.rulesSnippet)}"`)
|
|
2870
|
+
} else if (n.type === 'inject') {
|
|
2871
|
+
if (n.topic) parts.push(`topic=${safeLine(n.topic)}`)
|
|
2872
|
+
if (n.payload) parts.push(`payload="${safeLine(n.payload)}"`)
|
|
2873
|
+
if (n.payloadType) parts.push(`payloadType=${safeLine(n.payloadType)}`)
|
|
2874
|
+
if (n.injectOnce) parts.push('once=true')
|
|
2875
|
+
if (n.repeat) parts.push(`repeat=${safeLine(n.repeat)}`)
|
|
2876
|
+
if (n.crontab) parts.push(`crontab="${safeLine(n.crontab)}"`)
|
|
2877
|
+
} else if (n.type === 'template') {
|
|
2878
|
+
if (n.templateSnippet) parts.push(`template="${safeLine(n.templateSnippet)}"`)
|
|
2879
|
+
} else if (n.type === 'api-current-state' || n.type === 'server-state-changed') {
|
|
2880
|
+
if (n.entityId) parts.push(`entityId=${safeLine(n.entityId)}`)
|
|
2736
2881
|
} else {
|
|
2737
2882
|
if (n.topic) parts.push(`topic=${safeLine(n.topic)}`)
|
|
2883
|
+
if (n.payload) parts.push(`payload="${safeLine(n.payload)}"`)
|
|
2884
|
+
if (n.outputTopic) parts.push(`outputTopic=${safeLine(n.outputTopic)}`)
|
|
2885
|
+
if (n.setTopicType) parts.push(`setTopicType=${safeLine(n.setTopicType)}`)
|
|
2738
2886
|
}
|
|
2739
2887
|
lines.push(`- ${parts.join(' ')}`)
|
|
2740
2888
|
}
|
|
@@ -3531,6 +3679,8 @@ module.exports = function (RED) {
|
|
|
3531
3679
|
|
|
3532
3680
|
node.analysisWindowSec = Number(config.analysisWindowSec || 60)
|
|
3533
3681
|
node.historyWindowSec = Number(config.historyWindowSec || 300)
|
|
3682
|
+
node.historyStoreToDisk = config.historyStoreToDisk !== undefined ? coerceBoolean(config.historyStoreToDisk) : false
|
|
3683
|
+
node.historyStoreRetentionDays = Math.max(1, Number.isFinite(Number(config.historyStoreRetentionDays)) ? Number(config.historyStoreRetentionDays) : 10)
|
|
3534
3684
|
node.emitIntervalSec = Number(config.emitIntervalSec || 0)
|
|
3535
3685
|
node.topN = Number(config.topN || 10)
|
|
3536
3686
|
|
|
@@ -3561,9 +3711,6 @@ module.exports = function (RED) {
|
|
|
3561
3711
|
node.llmMaxEventsInPrompt = (config.llmMaxEventsInPrompt === undefined || config.llmMaxEventsInPrompt === '') ? 120 : Number(config.llmMaxEventsInPrompt)
|
|
3562
3712
|
node.llmIncludeRaw = config.llmIncludeRaw !== undefined ? coerceBoolean(config.llmIncludeRaw) : false
|
|
3563
3713
|
node.llmIncludeFlowContext = config.llmIncludeFlowContext !== undefined ? coerceBoolean(config.llmIncludeFlowContext) : true
|
|
3564
|
-
node.llmMaxFlowNodesInPrompt = (config.llmMaxFlowNodesInPrompt === undefined || config.llmMaxFlowNodesInPrompt === '')
|
|
3565
|
-
? 400
|
|
3566
|
-
: Number(config.llmMaxFlowNodesInPrompt)
|
|
3567
3714
|
node.llmIncludeDocsSnippets = config.llmIncludeDocsSnippets !== undefined ? coerceBoolean(config.llmIncludeDocsSnippets) : true
|
|
3568
3715
|
node.llmDocsLanguage = config.llmDocsLanguage ? String(config.llmDocsLanguage) : 'it'
|
|
3569
3716
|
node.llmDocsMaxSnippets = (config.llmDocsMaxSnippets === undefined || config.llmDocsMaxSnippets === '') ? 5 : Number(config.llmDocsMaxSnippets)
|
|
@@ -3632,6 +3779,7 @@ module.exports = function (RED) {
|
|
|
3632
3779
|
node._gaRateSeries = new Map()
|
|
3633
3780
|
node._gaLabelCsvCache = { ref: null, map: {} }
|
|
3634
3781
|
node._busConnectionWatchTimer = null
|
|
3782
|
+
node._historyDiskLastPruneAt = 0
|
|
3635
3783
|
node._busConnectionState = (node.serverKNX && typeof node.serverKNX.linkStatus === 'string')
|
|
3636
3784
|
? String(node.serverKNX.linkStatus).toLowerCase()
|
|
3637
3785
|
: 'unknown'
|
|
@@ -4738,7 +4886,8 @@ module.exports = function (RED) {
|
|
|
4738
4886
|
const compactMode = compact === true
|
|
4739
4887
|
const maxEventsRequested = Math.max(10, Number(node.llmMaxEventsInPrompt) || 120)
|
|
4740
4888
|
const maxEvents = Math.min(compactMode ? 80 : 240, maxEventsRequested)
|
|
4741
|
-
const
|
|
4889
|
+
const promptEvents = selectTelegramsForPrompt({ question, maxEvents })
|
|
4890
|
+
const recent = Array.isArray(promptEvents.events) ? promptEvents.events : []
|
|
4742
4891
|
const wantsSvgChart = shouldGenerateSvgChart(question)
|
|
4743
4892
|
const areasSnapshot = buildAreasSnapshot({ summary })
|
|
4744
4893
|
const areasContext = buildAreasPromptContext(areasSnapshot)
|
|
@@ -4751,6 +4900,7 @@ module.exports = function (RED) {
|
|
|
4751
4900
|
return `${new Date(t.ts).toISOString()} ${t.event} ${t.source} -> ${t.destination}${devName} dpt=${t.dpt} payload=${payloadStr}${rawStr}`
|
|
4752
4901
|
})
|
|
4753
4902
|
const recentLines = takeLastItemsByCharBudget(lines, compactMode ? 2600 : 7000)
|
|
4903
|
+
const archiveScopeLine = `Prompt event source: ${promptEvents.source}. Time range: ${promptEvents.range && promptEvents.range.label ? promptEvents.range.label : 'recent events'}. Events selected: ${recent.length}.`
|
|
4754
4904
|
|
|
4755
4905
|
let flowContext = ''
|
|
4756
4906
|
if (node.llmIncludeFlowContext) {
|
|
@@ -4760,11 +4910,7 @@ module.exports = function (RED) {
|
|
|
4760
4910
|
if (node._flowContextCache && node._flowContextCache.text && (now - (node._flowContextCache.at || 0)) < ttlMs) {
|
|
4761
4911
|
flowContext = node._flowContextCache.text
|
|
4762
4912
|
} else {
|
|
4763
|
-
|
|
4764
|
-
const maxFlowNodes = compactMode
|
|
4765
|
-
? (configuredMaxFlowNodes > 0 ? Math.min(configuredMaxFlowNodes, 30) : 0)
|
|
4766
|
-
: configuredMaxFlowNodes
|
|
4767
|
-
flowContext = buildKnxUltimateFlowInventory({ maxNodes: maxFlowNodes })
|
|
4913
|
+
flowContext = buildKnxUltimateProjectInventory()
|
|
4768
4914
|
flowContext = truncatePromptText(flowContext, flowMaxChars)
|
|
4769
4915
|
node._flowContextCache = { at: now, text: flowContext }
|
|
4770
4916
|
}
|
|
@@ -4812,7 +4958,9 @@ module.exports = function (RED) {
|
|
|
4812
4958
|
wantsSvgChart ? '- Do not use JavaScript, external URLs, or <foreignObject>.' : '',
|
|
4813
4959
|
wantsSvgChart ? '- Prefer width via viewBox and include labels + legend when useful.' : '',
|
|
4814
4960
|
wantsSvgChart ? '' : '',
|
|
4815
|
-
|
|
4961
|
+
archiveScopeLine,
|
|
4962
|
+
'',
|
|
4963
|
+
'Selected KNX telegrams:',
|
|
4816
4964
|
recentLines.join('\n'),
|
|
4817
4965
|
'',
|
|
4818
4966
|
'User request:',
|
|
@@ -4859,6 +5007,172 @@ module.exports = function (RED) {
|
|
|
4859
5007
|
return path.join(baseDir, 'knxai', 'config', `knxai-config-${node.id}.json`)
|
|
4860
5008
|
}
|
|
4861
5009
|
|
|
5010
|
+
const getHistoryArchiveDir = () => {
|
|
5011
|
+
const baseDir = (node.serverKNX && node.serverKNX.userDir)
|
|
5012
|
+
? node.serverKNX.userDir
|
|
5013
|
+
: path.join(RED.settings.userDir, 'knxultimatestorage')
|
|
5014
|
+
return path.join(baseDir, 'knxai', 'history', node.id)
|
|
5015
|
+
}
|
|
5016
|
+
|
|
5017
|
+
const getHistoryArchiveFile = (dayKey) => path.join(getHistoryArchiveDir(), `${String(dayKey || '').trim() || formatArchiveDayKey(Date.now())}.jsonl`)
|
|
5018
|
+
|
|
5019
|
+
const pruneHistoryArchiveFiles = ({ force = false } = {}) => {
|
|
5020
|
+
if (node.historyStoreToDisk !== true) return
|
|
5021
|
+
const retentionDays = Math.max(1, Math.round(Number.isFinite(Number(node.historyStoreRetentionDays)) ? Number(node.historyStoreRetentionDays) : 1))
|
|
5022
|
+
const now = nowMs()
|
|
5023
|
+
if (!force && (now - Number(node._historyDiskLastPruneAt || 0)) < (60 * 60 * 1000)) return
|
|
5024
|
+
node._historyDiskLastPruneAt = now
|
|
5025
|
+
const dirPath = getHistoryArchiveDir()
|
|
5026
|
+
try {
|
|
5027
|
+
if (!fs.existsSync(dirPath)) return
|
|
5028
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
|
|
5029
|
+
const cutoffTs = now - ((retentionDays - 1) * 24 * 60 * 60 * 1000)
|
|
5030
|
+
const cutoffDayKey = formatArchiveDayKey(cutoffTs)
|
|
5031
|
+
for (let i = 0; i < entries.length; i++) {
|
|
5032
|
+
const entry = entries[i]
|
|
5033
|
+
if (!entry || !entry.isFile()) continue
|
|
5034
|
+
const match = String(entry.name || '').match(/^(\d{4}-\d{2}-\d{2})\.jsonl$/)
|
|
5035
|
+
if (!match) continue
|
|
5036
|
+
const dayKey = match[1]
|
|
5037
|
+
if (dayKey < cutoffDayKey) {
|
|
5038
|
+
try { fs.unlinkSync(path.join(dirPath, entry.name)) } catch (error) { /* ignore */ }
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
5041
|
+
} catch (error) {
|
|
5042
|
+
node.sysLogger?.warn(`KNX AI history prune error: ${error.message || error}`)
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
5045
|
+
|
|
5046
|
+
const persistTelegramToDisk = (telegram) => {
|
|
5047
|
+
if (node.historyStoreToDisk !== true || !telegram || typeof telegram !== 'object') return
|
|
5048
|
+
const archiveDir = getHistoryArchiveDir()
|
|
5049
|
+
if (!ensureDirectorySync(archiveDir)) return
|
|
5050
|
+
const dayKey = formatArchiveDayKey(telegram.ts || Date.now())
|
|
5051
|
+
const filePath = getHistoryArchiveFile(dayKey)
|
|
5052
|
+
const line = JSON.stringify(telegram) + '\n'
|
|
5053
|
+
fs.appendFile(filePath, line, 'utf8', (error) => {
|
|
5054
|
+
if (error) node.sysLogger?.warn(`KNX AI history append error: ${error.message || error}`)
|
|
5055
|
+
})
|
|
5056
|
+
pruneHistoryArchiveFiles()
|
|
5057
|
+
}
|
|
5058
|
+
|
|
5059
|
+
const loadRecentHistoryFromDisk = () => {
|
|
5060
|
+
if (node.historyStoreToDisk !== true) return
|
|
5061
|
+
const archiveDir = getHistoryArchiveDir()
|
|
5062
|
+
try {
|
|
5063
|
+
if (!fs.existsSync(archiveDir)) return
|
|
5064
|
+
const now = nowMs()
|
|
5065
|
+
const cutoffTs = now - (Math.max(5, Number(node.historyWindowSec || 5)) * 1000)
|
|
5066
|
+
const dayKeys = collectArchiveDayKeysBetween({ fromTs: cutoffTs, toTs: now })
|
|
5067
|
+
if (!dayKeys.length) return
|
|
5068
|
+
const restored = []
|
|
5069
|
+
for (let i = 0; i < dayKeys.length; i++) {
|
|
5070
|
+
const filePath = getHistoryArchiveFile(dayKeys[i])
|
|
5071
|
+
if (!fs.existsSync(filePath)) continue
|
|
5072
|
+
const raw = fs.readFileSync(filePath, 'utf8')
|
|
5073
|
+
if (!raw || String(raw).trim() === '') continue
|
|
5074
|
+
const lines = raw.split(/\r?\n/)
|
|
5075
|
+
for (let j = 0; j < lines.length; j++) {
|
|
5076
|
+
const line = lines[j]
|
|
5077
|
+
if (!line) continue
|
|
5078
|
+
try {
|
|
5079
|
+
const telegram = JSON.parse(line)
|
|
5080
|
+
const ts = Number(telegram && telegram.ts ? telegram.ts : 0)
|
|
5081
|
+
if (!Number.isFinite(ts) || ts < cutoffTs || ts > now) continue
|
|
5082
|
+
restored.push(telegram)
|
|
5083
|
+
} catch (error) {
|
|
5084
|
+
// Ignore malformed archive rows.
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
5087
|
+
}
|
|
5088
|
+
if (!restored.length) return
|
|
5089
|
+
restored.sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0))
|
|
5090
|
+
node._history = restored
|
|
5091
|
+
trimHistory(now)
|
|
5092
|
+
} catch (error) {
|
|
5093
|
+
node.sysLogger?.warn(`KNX AI history restore error: ${error.message || error}`)
|
|
5094
|
+
}
|
|
5095
|
+
}
|
|
5096
|
+
|
|
5097
|
+
const loadHistorySliceFromDisk = ({ fromTs, toTs, limit = 240 } = {}) => {
|
|
5098
|
+
if (node.historyStoreToDisk !== true) return []
|
|
5099
|
+
const archiveDir = getHistoryArchiveDir()
|
|
5100
|
+
try {
|
|
5101
|
+
if (!fs.existsSync(archiveDir)) return []
|
|
5102
|
+
const from = Number(fromTs || 0)
|
|
5103
|
+
const to = Number(toTs || 0)
|
|
5104
|
+
if (!Number.isFinite(from) || !Number.isFinite(to) || to < from) return []
|
|
5105
|
+
const dayKeys = collectArchiveDayKeysBetween({ fromTs: from, toTs: to })
|
|
5106
|
+
if (!dayKeys.length) return []
|
|
5107
|
+
const items = []
|
|
5108
|
+
for (let i = 0; i < dayKeys.length; i++) {
|
|
5109
|
+
const filePath = getHistoryArchiveFile(dayKeys[i])
|
|
5110
|
+
if (!fs.existsSync(filePath)) continue
|
|
5111
|
+
const raw = fs.readFileSync(filePath, 'utf8')
|
|
5112
|
+
if (!raw || String(raw).trim() === '') continue
|
|
5113
|
+
const lines = raw.split(/\r?\n/)
|
|
5114
|
+
for (let j = 0; j < lines.length; j++) {
|
|
5115
|
+
const line = lines[j]
|
|
5116
|
+
if (!line) continue
|
|
5117
|
+
try {
|
|
5118
|
+
const telegram = JSON.parse(line)
|
|
5119
|
+
const ts = Number(telegram && telegram.ts ? telegram.ts : 0)
|
|
5120
|
+
if (!Number.isFinite(ts) || ts < from || ts > to) continue
|
|
5121
|
+
items.push(telegram)
|
|
5122
|
+
} catch (error) {
|
|
5123
|
+
// Ignore malformed archive rows.
|
|
5124
|
+
}
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
if (!items.length) return []
|
|
5128
|
+
items.sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0))
|
|
5129
|
+
return items.slice(-Math.max(1, Number(limit || 1)))
|
|
5130
|
+
} catch (error) {
|
|
5131
|
+
node.sysLogger?.warn(`KNX AI history load slice error: ${error.message || error}`)
|
|
5132
|
+
return []
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
|
|
5136
|
+
const selectTelegramsForPrompt = ({ question, maxEvents }) => {
|
|
5137
|
+
const now = nowMs()
|
|
5138
|
+
const maxItems = Math.max(10, Number(maxEvents) || 120)
|
|
5139
|
+
const explicitRange = parseQuestionTimeRange(question, now)
|
|
5140
|
+
const fallbackRange = node.historyStoreToDisk === true
|
|
5141
|
+
? { fromTs: now - (24 * 60 * 60 * 1000), toTs: now, label: 'last 24 hours', explicit: false }
|
|
5142
|
+
: { fromTs: now - (Math.max(5, Number(node.historyWindowSec || 5)) * 1000), toTs: now, label: 'memory window', explicit: false }
|
|
5143
|
+
const range = explicitRange || fallbackRange
|
|
5144
|
+
|
|
5145
|
+
let selected = []
|
|
5146
|
+
let source = 'memory'
|
|
5147
|
+
if (node.historyStoreToDisk === true) {
|
|
5148
|
+
const diskItems = loadHistorySliceFromDisk({ fromTs: range.fromTs, toTs: range.toTs, limit: maxItems * 3 })
|
|
5149
|
+
const memoryItems = node._history.filter(t => Number(t && t.ts ? t.ts : 0) >= range.fromTs && Number(t && t.ts ? t.ts : 0) <= range.toTs)
|
|
5150
|
+
const dedupe = new Map()
|
|
5151
|
+
diskItems.concat(memoryItems).forEach((telegram) => {
|
|
5152
|
+
if (!telegram || typeof telegram !== 'object') return
|
|
5153
|
+
const key = [
|
|
5154
|
+
Number(telegram.ts || 0),
|
|
5155
|
+
String(telegram.event || ''),
|
|
5156
|
+
String(telegram.source || ''),
|
|
5157
|
+
String(telegram.destination || ''),
|
|
5158
|
+
normalizeValueForCompare(telegram.payload),
|
|
5159
|
+
String(telegram.rawHex || '')
|
|
5160
|
+
].join('|')
|
|
5161
|
+
dedupe.set(key, telegram)
|
|
5162
|
+
})
|
|
5163
|
+
selected = Array.from(dedupe.values()).sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0)).slice(-maxItems)
|
|
5164
|
+
source = 'archive+memory'
|
|
5165
|
+
} else {
|
|
5166
|
+
selected = node._history.slice(-maxItems)
|
|
5167
|
+
}
|
|
5168
|
+
|
|
5169
|
+
return {
|
|
5170
|
+
events: selected,
|
|
5171
|
+
source,
|
|
5172
|
+
range
|
|
5173
|
+
}
|
|
5174
|
+
}
|
|
5175
|
+
|
|
4862
5176
|
const loadPersistedAiConfig = () => {
|
|
4863
5177
|
if (node._persistedAiConfigCache && typeof node._persistedAiConfigCache === 'object') return node._persistedAiConfigCache
|
|
4864
5178
|
const configPath = getAiConfigStorageFile()
|
|
@@ -7068,6 +7382,7 @@ module.exports = function (RED) {
|
|
|
7068
7382
|
const telegram = extractTelegram(msg)
|
|
7069
7383
|
if (!telegram) return
|
|
7070
7384
|
node._history.push(telegram)
|
|
7385
|
+
persistTelegramToDisk(telegram)
|
|
7071
7386
|
resolveTelegramWaiters(telegram)
|
|
7072
7387
|
trackTransitionTelemetry(telegram)
|
|
7073
7388
|
const now = telegram.ts
|
|
@@ -7319,6 +7634,13 @@ module.exports = function (RED) {
|
|
|
7319
7634
|
}, Math.max(5, node.emitIntervalSec) * 1000)
|
|
7320
7635
|
}
|
|
7321
7636
|
|
|
7637
|
+
try {
|
|
7638
|
+
pruneHistoryArchiveFiles({ force: true })
|
|
7639
|
+
loadRecentHistoryFromDisk()
|
|
7640
|
+
} catch (error) {
|
|
7641
|
+
node.sysLogger?.warn(`KNX AI history startup error: ${error.message || error}`)
|
|
7642
|
+
}
|
|
7643
|
+
|
|
7322
7644
|
if (node._busConnectionWatchTimer) clearInterval(node._busConnectionWatchTimer)
|
|
7323
7645
|
node._busConnectionWatchTimer = setInterval(() => {
|
|
7324
7646
|
pollBusConnectionStatus()
|
|
@@ -31,6 +31,8 @@ Hier sind alle Felder aufgeführt, wie sie im KNX-AI-Editor sichtbar sind.
|
|
|
31
31
|
### Analysis
|
|
32
32
|
- **Analysis window (seconds)**: Hauptfenster für Summary/Rate-Berechnung.
|
|
33
33
|
- **History window (seconds)**: Aufbewahrungsfenster der internen Telegramm-Historie.
|
|
34
|
+
- **Captured telegrams also on disk archivieren**: Speichert Telegramme zusätzlich zu RAM in `knxultimatestorage/knxai/history/<node-id>/YYYY-MM-DD.jsonl`.
|
|
35
|
+
- **Aufbewahrung des Festplattenarchivs (Tage)**: Anzahl Tage, die Archivdateien auf Platte behalten werden, bevor sie automatisch gelöscht werden.
|
|
34
36
|
- **Max stored events**: Maximale Anzahl Telegramme im Speicher.
|
|
35
37
|
- **Auto emit summary (seconds, 0=off)**: Periodisches Summary-Intervall.
|
|
36
38
|
- **Top list size**: Anzahl Top-Gruppenadressen/Quellen in der Summary.
|
|
@@ -45,27 +47,32 @@ Hier sind alle Felder aufgeführt, wie sie im KNX-AI-Editor sichtbar sind.
|
|
|
45
47
|
- **Flap window (seconds)**: Zeitfenster für Flapping-/Wechselraten-Erkennung.
|
|
46
48
|
- **Max changes per GA in window (0=off)**: Maximal erlaubte Änderungen im Fenster.
|
|
47
49
|
|
|
48
|
-
###
|
|
49
|
-
- Der Tab **LLM Assistant** steht im Editor jetzt an erster Stelle für eine schnellere Einrichtung.
|
|
50
|
+
### KI-Assistent
|
|
50
51
|
- **Enable LLM assistant**: Aktiviert Ask/Chat-Funktionen.
|
|
51
52
|
- **Provider**: LLM-Backend (OpenAI-compatible oder Ollama).
|
|
52
53
|
- **Endpoint URL**: URL des Chat/Completions-Endpunkts.
|
|
53
54
|
- **API key**: API-Schlüssel (für lokales Ollama nicht erforderlich).
|
|
54
55
|
- **Model**: Modell-ID/Name.
|
|
55
|
-
- **System prompt**: Globale Instruktion für KNX-Analyse.
|
|
56
|
-
- **
|
|
57
|
-
- **Max tokens**: Maximalzahl Completion-Tokens.
|
|
58
|
-
- **Timeout (ms)**: HTTP-Timeout für LLM-Anfragen.
|
|
59
|
-
- **Recent events included**: Max. Anzahl aktueller Events im Prompt.
|
|
56
|
+
- **System prompt**: Globale Instruktion für KNX-Analyse (Advanced).
|
|
57
|
+
- Wenn das Festplattenarchiv aktiv ist, nutzt **Ask** standardmäßig dieses Archiv: explizite Datumsangaben/Zeitbereiche werden beachtet, sonst durchsucht der Assistent die letzten 24 Stunden plus aktuelle RAM-Events.
|
|
60
58
|
- **Include raw payload hex**: Rohe Hex-Payload im Prompt einfügen.
|
|
61
|
-
- **
|
|
62
|
-
- **Max flow nodes included**: Limit der inkludierten Flow-Nodes.
|
|
59
|
+
- **Node-RED-Projektinventar einbeziehen**: Nimmt das gesamte Node-RED-Projektinventar in den Prompt auf, einschließlich KNX-Nodes und anderer hilfreicher Nodes wie function/change/inject/template, wenn sie KNX-Logik oder Gruppenadressen enthalten.
|
|
63
60
|
- **Include documentation snippets (help/README/examples)**: Doku-Kontext einfügen.
|
|
64
61
|
- **Docs language**: Bevorzugte Sprache der Doku-Snippets.
|
|
65
|
-
- **Max docs snippets**: Max. Anzahl Doku-Snippets.
|
|
66
|
-
- **Max docs chars**: Max. Gesamtzeichen aus Doku.
|
|
67
62
|
- Button **Refresh**: Provider abfragen und verfügbare Modelle laden.
|
|
68
63
|
|
|
64
|
+
### Advanced
|
|
65
|
+
- **Analysis window (seconds)**: Hauptfenster für Summary/Rate-Berechnung.
|
|
66
|
+
- **Max stored events**: Maximale Anzahl Telegramme im Speicher.
|
|
67
|
+
- **Top list size**: Anzahl Top-Gruppenadressen/Quellen in der Summary.
|
|
68
|
+
- **Pattern max lag (ms)**: Maximaler Zeitabstand für Pattern-Korrelation.
|
|
69
|
+
- **Pattern min occurrences**: Mindestanzahl, bevor ein Pattern gemeldet wird.
|
|
70
|
+
- **Rate window (seconds)**: Gleitendes Zeitfenster für Rate-Prüfungen.
|
|
71
|
+
- **Max overall telegrams/sec (0=off)**: Schwellwert für gesamten Bus.
|
|
72
|
+
- **Max telegrams/sec per GA (0=off)**: Schwellwert pro Gruppenadresse.
|
|
73
|
+
- **Flap window (seconds)**: Zeitfenster für Flapping-/Wechselraten-Erkennung.
|
|
74
|
+
- **Max changes per GA in window (0=off)**: Maximal erlaubte Änderungen im Fenster.
|
|
75
|
+
|
|
69
76
|
### Ollama Schnellstart (lokal)
|
|
70
77
|
- **Provider = Ollama** auswählen.
|
|
71
78
|
- Standard-Endpoint: `http://localhost:11434/api/chat`.
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
"title": "KNX AI (Traffic Analyzer)",
|
|
4
4
|
"sections": {
|
|
5
5
|
"capture": "Capture",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
6
|
+
"storage": "Speicher & Zusammenfassung",
|
|
7
|
+
"detection": "Erkennung & Warnungen",
|
|
8
|
+
"llmConnection": "KI-Assistent-Verbindung",
|
|
9
|
+
"llmContext": "KI-Assistent-Kontext",
|
|
10
|
+
"advanced": "Erweiterte Einstellungen"
|
|
9
11
|
},
|
|
10
12
|
"properties": {
|
|
11
13
|
"server": "Gateway",
|
|
@@ -16,6 +18,8 @@
|
|
|
16
18
|
"notifyreadrequest": "Capture GroupValue_Read",
|
|
17
19
|
"analysisWindowSec": "Analysis window (seconds)",
|
|
18
20
|
"historyWindowSec": "History window (seconds)",
|
|
21
|
+
"historyStoreToDisk": "Captured telegrams also on disk archivieren",
|
|
22
|
+
"historyStoreRetentionDays": "Aufbewahrung des Festplattenarchivs (Tage)",
|
|
19
23
|
"maxEvents": "Max stored events",
|
|
20
24
|
"emitIntervalSec": "Auto emit summary (seconds, 0=off)",
|
|
21
25
|
"topN": "Top list size",
|
|
@@ -33,17 +37,10 @@
|
|
|
33
37
|
"llmApiKey": "API key",
|
|
34
38
|
"llmModel": "Model",
|
|
35
39
|
"llmSystemPrompt": "System prompt",
|
|
36
|
-
"llmTemperature": "Temperature",
|
|
37
|
-
"llmMaxTokens": "Max tokens",
|
|
38
|
-
"llmTimeoutMs": "Timeout (ms)",
|
|
39
|
-
"llmMaxEventsInPrompt": "Recent events included",
|
|
40
40
|
"llmIncludeRaw": "Include raw payload hex",
|
|
41
|
-
"llmIncludeFlowContext": "
|
|
42
|
-
"llmMaxFlowNodesInPrompt": "Max flow nodes included",
|
|
41
|
+
"llmIncludeFlowContext": "Node-RED-Projektinventar einbeziehen",
|
|
43
42
|
"llmIncludeDocsSnippets": "Include documentation snippets (help/README/examples)",
|
|
44
|
-
"llmDocsLanguage": "Docs language"
|
|
45
|
-
"llmDocsMaxSnippets": "Max docs snippets",
|
|
46
|
-
"llmDocsMaxChars": "Max docs chars"
|
|
43
|
+
"llmDocsLanguage": "Docs language"
|
|
47
44
|
},
|
|
48
45
|
"outputs": {
|
|
49
46
|
"summary": "Zusammenfassung/Statistik",
|
|
@@ -31,6 +31,8 @@ All fields exposed in the KNX AI editor are listed below.
|
|
|
31
31
|
### Analysis
|
|
32
32
|
- **Analysis window (seconds)**: Main analysis window used for summaries/rates.
|
|
33
33
|
- **History window (seconds)**: Retention window for internal telegram history.
|
|
34
|
+
- **Also archive captured telegrams to disk**: Stores captured telegrams in `knxultimatestorage/knxai/history/<node-id>/YYYY-MM-DD.jsonl` in addition to RAM.
|
|
35
|
+
- **Disk archive retention (days)**: Number of days kept on disk before old archive files are deleted automatically.
|
|
34
36
|
- **Max stored events**: Maximum number of telegrams kept in memory.
|
|
35
37
|
- **Auto emit summary (seconds, 0=off)**: Periodic summary output interval.
|
|
36
38
|
- **Top list size**: Number of top group addresses/sources in summary.
|
|
@@ -45,27 +47,32 @@ All fields exposed in the KNX AI editor are listed below.
|
|
|
45
47
|
- **Flap window (seconds)**: Time window for flapping/change-rate detection.
|
|
46
48
|
- **Max changes per GA in window (0=off)**: Max allowed changes in flap window.
|
|
47
49
|
|
|
48
|
-
###
|
|
49
|
-
- The **LLM Assistant** tab is shown first in the editor for faster setup.
|
|
50
|
+
### AI Assistant
|
|
50
51
|
- **Enable LLM assistant**: Enable Ask/chat assistant features.
|
|
51
52
|
- **Provider**: Select LLM backend (OpenAI-compatible or Ollama).
|
|
52
53
|
- **Endpoint URL**: Chat/completions endpoint URL.
|
|
53
54
|
- **API key**: API key (not required for local Ollama).
|
|
54
55
|
- **Model**: Model ID/name.
|
|
55
|
-
- **System prompt**: Global instruction for KNX analysis behavior.
|
|
56
|
-
- **
|
|
57
|
-
- **Max tokens**: Max completion tokens.
|
|
58
|
-
- **Timeout (ms)**: HTTP timeout for LLM requests.
|
|
59
|
-
- **Recent events included**: Max recent telegram events in prompt.
|
|
56
|
+
- **System prompt**: Global instruction for KNX analysis behavior (Advanced).
|
|
57
|
+
- If disk archive is enabled, **Ask** uses the archive by default: explicit dates/ranges are honored, otherwise the assistant searches the last 24 hours plus current RAM events.
|
|
60
58
|
- **Include raw payload hex**: Include raw telegram hex in prompt.
|
|
61
|
-
- **Include Node-RED
|
|
62
|
-
- **Max flow nodes included**: Limit nodes included from flow inventory.
|
|
59
|
+
- **Include Node-RED project inventory**: Include the whole Node-RED project inventory in the prompt, including KNX nodes and other useful nodes such as function/change/inject/template when they contain KNX-related logic or group addresses.
|
|
63
60
|
- **Include documentation snippets (help/README/examples)**: Include docs context.
|
|
64
61
|
- **Docs language**: Preferred language for docs snippets.
|
|
65
|
-
- **Max docs snippets**: Max number of docs snippets.
|
|
66
|
-
- **Max docs chars**: Max total docs characters.
|
|
67
62
|
- **Refresh** button: Query provider and load available model IDs.
|
|
68
63
|
|
|
64
|
+
### Advanced
|
|
65
|
+
- **Analysis window (seconds)**: Main analysis window used for summaries/rates.
|
|
66
|
+
- **Max stored events**: Maximum number of telegrams kept in memory.
|
|
67
|
+
- **Top list size**: Number of top group addresses/sources in summary.
|
|
68
|
+
- **Pattern max lag (ms)**: Max time gap for pattern transition matching.
|
|
69
|
+
- **Pattern min occurrences**: Minimum occurrences before a pattern is reported.
|
|
70
|
+
- **Rate window (seconds)**: Sliding time window for anomaly rate checks.
|
|
71
|
+
- **Max overall telegrams/sec (0=off)**: Overall bus rate threshold.
|
|
72
|
+
- **Max telegrams/sec per GA (0=off)**: Per-group-address rate threshold.
|
|
73
|
+
- **Flap window (seconds)**: Time window for flapping/change-rate detection.
|
|
74
|
+
- **Max changes per GA in window (0=off)**: Max allowed changes in flap window.
|
|
75
|
+
|
|
69
76
|
### Ollama quick setup (local)
|
|
70
77
|
- Choose **Provider = Ollama**.
|
|
71
78
|
- Default endpoint: `http://localhost:11434/api/chat`.
|