node-red-contrib-knx-ultimate 4.1.25 → 4.1.27
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 +8 -0
- package/nodes/knxUltimateAI.html +5 -3
- package/nodes/locales/de/knxUltimateAI.json +50 -3
- package/nodes/locales/en/knxUltimateAI.json +47 -0
- package/nodes/locales/es/knxUltimateAI.json +50 -3
- package/nodes/locales/fr/knxUltimateAI.json +49 -2
- package/nodes/locales/it/knxUltimateAI.json +47 -0
- package/nodes/locales/zh-CN/knxUltimateAI.json +50 -3
- package/nodes/plugins/knxUltimateAI-sidebar-plugin.html +59 -34
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
+
**Version 4.1.27** - February 2026<br/>
|
|
10
|
+
|
|
11
|
+
- Bumped KNX Engine to 5.2.8<br/>
|
|
12
|
+
|
|
13
|
+
**Version 4.1.26** - February 2026<br/>
|
|
14
|
+
|
|
15
|
+
- i18n: **KNX AI** sidebar tab: localized the Summary and UI strings in all supported languages (including output pin labels).<br/>
|
|
16
|
+
|
|
9
17
|
**Version 4.1.25** - February 2026<br/>
|
|
10
18
|
|
|
11
19
|
- FIX: **KNX Device**: periodic send (cyclic write) now also works when the value is restored from the persisted GA cache after a Node-RED restart.<br/>
|
package/nodes/knxUltimateAI.html
CHANGED
|
@@ -172,9 +172,11 @@
|
|
|
172
172
|
</script>
|
|
173
173
|
|
|
174
174
|
<script type="text/html" data-template-name="knxUltimateAI">
|
|
175
|
+
<b><span data-i18n="knxUltimateAI.title"></span></b>  <span style="color:red"     <i class="fa fa-youtube"></i></span><a
|
|
176
|
+
target="_blank" href="https://www.youtube.com/watch?v=qw7kjQ_mvdg&t=10s">See sample video</a>
|
|
177
|
+
<br /><br />
|
|
175
178
|
<div class="form-row">
|
|
176
|
-
|
|
177
|
-
<br/><br/>
|
|
179
|
+
|
|
178
180
|
<label for="node-input-server"><i class="fa fa-tag"></i> <span data-i18n="knxUltimateAI.properties.server"></span></label>
|
|
179
181
|
<input type="text" id="node-input-server">
|
|
180
182
|
</div>
|
|
@@ -374,4 +376,4 @@
|
|
|
374
376
|
</div>
|
|
375
377
|
</div>
|
|
376
378
|
</div>
|
|
377
|
-
</script>
|
|
379
|
+
</script>
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
"llmDocsMaxChars": "Max docs chars"
|
|
47
47
|
},
|
|
48
48
|
"outputs": {
|
|
49
|
-
"summary": "
|
|
50
|
-
"anomalies": "
|
|
51
|
-
"assistant": "
|
|
49
|
+
"summary": "Zusammenfassung/Statistik",
|
|
50
|
+
"anomalies": "Anomalien",
|
|
51
|
+
"assistant": "KI-Assistent"
|
|
52
52
|
},
|
|
53
53
|
"selectlists": {
|
|
54
54
|
"llmProvider": {
|
|
@@ -64,6 +64,53 @@
|
|
|
64
64
|
},
|
|
65
65
|
"messages": {
|
|
66
66
|
"ollamaNotSupported": "Ollama integration is marked as not yet supported (testing in progress)."
|
|
67
|
+
},
|
|
68
|
+
"sidebar": {
|
|
69
|
+
"ui": {
|
|
70
|
+
"refreshNodeList": "Knotenliste aktualisieren",
|
|
71
|
+
"refreshSummary": "Zusammenfassung aktualisieren",
|
|
72
|
+
"auto": "Auto",
|
|
73
|
+
"sections": {
|
|
74
|
+
"summary": "Zusammenfassung",
|
|
75
|
+
"anomalies": "Anomalien",
|
|
76
|
+
"ask": "Fragen"
|
|
77
|
+
},
|
|
78
|
+
"empty": {
|
|
79
|
+
"noNodes": "Keine KNX AI-Knoten gefunden.",
|
|
80
|
+
"noAnomalies": "Keine Anomalien."
|
|
81
|
+
},
|
|
82
|
+
"chat": {
|
|
83
|
+
"placeholder": "Stelle eine Frage zum KNX-Verkehr…",
|
|
84
|
+
"send": "Senden",
|
|
85
|
+
"pending": "Ich denke nach…",
|
|
86
|
+
"llmDisabled": "LLM in der Knoten-Konfiguration deaktiviert",
|
|
87
|
+
"emptyAnswer": "(leere Antwort)"
|
|
88
|
+
},
|
|
89
|
+
"status": {
|
|
90
|
+
"ready": "Bereit",
|
|
91
|
+
"loadingNodes": "Lade Knoten…",
|
|
92
|
+
"loading": "Laden…",
|
|
93
|
+
"asking": "Frage…"
|
|
94
|
+
},
|
|
95
|
+
"errors": {
|
|
96
|
+
"loadNodes": "Knoten konnten nicht geladen werden",
|
|
97
|
+
"loadState": "Status konnte nicht geladen werden",
|
|
98
|
+
"askFailed": "Anfrage fehlgeschlagen"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"summary": {
|
|
102
|
+
"noData": "Keine Daten verfügbar.",
|
|
103
|
+
"header": {
|
|
104
|
+
"gateway": "Gateway: {{gatewayName}}",
|
|
105
|
+
"updated": "Aktualisiert: {{at}}"
|
|
106
|
+
},
|
|
107
|
+
"analysisWindowLine": "Analysefenster: {{seconds}}s",
|
|
108
|
+
"statsLine": "Telegramme: {{telegrams}} · Rate: {{rate}}/s · Echoed: {{echoed}} · Unbekannte DPT: {{unknownDpt}}",
|
|
109
|
+
"topGAsTitle": "Top-Gruppenadressen:",
|
|
110
|
+
"eventsTitle": "Ereignisse:",
|
|
111
|
+
"patternsTitle": "Muster (wiederkehrende Sequenzen):",
|
|
112
|
+
"patternItem": "{{from}} → {{to}} ({{count}} mal innerhalb von {{withinMs}}ms)"
|
|
113
|
+
}
|
|
67
114
|
}
|
|
68
115
|
}
|
|
69
116
|
}
|
|
@@ -69,6 +69,53 @@
|
|
|
69
69
|
"llmApiKey": "Paste API key (starts with sk-)",
|
|
70
70
|
"llmModel": "e.g. gpt-4o-mini",
|
|
71
71
|
"llmSystemPrompt": "Optional. Leave empty for default."
|
|
72
|
+
},
|
|
73
|
+
"sidebar": {
|
|
74
|
+
"ui": {
|
|
75
|
+
"refreshNodeList": "Refresh Node List",
|
|
76
|
+
"refreshSummary": "Refresh Summary",
|
|
77
|
+
"auto": "Auto",
|
|
78
|
+
"sections": {
|
|
79
|
+
"summary": "Summary",
|
|
80
|
+
"anomalies": "Anomalies",
|
|
81
|
+
"ask": "Ask"
|
|
82
|
+
},
|
|
83
|
+
"empty": {
|
|
84
|
+
"noNodes": "No KNX AI nodes found.",
|
|
85
|
+
"noAnomalies": "No anomalies."
|
|
86
|
+
},
|
|
87
|
+
"chat": {
|
|
88
|
+
"placeholder": "Ask a question about KNX traffic…",
|
|
89
|
+
"send": "Send",
|
|
90
|
+
"pending": "Thinking…",
|
|
91
|
+
"llmDisabled": "LLM disabled in node config",
|
|
92
|
+
"emptyAnswer": "(empty answer)"
|
|
93
|
+
},
|
|
94
|
+
"status": {
|
|
95
|
+
"ready": "Ready",
|
|
96
|
+
"loadingNodes": "Loading nodes…",
|
|
97
|
+
"loading": "Loading…",
|
|
98
|
+
"asking": "Asking…"
|
|
99
|
+
},
|
|
100
|
+
"errors": {
|
|
101
|
+
"loadNodes": "Failed to load nodes",
|
|
102
|
+
"loadState": "Failed to load state",
|
|
103
|
+
"askFailed": "Ask failed"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"summary": {
|
|
107
|
+
"noData": "No data available.",
|
|
108
|
+
"header": {
|
|
109
|
+
"gateway": "Gateway: {{gatewayName}}",
|
|
110
|
+
"updated": "Updated: {{at}}"
|
|
111
|
+
},
|
|
112
|
+
"analysisWindowLine": "Analysis window: {{seconds}}s",
|
|
113
|
+
"statsLine": "Telegrams: {{telegrams}} · Rate: {{rate}}/s · Echoed: {{echoed}} · Unknown DPT: {{unknownDpt}}",
|
|
114
|
+
"topGAsTitle": "Top Group Address:",
|
|
115
|
+
"eventsTitle": "Events:",
|
|
116
|
+
"patternsTitle": "Patterns (recurring sequences):",
|
|
117
|
+
"patternItem": "{{from}} → {{to}} ({{count}} times within {{withinMs}}ms)"
|
|
118
|
+
}
|
|
72
119
|
}
|
|
73
120
|
}
|
|
74
121
|
}
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
"llmDocsMaxChars": "Max docs chars"
|
|
47
47
|
},
|
|
48
48
|
"outputs": {
|
|
49
|
-
"summary": "
|
|
50
|
-
"anomalies": "
|
|
51
|
-
"assistant": "
|
|
49
|
+
"summary": "Resumen/Estadísticas",
|
|
50
|
+
"anomalies": "Anomalías",
|
|
51
|
+
"assistant": "Asistente IA"
|
|
52
52
|
},
|
|
53
53
|
"selectlists": {
|
|
54
54
|
"llmProvider": {
|
|
@@ -64,6 +64,53 @@
|
|
|
64
64
|
"llmApiKey": "Paste API key (starts with sk-)",
|
|
65
65
|
"llmModel": "e.g. gpt-4o-mini",
|
|
66
66
|
"llmSystemPrompt": "Optional. Leave empty for default."
|
|
67
|
+
},
|
|
68
|
+
"sidebar": {
|
|
69
|
+
"ui": {
|
|
70
|
+
"refreshNodeList": "Actualizar lista de nodos",
|
|
71
|
+
"refreshSummary": "Actualizar resumen",
|
|
72
|
+
"auto": "Auto",
|
|
73
|
+
"sections": {
|
|
74
|
+
"summary": "Resumen",
|
|
75
|
+
"anomalies": "Anomalías",
|
|
76
|
+
"ask": "Preguntar"
|
|
77
|
+
},
|
|
78
|
+
"empty": {
|
|
79
|
+
"noNodes": "No se encontraron nodos KNX AI.",
|
|
80
|
+
"noAnomalies": "Sin anomalías."
|
|
81
|
+
},
|
|
82
|
+
"chat": {
|
|
83
|
+
"placeholder": "Haz una pregunta sobre el tráfico KNX…",
|
|
84
|
+
"send": "Enviar",
|
|
85
|
+
"pending": "Pensando…",
|
|
86
|
+
"llmDisabled": "LLM deshabilitado en la configuración del nodo",
|
|
87
|
+
"emptyAnswer": "(respuesta vacía)"
|
|
88
|
+
},
|
|
89
|
+
"status": {
|
|
90
|
+
"ready": "Listo",
|
|
91
|
+
"loadingNodes": "Cargando nodos…",
|
|
92
|
+
"loading": "Cargando…",
|
|
93
|
+
"asking": "Preguntando…"
|
|
94
|
+
},
|
|
95
|
+
"errors": {
|
|
96
|
+
"loadNodes": "Error al cargar los nodos",
|
|
97
|
+
"loadState": "Error al cargar el estado",
|
|
98
|
+
"askFailed": "Falló la pregunta"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"summary": {
|
|
102
|
+
"noData": "No hay datos disponibles.",
|
|
103
|
+
"header": {
|
|
104
|
+
"gateway": "Gateway: {{gatewayName}}",
|
|
105
|
+
"updated": "Actualizado: {{at}}"
|
|
106
|
+
},
|
|
107
|
+
"analysisWindowLine": "Ventana de análisis: {{seconds}}s",
|
|
108
|
+
"statsLine": "Telegramas: {{telegrams}} · Tasa: {{rate}}/s · Eco: {{echoed}} · DPT desconocidos: {{unknownDpt}}",
|
|
109
|
+
"topGAsTitle": "Principales direcciones de grupo:",
|
|
110
|
+
"eventsTitle": "Eventos:",
|
|
111
|
+
"patternsTitle": "Patrones (secuencias recurrentes):",
|
|
112
|
+
"patternItem": "{{from}} → {{to}} ({{count}} veces en {{withinMs}}ms)"
|
|
113
|
+
}
|
|
67
114
|
}
|
|
68
115
|
}
|
|
69
116
|
}
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
"llmDocsMaxChars": "Max docs chars"
|
|
47
47
|
},
|
|
48
48
|
"outputs": {
|
|
49
|
-
"summary": "
|
|
49
|
+
"summary": "Résumé/Stats",
|
|
50
50
|
"anomalies": "Anomalies",
|
|
51
|
-
"assistant": "
|
|
51
|
+
"assistant": "Assistant IA"
|
|
52
52
|
},
|
|
53
53
|
"selectlists": {
|
|
54
54
|
"llmProvider": {
|
|
@@ -64,6 +64,53 @@
|
|
|
64
64
|
"llmApiKey": "Paste API key (starts with sk-)",
|
|
65
65
|
"llmModel": "e.g. gpt-4o-mini",
|
|
66
66
|
"llmSystemPrompt": "Optional. Leave empty for default."
|
|
67
|
+
},
|
|
68
|
+
"sidebar": {
|
|
69
|
+
"ui": {
|
|
70
|
+
"refreshNodeList": "Rafraîchir la liste des nœuds",
|
|
71
|
+
"refreshSummary": "Rafraîchir le résumé",
|
|
72
|
+
"auto": "Auto",
|
|
73
|
+
"sections": {
|
|
74
|
+
"summary": "Résumé",
|
|
75
|
+
"anomalies": "Anomalies",
|
|
76
|
+
"ask": "Demander"
|
|
77
|
+
},
|
|
78
|
+
"empty": {
|
|
79
|
+
"noNodes": "Aucun nœud KNX AI trouvé.",
|
|
80
|
+
"noAnomalies": "Aucune anomalie."
|
|
81
|
+
},
|
|
82
|
+
"chat": {
|
|
83
|
+
"placeholder": "Posez une question sur le trafic KNX…",
|
|
84
|
+
"send": "Envoyer",
|
|
85
|
+
"pending": "Je réfléchis…",
|
|
86
|
+
"llmDisabled": "LLM désactivé dans la configuration du nœud",
|
|
87
|
+
"emptyAnswer": "(réponse vide)"
|
|
88
|
+
},
|
|
89
|
+
"status": {
|
|
90
|
+
"ready": "Prêt",
|
|
91
|
+
"loadingNodes": "Chargement des nœuds…",
|
|
92
|
+
"loading": "Chargement…",
|
|
93
|
+
"asking": "Question…"
|
|
94
|
+
},
|
|
95
|
+
"errors": {
|
|
96
|
+
"loadNodes": "Impossible de charger les nœuds",
|
|
97
|
+
"loadState": "Impossible de charger l’état",
|
|
98
|
+
"askFailed": "Échec de la question"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"summary": {
|
|
102
|
+
"noData": "Aucune donnée disponible.",
|
|
103
|
+
"header": {
|
|
104
|
+
"gateway": "Passerelle : {{gatewayName}}",
|
|
105
|
+
"updated": "Mis à jour : {{at}}"
|
|
106
|
+
},
|
|
107
|
+
"analysisWindowLine": "Fenêtre d’analyse : {{seconds}} s",
|
|
108
|
+
"statsLine": "Télégrammes : {{telegrams}} · Taux : {{rate}}/s · Écho : {{echoed}} · DPT inconnus : {{unknownDpt}}",
|
|
109
|
+
"topGAsTitle": "Principales adresses de groupe :",
|
|
110
|
+
"eventsTitle": "Événements :",
|
|
111
|
+
"patternsTitle": "Motifs (séquences récurrentes) :",
|
|
112
|
+
"patternItem": "{{from}} → {{to}} ({{count}} fois en {{withinMs}}ms)"
|
|
113
|
+
}
|
|
67
114
|
}
|
|
68
115
|
}
|
|
69
116
|
}
|
|
@@ -69,6 +69,53 @@
|
|
|
69
69
|
"llmApiKey": "Incolla la chiave (inizia con sk-)",
|
|
70
70
|
"llmModel": "es. gpt-4o-mini",
|
|
71
71
|
"llmSystemPrompt": "Opzionale. Lascia vuoto per default."
|
|
72
|
+
},
|
|
73
|
+
"sidebar": {
|
|
74
|
+
"ui": {
|
|
75
|
+
"refreshNodeList": "Aggiorna lista nodi",
|
|
76
|
+
"refreshSummary": "Aggiorna summary",
|
|
77
|
+
"auto": "Auto",
|
|
78
|
+
"sections": {
|
|
79
|
+
"summary": "Summary",
|
|
80
|
+
"anomalies": "Anomalie",
|
|
81
|
+
"ask": "Chiedi"
|
|
82
|
+
},
|
|
83
|
+
"empty": {
|
|
84
|
+
"noNodes": "Nessun nodo KNX AI trovato.",
|
|
85
|
+
"noAnomalies": "Nessuna anomalia."
|
|
86
|
+
},
|
|
87
|
+
"chat": {
|
|
88
|
+
"placeholder": "Fai una domanda sul traffico KNX…",
|
|
89
|
+
"send": "Invia",
|
|
90
|
+
"pending": "Sto pensando…",
|
|
91
|
+
"llmDisabled": "LLM disabilitato nella configurazione del nodo",
|
|
92
|
+
"emptyAnswer": "(risposta vuota)"
|
|
93
|
+
},
|
|
94
|
+
"status": {
|
|
95
|
+
"ready": "Pronto",
|
|
96
|
+
"loadingNodes": "Carico nodi…",
|
|
97
|
+
"loading": "Carico…",
|
|
98
|
+
"asking": "Sto chiedendo…"
|
|
99
|
+
},
|
|
100
|
+
"errors": {
|
|
101
|
+
"loadNodes": "Impossibile caricare i nodi",
|
|
102
|
+
"loadState": "Impossibile caricare lo stato",
|
|
103
|
+
"askFailed": "Richiesta fallita"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"summary": {
|
|
107
|
+
"noData": "Nessun dato disponibile.",
|
|
108
|
+
"header": {
|
|
109
|
+
"gateway": "Gateway: {{gatewayName}}",
|
|
110
|
+
"updated": "Aggiornato: {{at}}"
|
|
111
|
+
},
|
|
112
|
+
"analysisWindowLine": "Finestra analisi: {{seconds}}s",
|
|
113
|
+
"statsLine": "Telegrammi: {{telegrams}} · Rate: {{rate}}/s · Echoed: {{echoed}} · DPT sconosciuti: {{unknownDpt}}",
|
|
114
|
+
"topGAsTitle": "Top Group Address:",
|
|
115
|
+
"eventsTitle": "Eventi:",
|
|
116
|
+
"patternsTitle": "Pattern (sequenze ricorrenti):",
|
|
117
|
+
"patternItem": "{{from}} → {{to}} ({{count}} volte entro {{withinMs}}ms)"
|
|
118
|
+
}
|
|
72
119
|
}
|
|
73
120
|
}
|
|
74
121
|
}
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
"llmDocsMaxChars": "Max docs chars"
|
|
47
47
|
},
|
|
48
48
|
"outputs": {
|
|
49
|
-
"summary": "
|
|
50
|
-
"anomalies": "
|
|
51
|
-
"assistant": "AI
|
|
49
|
+
"summary": "摘要/统计",
|
|
50
|
+
"anomalies": "异常",
|
|
51
|
+
"assistant": "AI 助手"
|
|
52
52
|
},
|
|
53
53
|
"selectlists": {
|
|
54
54
|
"llmProvider": {
|
|
@@ -64,6 +64,53 @@
|
|
|
64
64
|
"llmApiKey": "Paste API key (starts with sk-)",
|
|
65
65
|
"llmModel": "e.g. gpt-4o-mini",
|
|
66
66
|
"llmSystemPrompt": "Optional. Leave empty for default."
|
|
67
|
+
},
|
|
68
|
+
"sidebar": {
|
|
69
|
+
"ui": {
|
|
70
|
+
"refreshNodeList": "刷新节点列表",
|
|
71
|
+
"refreshSummary": "刷新摘要",
|
|
72
|
+
"auto": "自动",
|
|
73
|
+
"sections": {
|
|
74
|
+
"summary": "摘要",
|
|
75
|
+
"anomalies": "异常",
|
|
76
|
+
"ask": "提问"
|
|
77
|
+
},
|
|
78
|
+
"empty": {
|
|
79
|
+
"noNodes": "未找到 KNX AI 节点。",
|
|
80
|
+
"noAnomalies": "无异常。"
|
|
81
|
+
},
|
|
82
|
+
"chat": {
|
|
83
|
+
"placeholder": "询问有关 KNX 流量的问题…",
|
|
84
|
+
"send": "发送",
|
|
85
|
+
"pending": "思考中…",
|
|
86
|
+
"llmDisabled": "节点配置中已禁用 LLM",
|
|
87
|
+
"emptyAnswer": "(空回复)"
|
|
88
|
+
},
|
|
89
|
+
"status": {
|
|
90
|
+
"ready": "就绪",
|
|
91
|
+
"loadingNodes": "正在加载节点…",
|
|
92
|
+
"loading": "正在加载…",
|
|
93
|
+
"asking": "正在提问…"
|
|
94
|
+
},
|
|
95
|
+
"errors": {
|
|
96
|
+
"loadNodes": "加载节点失败",
|
|
97
|
+
"loadState": "加载状态失败",
|
|
98
|
+
"askFailed": "提问失败"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"summary": {
|
|
102
|
+
"noData": "暂无数据。",
|
|
103
|
+
"header": {
|
|
104
|
+
"gateway": "网关:{{gatewayName}}",
|
|
105
|
+
"updated": "更新于:{{at}}"
|
|
106
|
+
},
|
|
107
|
+
"analysisWindowLine": "分析窗口:{{seconds}} 秒",
|
|
108
|
+
"statsLine": "电报:{{telegrams}} · 速率:{{rate}}/秒 · 回显:{{echoed}} · 未知 DPT:{{unknownDpt}}",
|
|
109
|
+
"topGAsTitle": "最常见组地址:",
|
|
110
|
+
"eventsTitle": "事件:",
|
|
111
|
+
"patternsTitle": "模式(重复序列):",
|
|
112
|
+
"patternItem": "{{from}} → {{to}}({{count}} 次,{{withinMs}}ms 内)"
|
|
113
|
+
}
|
|
67
114
|
}
|
|
68
115
|
}
|
|
69
116
|
}
|
|
@@ -53,36 +53,54 @@
|
|
|
53
53
|
RED.sidebar.removeTab(tabId);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
const tr = (suffix, opts, fallback) => {
|
|
57
|
+
const key = 'knxUltimateAI.sidebar.' + suffix;
|
|
58
|
+
let val = '';
|
|
59
|
+
try { val = RED._(key, opts); } catch (e) { val = ''; }
|
|
60
|
+
if (!val || val === key) val = (fallback !== undefined ? fallback : suffix);
|
|
61
|
+
return val;
|
|
62
|
+
};
|
|
63
|
+
|
|
56
64
|
const container = $('<div>').css({ display: 'flex', flexDirection: 'column', height: '100%' });
|
|
57
65
|
const toolbar = $('<div>').attr('id', 'knx-ai-toolbar').appendTo(container);
|
|
58
66
|
const nodeSelect = $('<select>').attr('id', 'knx-ai-node-select').appendTo(toolbar);
|
|
59
|
-
const refreshNodesBtn = $('<button type="button" class="red-ui-button"
|
|
60
|
-
|
|
67
|
+
const refreshNodesBtn = $('<button type="button" class="red-ui-button"></button>').appendTo(toolbar);
|
|
68
|
+
$('<i class="fa fa-refresh"></i>').appendTo(refreshNodesBtn);
|
|
69
|
+
refreshNodesBtn.append(' ');
|
|
70
|
+
$('<span>').text(tr('ui.refreshNodeList', {}, 'Refresh Node List')).appendTo(refreshNodesBtn);
|
|
71
|
+
|
|
72
|
+
const refreshStateBtn = $('<button type="button" class="red-ui-button"></button>').appendTo(toolbar);
|
|
73
|
+
$('<i class="fa fa-repeat"></i>').appendTo(refreshStateBtn);
|
|
74
|
+
refreshStateBtn.append(' ');
|
|
75
|
+
$('<span>').text(tr('ui.refreshSummary', {}, 'Refresh Summary')).appendTo(refreshStateBtn);
|
|
61
76
|
const autoLabel = $('<label style="display:flex;align-items:center;gap:4px; margin:0 0 0 6px; font-size:0.9em;"></label>').appendTo(toolbar);
|
|
62
77
|
const autoChk = $('<input type="checkbox">').appendTo(autoLabel);
|
|
63
|
-
$('<span>').text('Auto').appendTo(autoLabel);
|
|
78
|
+
$('<span>').text(tr('ui.auto', {}, 'Auto')).appendTo(autoLabel);
|
|
64
79
|
$('<div class="knx-ai-spacer"></div>').appendTo(toolbar);
|
|
65
|
-
const statusEl = $('<div>').attr('id', 'knx-ai-status').text('Ready').appendTo(toolbar);
|
|
80
|
+
const statusEl = $('<div>').attr('id', 'knx-ai-status').text(tr('ui.status.ready', {}, 'Ready')).appendTo(toolbar);
|
|
66
81
|
|
|
67
82
|
const body = $('<div>').attr('id', 'knx-ai-body').appendTo(container);
|
|
68
83
|
|
|
69
|
-
const emptyNotice = $('<div>').css({ color: '#888', fontStyle: 'italic' }).text('No KNX AI nodes found.').appendTo(body);
|
|
84
|
+
const emptyNotice = $('<div>').css({ color: '#888', fontStyle: 'italic' }).text(tr('ui.empty.noNodes', {}, 'No KNX AI nodes found.')).appendTo(body);
|
|
70
85
|
|
|
71
86
|
const summarySection = $('<div class="knx-ai-section"></div>').appendTo(body);
|
|
72
|
-
$('<h3>').text('Summary').appendTo(summarySection);
|
|
87
|
+
$('<h3>').text(tr('ui.sections.summary', {}, 'Summary')).appendTo(summarySection);
|
|
73
88
|
const summaryPre = $('<pre>').attr('id', 'knx-ai-summary').text('').appendTo(summarySection);
|
|
74
89
|
|
|
75
90
|
const anomaliesSection = $('<div class="knx-ai-section"></div>').appendTo(body);
|
|
76
|
-
$('<h3>').text('Anomalies').appendTo(anomaliesSection);
|
|
91
|
+
$('<h3>').text(tr('ui.sections.anomalies', {}, 'Anomalies')).appendTo(anomaliesSection);
|
|
77
92
|
const anomaliesWrap = $('<div>').attr('id', 'knx-ai-anomalies').appendTo(anomaliesSection);
|
|
78
93
|
|
|
79
94
|
const chatSection = $('<div class="knx-ai-section"></div>').appendTo(body);
|
|
80
|
-
$('<h3>').text('Ask').appendTo(chatSection);
|
|
95
|
+
$('<h3>').text(tr('ui.sections.ask', {}, 'Ask')).appendTo(chatSection);
|
|
81
96
|
const chatBox = $('<div>').attr('id', 'knx-ai-chat').appendTo(chatSection);
|
|
82
97
|
const chatLog = $('<div>').attr('id', 'knx-ai-chat-log').appendTo(chatBox);
|
|
83
98
|
const chatInputRow = $('<div>').attr('id', 'knx-ai-chat-input').appendTo(chatBox);
|
|
84
|
-
const chatInput = $('<input type="text" placeholder
|
|
85
|
-
const chatSendBtn = $('<button type="button" class="red-ui-button"
|
|
99
|
+
const chatInput = $('<input type="text">').attr('placeholder', tr('ui.chat.placeholder', {}, 'Ask a question about KNX traffic…')).appendTo(chatInputRow);
|
|
100
|
+
const chatSendBtn = $('<button type="button" class="red-ui-button"></button>').appendTo(chatInputRow);
|
|
101
|
+
$('<i class="fa fa-paper-plane"></i>').appendTo(chatSendBtn);
|
|
102
|
+
chatSendBtn.append(' ');
|
|
103
|
+
$('<span>').text(tr('ui.chat.send', {}, 'Send')).appendTo(chatSendBtn);
|
|
86
104
|
|
|
87
105
|
const storageKey = 'knxUltimateAI:selectedNodeId';
|
|
88
106
|
const autoKey = 'knxUltimateAI:autoRefresh';
|
|
@@ -122,7 +140,7 @@
|
|
|
122
140
|
const renderAnomalies = (items) => {
|
|
123
141
|
anomaliesWrap.empty();
|
|
124
142
|
if (!items || !items.length) {
|
|
125
|
-
$('<div>').css({ color: '#888', fontStyle: 'italic' }).text('No anomalies.').appendTo(anomaliesWrap);
|
|
143
|
+
$('<div>').css({ color: '#888', fontStyle: 'italic' }).text(tr('ui.empty.noAnomalies', {}, 'No anomalies.')).appendTo(anomaliesWrap);
|
|
126
144
|
return;
|
|
127
145
|
}
|
|
128
146
|
items.slice().reverse().slice(0, 30).forEach((entry) => {
|
|
@@ -148,7 +166,7 @@
|
|
|
148
166
|
if (kind === 'assistant') {
|
|
149
167
|
const raw = normalizeChatText(text);
|
|
150
168
|
const trimmed = raw.trim();
|
|
151
|
-
$msg.html(trimmed ? renderMarkdownToHtml(raw) : renderMarkdownToHtml('(
|
|
169
|
+
$msg.html(trimmed ? renderMarkdownToHtml(raw) : renderMarkdownToHtml(tr('ui.chat.emptyAnswer', {}, '(empty answer)')));
|
|
152
170
|
} else {
|
|
153
171
|
$msg.text(normalizeChatText(text));
|
|
154
172
|
}
|
|
@@ -159,7 +177,7 @@
|
|
|
159
177
|
if (pendingChatEl) return;
|
|
160
178
|
pendingChatEl = $('<div class="knx-ai-chat-msg knx-ai-chat-pending"></div>').appendTo(chatLog);
|
|
161
179
|
$('<i class="fa fa-spinner fa-spin"></i>').appendTo(pendingChatEl);
|
|
162
|
-
$('<span>').text('
|
|
180
|
+
$('<span>').text(tr('ui.chat.pending', {}, 'Thinking…')).appendTo(pendingChatEl);
|
|
163
181
|
try { chatLog.scrollTop(chatLog[0].scrollHeight); } catch (e) { }
|
|
164
182
|
};
|
|
165
183
|
|
|
@@ -313,24 +331,29 @@
|
|
|
313
331
|
const formatSummaryText = (data) => {
|
|
314
332
|
const nodeInfo = data && data.node ? data.node : {};
|
|
315
333
|
const s = data && data.summary ? data.summary : null;
|
|
316
|
-
if (!s) return '
|
|
334
|
+
if (!s) return tr('summary.noData', {}, 'No data available.');
|
|
317
335
|
|
|
318
336
|
const lines = [];
|
|
319
337
|
const headerBits = [];
|
|
320
338
|
if (nodeInfo.name) headerBits.push(nodeInfo.name);
|
|
321
|
-
if (nodeInfo.gatewayName) headerBits.push('Gateway: ' + nodeInfo.gatewayName);
|
|
322
|
-
if (s.meta && s.meta.generatedAt) headerBits.push('
|
|
339
|
+
if (nodeInfo.gatewayName) headerBits.push(tr('summary.header.gateway', { gatewayName: nodeInfo.gatewayName }, 'Gateway: ' + nodeInfo.gatewayName));
|
|
340
|
+
if (s.meta && s.meta.generatedAt) headerBits.push(tr('summary.header.updated', { at: s.meta.generatedAt }, 'Updated: ' + s.meta.generatedAt));
|
|
323
341
|
if (headerBits.length) lines.push(headerBits.join(' · '));
|
|
324
342
|
lines.push('');
|
|
325
343
|
|
|
326
344
|
const c = s.counters || {};
|
|
327
345
|
const win = (s.meta && s.meta.analysisWindowSec) ? s.meta.analysisWindowSec : '';
|
|
328
|
-
lines.push(`
|
|
329
|
-
lines.push(
|
|
346
|
+
lines.push(tr('summary.analysisWindowLine', { seconds: win }, `Analysis window: ${win}s`));
|
|
347
|
+
lines.push(tr('summary.statsLine', {
|
|
348
|
+
telegrams: (c.telegrams ?? 0),
|
|
349
|
+
rate: (c.overallRatePerSec ?? 0),
|
|
350
|
+
echoed: (c.echoed ?? 0),
|
|
351
|
+
unknownDpt: (c.unknownDpt ?? 0)
|
|
352
|
+
}, `Telegrams: ${c.telegrams ?? 0} · Rate: ${(c.overallRatePerSec ?? 0)}/s · Echoed: ${c.echoed ?? 0} · Unknown DPT: ${c.unknownDpt ?? 0}`));
|
|
330
353
|
|
|
331
354
|
if (Array.isArray(s.topGAs) && s.topGAs.length) {
|
|
332
355
|
lines.push('');
|
|
333
|
-
lines.push('Top Group Address:');
|
|
356
|
+
lines.push(tr('summary.topGAsTitle', {}, 'Top Group Address:'));
|
|
334
357
|
s.topGAs.slice(0, 20).forEach((x, idx) => {
|
|
335
358
|
lines.push(`${idx + 1}. ${x.ga} (${x.count})`);
|
|
336
359
|
});
|
|
@@ -338,7 +361,7 @@
|
|
|
338
361
|
|
|
339
362
|
if (s.byEvent && Object.keys(s.byEvent).length) {
|
|
340
363
|
lines.push('');
|
|
341
|
-
lines.push('
|
|
364
|
+
lines.push(tr('summary.eventsTitle', {}, 'Events:'));
|
|
342
365
|
Object.keys(s.byEvent).sort().forEach((k) => {
|
|
343
366
|
lines.push(`- ${k}: ${s.byEvent[k]}`);
|
|
344
367
|
});
|
|
@@ -346,9 +369,11 @@
|
|
|
346
369
|
|
|
347
370
|
if (Array.isArray(s.patterns) && s.patterns.length) {
|
|
348
371
|
lines.push('');
|
|
349
|
-
lines.push('
|
|
372
|
+
lines.push(tr('summary.patternsTitle', {}, 'Patterns:'));
|
|
350
373
|
s.patterns.slice(0, 15).forEach((p) => {
|
|
351
|
-
|
|
374
|
+
const item = tr('summary.patternItem', { from: p.from, to: p.to, count: p.count, withinMs: p.withinMs },
|
|
375
|
+
`${p.from} → ${p.to} (${p.count} times within ${p.withinMs}ms)`);
|
|
376
|
+
lines.push(`- ${item}`);
|
|
352
377
|
});
|
|
353
378
|
}
|
|
354
379
|
|
|
@@ -379,16 +404,16 @@
|
|
|
379
404
|
};
|
|
380
405
|
|
|
381
406
|
const fetchNodes = () => {
|
|
382
|
-
setStatus('Loading nodes…');
|
|
407
|
+
setStatus(tr('ui.status.loadingNodes', {}, 'Loading nodes…'));
|
|
383
408
|
const currentSelected = nodeSelect.val() || loadStoredNode() || '';
|
|
384
409
|
return $.getJSON('knxUltimateAI/sidebar/nodes')
|
|
385
410
|
.done((data) => {
|
|
386
411
|
populateNodes(data && data.nodes ? data.nodes : [], currentSelected);
|
|
387
|
-
setStatus('Ready');
|
|
412
|
+
setStatus(tr('ui.status.ready', {}, 'Ready'));
|
|
388
413
|
})
|
|
389
414
|
.fail((xhr) => {
|
|
390
415
|
setEnabled(false);
|
|
391
|
-
let err = 'Failed to load nodes';
|
|
416
|
+
let err = tr('ui.errors.loadNodes', {}, 'Failed to load nodes');
|
|
392
417
|
try { if (xhr && xhr.responseJSON && xhr.responseJSON.error) err = xhr.responseJSON.error; } catch (e) { }
|
|
393
418
|
setStatus(err);
|
|
394
419
|
});
|
|
@@ -399,7 +424,7 @@
|
|
|
399
424
|
if (!nodeId) return;
|
|
400
425
|
const fresh = opts && opts.fresh ? 1 : 0;
|
|
401
426
|
lastStateNodeId = nodeId;
|
|
402
|
-
setStatus('Loading…');
|
|
427
|
+
setStatus(tr('ui.status.loading', {}, 'Loading…'));
|
|
403
428
|
return $.getJSON('knxUltimateAI/sidebar/state?nodeId=' + encodeURIComponent(nodeId) + '&fresh=' + fresh)
|
|
404
429
|
.done((data) => {
|
|
405
430
|
summaryPre.text(formatSummaryText(data));
|
|
@@ -409,17 +434,17 @@
|
|
|
409
434
|
if (!llmEnabled) {
|
|
410
435
|
chatInput.prop('disabled', true);
|
|
411
436
|
chatSendBtn.prop('disabled', true);
|
|
412
|
-
chatInput.attr('placeholder', 'LLM disabled in node config');
|
|
437
|
+
chatInput.attr('placeholder', tr('ui.chat.llmDisabled', {}, 'LLM disabled in node config'));
|
|
413
438
|
} else {
|
|
414
439
|
chatInput.prop('disabled', false);
|
|
415
440
|
chatSendBtn.prop('disabled', false);
|
|
416
|
-
chatInput.attr('placeholder', 'Ask a question about KNX traffic…');
|
|
441
|
+
chatInput.attr('placeholder', tr('ui.chat.placeholder', {}, 'Ask a question about KNX traffic…'));
|
|
417
442
|
}
|
|
418
443
|
|
|
419
|
-
setStatus('Ready');
|
|
444
|
+
setStatus(tr('ui.status.ready', {}, 'Ready'));
|
|
420
445
|
})
|
|
421
446
|
.fail((xhr) => {
|
|
422
|
-
let err = 'Failed to load state';
|
|
447
|
+
let err = tr('ui.errors.loadState', {}, 'Failed to load state');
|
|
423
448
|
try { if (xhr && xhr.responseJSON && xhr.responseJSON.error) err = xhr.responseJSON.error; } catch (e) { }
|
|
424
449
|
setStatus(err);
|
|
425
450
|
});
|
|
@@ -431,7 +456,7 @@
|
|
|
431
456
|
if (!nodeId || !q) return;
|
|
432
457
|
chatInput.val('');
|
|
433
458
|
appendChat('user', q);
|
|
434
|
-
setStatus('Asking…');
|
|
459
|
+
setStatus(tr('ui.status.asking', {}, 'Asking…'));
|
|
435
460
|
chatSendBtn.prop('disabled', true);
|
|
436
461
|
showChatPending();
|
|
437
462
|
return $.ajax({
|
|
@@ -444,14 +469,14 @@
|
|
|
444
469
|
const answer = (data && data.answer !== undefined) ? data.answer : '';
|
|
445
470
|
hideChatPending();
|
|
446
471
|
appendChat('assistant', answer);
|
|
447
|
-
setStatus('Ready');
|
|
472
|
+
setStatus(tr('ui.status.ready', {}, 'Ready'));
|
|
448
473
|
})
|
|
449
474
|
.fail((xhr) => {
|
|
450
|
-
let err = 'Ask failed';
|
|
475
|
+
let err = tr('ui.errors.askFailed', {}, 'Ask failed');
|
|
451
476
|
try { if (xhr && xhr.responseJSON && xhr.responseJSON.error) err = xhr.responseJSON.error; } catch (e) { }
|
|
452
477
|
hideChatPending();
|
|
453
478
|
appendChat('error', err);
|
|
454
|
-
setStatus('Ready');
|
|
479
|
+
setStatus(tr('ui.status.ready', {}, 'Ready'));
|
|
455
480
|
})
|
|
456
481
|
.always(() => {
|
|
457
482
|
hideChatPending();
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=20.18.1"
|
|
5
5
|
},
|
|
6
|
-
"version": "4.1.
|
|
6
|
+
"version": "4.1.27",
|
|
7
7
|
"description": "Control your KNX and KNX Secure intallation via Node-Red! A bunch of KNX nodes, with integrated Philips HUE control, ETS group address importer, and KNX routing between interfaces. Easy to use and highly configurable.",
|
|
8
8
|
"files": [
|
|
9
9
|
"nodes/",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"dns-sync": "0.2.1",
|
|
20
20
|
"js-yaml": "4.1.1",
|
|
21
|
-
"knxultimate": "5.2.
|
|
21
|
+
"knxultimate": "5.2.8",
|
|
22
22
|
"lodash": "4.17.21",
|
|
23
23
|
"node-color-log": "12.0.1",
|
|
24
24
|
"ping": "0.4.4",
|
|
@@ -104,4 +104,4 @@
|
|
|
104
104
|
"mocha": "^10.4.0",
|
|
105
105
|
"marked": "^14.1.0"
|
|
106
106
|
}
|
|
107
|
-
}
|
|
107
|
+
}
|