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 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/>
@@ -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>&nbsp&nbsp<span style="color:red" &nbsp &nbsp <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
- <b><span data-i18n="knxUltimateAI.title"></span></b>
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": "Summary/Stats",
50
- "anomalies": "Anomalies",
51
- "assistant": "AI 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": "Summary/Stats",
50
- "anomalies": "Anomalies",
51
- "assistant": "AI 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": "Summary/Stats",
49
+ "summary": "Résumé/Stats",
50
50
  "anomalies": "Anomalies",
51
- "assistant": "AI 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": "Summary/Stats",
50
- "anomalies": "Anomalies",
51
- "assistant": "AI Assistant"
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"><i class="fa fa-refresh"></i> Refresh Node List</button>').appendTo(toolbar);
60
- const refreshStateBtn = $('<button type="button" class="red-ui-button"><i class="fa fa-repeat"></i> Refresh Summary</button>').appendTo(toolbar);
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="Ask a question about KNX traffic…">').appendTo(chatInputRow);
85
- const chatSendBtn = $('<button type="button" class="red-ui-button"><i class="fa fa-paper-plane"></i> Send</button>').appendTo(chatInputRow);
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('(risposta vuota)'));
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('Sto pensando…').appendTo(pendingChatEl);
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 'Nessun dato disponibile.';
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('Aggiornato: ' + s.meta.generatedAt);
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(`Finestra analisi: ${win}s`);
329
- lines.push(`Telegrammi: ${c.telegrams ?? 0} · Rate: ${(c.overallRatePerSec ?? 0)}/s · Echoed: ${c.echoed ?? 0} · DPT sconosciuti: ${c.unknownDpt ?? 0}`);
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('Eventi:');
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('Pattern (sequenze ricorrenti):');
372
+ lines.push(tr('summary.patternsTitle', {}, 'Patterns:'));
350
373
  s.patterns.slice(0, 15).forEach((p) => {
351
- lines.push(`- ${p.from} ${p.to} (${p.count} volte entro ${p.withinMs}ms)`);
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.25",
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.7",
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
+ }