node-red-contrib-knx-ultimate 7.1.0-beta.2 → 7.1.2
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 +9 -0
- package/README.md +5 -1
- package/nodes/knxUltimateAI.html +1820 -0
- package/nodes/knxUltimateAI.js +16797 -0
- package/nodes/knxUltimateAIHomeAssistant.html +40 -0
- package/nodes/knxUltimateAIHomeAssistant.js +152 -0
- package/nodes/knxUltimateHueController.html +1 -1
- package/nodes/knxUltimateMatterBridge.html +2 -2
- package/nodes/knxUltimateMatterControllerDevice.html +2 -2
- package/nodes/locales/de/knxUltimateAI.html +5 -0
- package/nodes/locales/de/knxUltimateAI.json +231 -0
- package/nodes/locales/de/knxUltimateUtility.json +8 -0
- package/nodes/locales/en/knxUltimateAI.html +5 -0
- package/nodes/locales/en/knxUltimateAI.json +234 -0
- package/nodes/locales/en/knxUltimateUtility.json +8 -0
- package/nodes/locales/es/knxUltimateAI.html +5 -0
- package/nodes/locales/es/knxUltimateAI.json +231 -0
- package/nodes/locales/es/knxUltimateUtility.json +9 -1
- package/nodes/locales/fr/knxUltimateAI.html +5 -0
- package/nodes/locales/fr/knxUltimateAI.json +231 -0
- package/nodes/locales/fr/knxUltimateUtility.json +8 -0
- package/nodes/locales/it/knxUltimateAI.html +5 -0
- package/nodes/locales/it/knxUltimateAI.json +234 -0
- package/nodes/locales/it/knxUltimateUtility.json +8 -0
- package/nodes/locales/zh-CN/knxUltimateAI.html +5 -0
- package/nodes/locales/zh-CN/knxUltimateAI.json +231 -0
- package/nodes/locales/zh-CN/knxUltimateUtility.json +8 -0
- package/nodes/plugins/knxUltimate-cerebrum-runtime-plugin.js +79 -0
- package/nodes/plugins/knxUltimate-migration-notice-plugin.html +19 -0
- package/nodes/plugins/knxUltimateAI-vue/assets/app.css +1 -0
- package/nodes/plugins/knxUltimateAI-vue/assets/app.js +13 -0
- package/nodes/plugins/knxUltimateAI-vue/assets/chunk-html2canvas.esm.js +5 -0
- package/nodes/plugins/knxUltimateAI-vue/assets/chunk-index.es.js +5 -0
- package/nodes/plugins/knxUltimateAI-vue/assets/chunk-jspdf.es.min.js +79 -0
- package/nodes/plugins/knxUltimateAI-vue/assets/chunk-purify.es.js +3 -0
- package/nodes/plugins/knxUltimateAI-vue/index.html +13 -0
- package/nodes/plugins/knxUltimateViewer-vue/assets/app.js +2 -2
- package/nodes/utils/knxAiCamera.js +302 -0
- package/nodes/utils/knxAiCatalogRetrieval.js +349 -0
- package/nodes/utils/knxAiCerebrum.js +406 -0
- package/nodes/utils/knxAiChatContext.js +496 -0
- package/nodes/utils/knxAiEventHistory.js +521 -0
- package/nodes/utils/knxAiHomeMemory.js +1021 -0
- package/nodes/utils/knxAiScheduler.js +445 -0
- package/nodes/utils/knxAiSemanticContext.js +662 -0
- package/nodes/utils/knxAiTelegramVoice.js +545 -0
- package/nodes/utils/knxAiWebAccess.js +631 -0
- package/package.json +17 -9
- package/resources/KNXAIChatAdapterMappings.js +416 -0
- package/resources/hueControllerMigrationDialog.js +3 -0
- package/resources/knxUtilityMigration.js +3 -0
- package/resources/legacyMigrationNotice.js +143 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=20.18.1"
|
|
5
5
|
},
|
|
6
|
-
"version": "7.1.
|
|
6
|
+
"version": "7.1.2",
|
|
7
7
|
"description": "KNX Ultimate is the most advanced KNX integration for Node-RED, providing secure KNX/IP communication, routing, ETS project import, Philips Hue, Matter Controller and Matter Bridge (control matter device via KNX and expose KNX GA via Matter), MQTT and Modbus adapters, diagnostics, virtual devices, and powerful automation nodes. Build professional, reliable, and scalable smart home and building automation projects with minimal effort.",
|
|
8
8
|
"files": [
|
|
9
9
|
"nodes/",
|
|
@@ -24,13 +24,17 @@
|
|
|
24
24
|
"mqtt": "^5.15.2",
|
|
25
25
|
"ping": "0.4.4",
|
|
26
26
|
"simple-get": "4.0.1",
|
|
27
|
-
"winston": "^3.17.0"
|
|
27
|
+
"winston": "^3.17.0",
|
|
28
|
+
"google-translate-tts": "^0.3.0",
|
|
29
|
+
"js-yaml": "4.3.2"
|
|
28
30
|
},
|
|
29
31
|
"node-red": {
|
|
30
32
|
"version": ">=3.1.1",
|
|
31
33
|
"plugins": {
|
|
32
34
|
"commonFunctions": "/nodes/commonFunctions.js",
|
|
33
|
-
"knxUltimateFlowBubbles": "/nodes/plugins/knxUltimate-flow-bubbles-plugin.html"
|
|
35
|
+
"knxUltimateFlowBubbles": "/nodes/plugins/knxUltimate-flow-bubbles-plugin.html",
|
|
36
|
+
"knxUltimateMigrationNotice": "/nodes/plugins/knxUltimate-migration-notice-plugin.html",
|
|
37
|
+
"knxUltimateCerebrumRuntime": "/nodes/plugins/knxUltimate-cerebrum-runtime-plugin.js"
|
|
34
38
|
},
|
|
35
39
|
"nodes": {
|
|
36
40
|
"knxUltimate": "/nodes/knxUltimate.js",
|
|
@@ -71,7 +75,9 @@
|
|
|
71
75
|
"knxUltimateHueCameraMotion": "/nodes/knxUltimateHueCameraMotion.js",
|
|
72
76
|
"matter-config": "/nodes/matter-config.js",
|
|
73
77
|
"matterbridge-config": "/nodes/matterbridge-config.js",
|
|
74
|
-
"knxUltimateMatterBridge": "/nodes/knxUltimateMatterBridge.js"
|
|
78
|
+
"knxUltimateMatterBridge": "/nodes/knxUltimateMatterBridge.js",
|
|
79
|
+
"knxUltimateAI": "/nodes/knxUltimateAI.js",
|
|
80
|
+
"knxUltimateAIHomeAssistant": "/nodes/knxUltimateAIHomeAssistant.js"
|
|
75
81
|
}
|
|
76
82
|
},
|
|
77
83
|
"repository": {
|
|
@@ -105,7 +111,7 @@
|
|
|
105
111
|
},
|
|
106
112
|
"license": "MIT",
|
|
107
113
|
"scripts": {
|
|
108
|
-
"build": "npm run knx-viewer:build",
|
|
114
|
+
"build": "npm run knx-ai:build && npm run knx-viewer:build",
|
|
109
115
|
"prepack": "npm run build",
|
|
110
116
|
"prepublishOnly": "npm run build",
|
|
111
117
|
"test": "npm run hue-controller:check && npm run knx-utility:check && DEBUG_KNX_HUE_TEST=0 npm run test:unit && node scripts/check-node-load.js",
|
|
@@ -137,15 +143,17 @@
|
|
|
137
143
|
"docs:serve": "node scripts/dev-serve-docs.js",
|
|
138
144
|
"docs:serve:norl": "node scripts/dev-serve-docs.js",
|
|
139
145
|
"knx-utility:generate": "node scripts/generate-knx-utility-profiles.js",
|
|
140
|
-
"knx-utility:check": "node scripts/generate-knx-utility-profiles.js --check"
|
|
146
|
+
"knx-utility:check": "node scripts/generate-knx-utility-profiles.js --check",
|
|
147
|
+
"knx-ai:dev": "vite --config ui/knxUltimateAI-vue/vite.config.mjs",
|
|
148
|
+
"knx-ai:build": "vite build --config ui/knxUltimateAI-vue/vite.config.mjs"
|
|
141
149
|
},
|
|
142
150
|
"devDependencies": {
|
|
143
151
|
"@vitejs/plugin-vue": "^6.0.8",
|
|
144
152
|
"chai": "^4.3.10",
|
|
145
|
-
"js-yaml": "4.3.2",
|
|
146
153
|
"marked": "^14.1.0",
|
|
147
154
|
"mocha": "^10.4.0",
|
|
148
155
|
"vite": "^7.3.6",
|
|
149
|
-
"vue": "^3.5.41"
|
|
156
|
+
"vue": "^3.5.41",
|
|
157
|
+
"jspdf": "^4.2.1"
|
|
150
158
|
}
|
|
151
|
-
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
; (function (root) {
|
|
2
|
+
const KNXAIChatAdapterMappings = [
|
|
3
|
+
{
|
|
4
|
+
id: 'windkh-telegrambot',
|
|
5
|
+
title: 'windkh/node-red-contrib-telegrambot',
|
|
6
|
+
inputCode: `// Receiver/Event -> Cerebrum Ultimate
|
|
7
|
+
// Supports normal "telegram receiver" messages, including the one-time
|
|
8
|
+
// reply-keyboard confirmation buttons, plus legacy callback_query events.
|
|
9
|
+
const telegram = msg.payload;
|
|
10
|
+
if (!telegram || typeof telegram !== 'object') return;
|
|
11
|
+
|
|
12
|
+
const chatId = telegram.chatId;
|
|
13
|
+
if (chatId === undefined || chatId === null) return;
|
|
14
|
+
|
|
15
|
+
msg.sessionId = String(chatId);
|
|
16
|
+
msg.language =
|
|
17
|
+
(msg.originalMessage && msg.originalMessage.from && msg.originalMessage.from.language_code) ||
|
|
18
|
+
msg.language ||
|
|
19
|
+
'';
|
|
20
|
+
|
|
21
|
+
if (telegram.type === 'voice') {
|
|
22
|
+
const original = msg.originalMessage && typeof msg.originalMessage === 'object'
|
|
23
|
+
? msg.originalMessage
|
|
24
|
+
: {};
|
|
25
|
+
const voice = original.voice && typeof original.voice === 'object'
|
|
26
|
+
? original.voice
|
|
27
|
+
: original.message && original.message.voice && typeof original.message.voice === 'object'
|
|
28
|
+
? original.message.voice
|
|
29
|
+
: {};
|
|
30
|
+
const fileId = String(telegram.content || voice.file_id || '').trim();
|
|
31
|
+
const weblink = String(telegram.weblink || msg.weblink || '').trim();
|
|
32
|
+
const botDetails = msg.telegramBot && typeof msg.telegramBot === 'object' ? msg.telegramBot : {};
|
|
33
|
+
const botApiBase = String(botDetails.baseApiUrl || botDetails.baseapiurl || botDetails.baseApiURL || '').trim();
|
|
34
|
+
let allowedOrigin = 'https://api.telegram.org';
|
|
35
|
+
if (botApiBase) {
|
|
36
|
+
try { allowedOrigin = new URL(botApiBase).origin; } catch (error) { /* use Telegram default */ }
|
|
37
|
+
}
|
|
38
|
+
if (!fileId && !weblink) return;
|
|
39
|
+
msg.topic = 'ask';
|
|
40
|
+
msg.knxAi = Object.assign({}, msg.knxAi, {
|
|
41
|
+
sessionId: String(chatId),
|
|
42
|
+
voiceInput: {
|
|
43
|
+
source: 'telegram',
|
|
44
|
+
originalType: 'voice',
|
|
45
|
+
fileId: fileId,
|
|
46
|
+
weblink: weblink,
|
|
47
|
+
allowedOrigin: allowedOrigin,
|
|
48
|
+
mediaType: String(voice.mime_type || telegram.contentType || telegram.mimeType || 'audio/ogg'),
|
|
49
|
+
filename: String(voice.file_name || telegram.filename || 'telegram-voice.ogg'),
|
|
50
|
+
durationSeconds: Math.max(0, Number(voice.duration || telegram.duration) || 0),
|
|
51
|
+
fileSize: Math.max(0, Number(voice.file_size || telegram.fileSize) || 0)
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return msg;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const content = typeof telegram.content === 'string' ? telegram.content.trim() : '';
|
|
58
|
+
if (content === '') return;
|
|
59
|
+
|
|
60
|
+
if (telegram.type === 'callback_query' && (content === 'confirm' || content === 'cancel')) {
|
|
61
|
+
msg.topic = content;
|
|
62
|
+
msg.knxAi = Object.assign({}, msg.knxAi, {
|
|
63
|
+
sessionId: String(chatId),
|
|
64
|
+
confirm: content === 'confirm'
|
|
65
|
+
});
|
|
66
|
+
return msg;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (telegram.type !== 'message') return;
|
|
70
|
+
msg.topic = 'ask';
|
|
71
|
+
msg.prompt = content;
|
|
72
|
+
return msg;`,
|
|
73
|
+
outputCode: `// Cerebrum Ultimate chat output -> Telegram sender
|
|
74
|
+
// Converts the text reply and, when required, adds a one-time reply keyboard.
|
|
75
|
+
// A button click returns through the normal Telegram receiver, so no separate
|
|
76
|
+
// callback event node is required.
|
|
77
|
+
const source = msg.inputMessage && typeof msg.inputMessage === 'object'
|
|
78
|
+
? msg.inputMessage
|
|
79
|
+
: inputMessage;
|
|
80
|
+
const sourcePayload = source && source.payload && typeof source.payload === 'object'
|
|
81
|
+
? source.payload
|
|
82
|
+
: {};
|
|
83
|
+
const chatId = sourcePayload.chatId !== undefined
|
|
84
|
+
? sourcePayload.chatId
|
|
85
|
+
: source && source.chatId;
|
|
86
|
+
if (chatId === undefined || chatId === null) return;
|
|
87
|
+
|
|
88
|
+
let content = msg.payload;
|
|
89
|
+
if (content && typeof content === 'object') {
|
|
90
|
+
content = content.error || content.message || JSON.stringify(content);
|
|
91
|
+
}
|
|
92
|
+
content = String(content === undefined || content === null ? '' : content);
|
|
93
|
+
|
|
94
|
+
const image = msg.knxAi && msg.knxAi.image;
|
|
95
|
+
if (image && Buffer.isBuffer(image.data)) {
|
|
96
|
+
msg.payload = {
|
|
97
|
+
chatId: chatId,
|
|
98
|
+
type: 'photo',
|
|
99
|
+
content: image.data,
|
|
100
|
+
options: {
|
|
101
|
+
caption: content.slice(0, 1024)
|
|
102
|
+
},
|
|
103
|
+
fileOptions: {
|
|
104
|
+
filename: image.filename || 'camera-snapshot.jpg',
|
|
105
|
+
contentType: image.mediaType || 'image/jpeg'
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
return msg;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const audio = msg.knxAi && msg.knxAi.audio;
|
|
112
|
+
if (audio && Buffer.isBuffer(audio.data)) {
|
|
113
|
+
const voiceLanguage = String(msg.knxAi.language || (source && source.language) || '').trim().toLowerCase().split(/[-_]/)[0];
|
|
114
|
+
const disclosureLabels = {
|
|
115
|
+
de: 'KI-generierte Stimme',
|
|
116
|
+
en: 'AI-generated voice',
|
|
117
|
+
es: 'Voz generada por IA',
|
|
118
|
+
fr: 'Voix générée par l’IA',
|
|
119
|
+
it: 'Voce generata dall’IA',
|
|
120
|
+
zh: 'AI 生成的语音'
|
|
121
|
+
};
|
|
122
|
+
const disclosure = disclosureLabels[voiceLanguage] || disclosureLabels.en;
|
|
123
|
+
const voiceOptions = { caption: [disclosure, content].filter(Boolean).join('\\n').slice(0, 1024) };
|
|
124
|
+
const confirmation = msg.knxAi && msg.knxAi.confirmationRequest;
|
|
125
|
+
if (confirmation && confirmation.required === true && Array.isArray(confirmation.actions)) {
|
|
126
|
+
voiceOptions.reply_markup = JSON.stringify({
|
|
127
|
+
keyboard: [confirmation.actions.map(function (action) {
|
|
128
|
+
return { text: action.label };
|
|
129
|
+
})],
|
|
130
|
+
resize_keyboard: true,
|
|
131
|
+
one_time_keyboard: true
|
|
132
|
+
});
|
|
133
|
+
} else if (msg.knxAi && Array.isArray(msg.knxAi.suggestions) && msg.knxAi.suggestions.length) {
|
|
134
|
+
voiceOptions.reply_markup = JSON.stringify({
|
|
135
|
+
keyboard: msg.knxAi.suggestions.slice(0, 3).map(function (suggestion) {
|
|
136
|
+
return [{ text: Array.from(String(suggestion.text || suggestion.label || '').trim()).slice(0, 64).join('') }];
|
|
137
|
+
}).filter(function (row) { return row[0].text; }),
|
|
138
|
+
resize_keyboard: true,
|
|
139
|
+
one_time_keyboard: true
|
|
140
|
+
});
|
|
141
|
+
} else if (/^knx_confirmation_/.test(String(msg.knxAi.type || '')) || /^knx_routine_/.test(String(msg.knxAi.type || ''))) {
|
|
142
|
+
voiceOptions.reply_markup = JSON.stringify({ remove_keyboard: true });
|
|
143
|
+
}
|
|
144
|
+
msg.payload = {
|
|
145
|
+
chatId: chatId,
|
|
146
|
+
type: 'voice',
|
|
147
|
+
content: audio.data,
|
|
148
|
+
options: voiceOptions,
|
|
149
|
+
fileOptions: {
|
|
150
|
+
filename: audio.filename || 'knx-ai-reply.ogg',
|
|
151
|
+
contentType: audio.mediaType || 'audio/ogg'
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
return msg;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const telegramPayload = {
|
|
158
|
+
chatId: chatId,
|
|
159
|
+
type: 'message',
|
|
160
|
+
content: content
|
|
161
|
+
};
|
|
162
|
+
const confirmation = msg.knxAi && msg.knxAi.confirmationRequest;
|
|
163
|
+
if (confirmation && confirmation.required === true && Array.isArray(confirmation.actions)) {
|
|
164
|
+
telegramPayload.options = {
|
|
165
|
+
reply_markup: JSON.stringify({
|
|
166
|
+
keyboard: [confirmation.actions.map(function (action) {
|
|
167
|
+
return { text: action.label };
|
|
168
|
+
})],
|
|
169
|
+
resize_keyboard: true,
|
|
170
|
+
one_time_keyboard: true
|
|
171
|
+
})
|
|
172
|
+
};
|
|
173
|
+
} else if (msg.knxAi && Array.isArray(msg.knxAi.suggestions) && msg.knxAi.suggestions.length) {
|
|
174
|
+
telegramPayload.options = {
|
|
175
|
+
reply_markup: JSON.stringify({
|
|
176
|
+
keyboard: msg.knxAi.suggestions.slice(0, 3).map(function (suggestion) {
|
|
177
|
+
return [{ text: Array.from(String(suggestion.text || suggestion.label || '').trim()).slice(0, 64).join('') }];
|
|
178
|
+
}).filter(function (row) { return row[0].text; }),
|
|
179
|
+
resize_keyboard: true,
|
|
180
|
+
one_time_keyboard: true
|
|
181
|
+
})
|
|
182
|
+
};
|
|
183
|
+
} else if (msg.knxAi && (/^knx_confirmation_/.test(String(msg.knxAi.type || '')) || /^knx_routine_/.test(String(msg.knxAi.type || '')))) {
|
|
184
|
+
telegramPayload.options = {
|
|
185
|
+
reply_markup: JSON.stringify({ remove_keyboard: true })
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
msg.payload = telegramPayload;
|
|
190
|
+
return msg;`
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: 'redbot-telegram',
|
|
194
|
+
title: 'RedBot / node-red-contrib-chatbot (Telegram)',
|
|
195
|
+
inputCode: `// RedBot Telegram Receiver -> Cerebrum Ultimate
|
|
196
|
+
// RedBot normalizes Telegram text, voice audio and inline-button postbacks in msg.payload.
|
|
197
|
+
const redbot = msg.payload;
|
|
198
|
+
if (!redbot || typeof redbot !== 'object') return;
|
|
199
|
+
if (redbot.transport && redbot.transport !== 'telegram') return;
|
|
200
|
+
|
|
201
|
+
const chatId = redbot.chatId;
|
|
202
|
+
if (chatId === undefined || chatId === null) return;
|
|
203
|
+
|
|
204
|
+
msg.sessionId = String(chatId);
|
|
205
|
+
msg.language =
|
|
206
|
+
(msg.originalMessage && msg.originalMessage.from && msg.originalMessage.from.language_code) ||
|
|
207
|
+
redbot.language ||
|
|
208
|
+
msg.language ||
|
|
209
|
+
'';
|
|
210
|
+
|
|
211
|
+
const redbotType = String(redbot.type || '').trim().toLowerCase();
|
|
212
|
+
if ((redbotType === 'audio' || redbotType === 'voice') && Buffer.isBuffer(redbot.content)) {
|
|
213
|
+
const original = msg.originalMessage && typeof msg.originalMessage === 'object'
|
|
214
|
+
? msg.originalMessage
|
|
215
|
+
: {};
|
|
216
|
+
const voice = original.voice && typeof original.voice === 'object'
|
|
217
|
+
? original.voice
|
|
218
|
+
: original.message && original.message.voice && typeof original.message.voice === 'object'
|
|
219
|
+
? original.message.voice
|
|
220
|
+
: {};
|
|
221
|
+
msg.topic = 'ask';
|
|
222
|
+
msg.knxAi = Object.assign({}, msg.knxAi, {
|
|
223
|
+
sessionId: String(chatId),
|
|
224
|
+
voiceInput: {
|
|
225
|
+
source: 'telegram',
|
|
226
|
+
originalType: 'voice',
|
|
227
|
+
transport: 'redbot-buffer',
|
|
228
|
+
data: redbot.content,
|
|
229
|
+
fileId: String(voice.file_id || '').trim(),
|
|
230
|
+
mediaType: String(voice.mime_type || redbot.mimeType || 'audio/ogg'),
|
|
231
|
+
filename: String(voice.file_name || redbot.filename || 'telegram-voice.ogg'),
|
|
232
|
+
durationSeconds: Math.max(0, Number(voice.duration || redbot.duration) || 0),
|
|
233
|
+
fileSize: Math.max(0, Number(voice.file_size || redbot.fileSize) || redbot.content.length)
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
return msg;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const content = typeof redbot.content === 'string' ? redbot.content.trim() : '';
|
|
240
|
+
if (redbotType !== 'message' || content === '') return;
|
|
241
|
+
|
|
242
|
+
const action = content.toLowerCase();
|
|
243
|
+
if (action === 'confirm' || action === 'cancel') {
|
|
244
|
+
msg.topic = action;
|
|
245
|
+
msg.knxAi = Object.assign({}, msg.knxAi, {
|
|
246
|
+
sessionId: String(chatId),
|
|
247
|
+
confirm: action === 'confirm'
|
|
248
|
+
});
|
|
249
|
+
return msg;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
msg.topic = 'ask';
|
|
253
|
+
msg.prompt = content;
|
|
254
|
+
return msg;`,
|
|
255
|
+
outputCode: `// Cerebrum Ultimate chat output -> RedBot Telegram Sender
|
|
256
|
+
// Preserves RedBot conversation helpers and emits either a plain message
|
|
257
|
+
// or inline postback buttons for KNX confirmation/cancellation.
|
|
258
|
+
const source = msg.inputMessage && typeof msg.inputMessage === 'object'
|
|
259
|
+
? msg.inputMessage
|
|
260
|
+
: inputMessage;
|
|
261
|
+
const sourcePayload = source && source.payload && typeof source.payload === 'object'
|
|
262
|
+
? source.payload
|
|
263
|
+
: {};
|
|
264
|
+
const originalMessage = source && source.originalMessage && typeof source.originalMessage === 'object'
|
|
265
|
+
? source.originalMessage
|
|
266
|
+
: {};
|
|
267
|
+
const chatId = sourcePayload.chatId !== undefined
|
|
268
|
+
? sourcePayload.chatId
|
|
269
|
+
: originalMessage.chat && originalMessage.chat.id;
|
|
270
|
+
if (chatId === undefined || chatId === null) return;
|
|
271
|
+
|
|
272
|
+
// RedBot's Sender uses these helpers to resolve the active bot and chat context.
|
|
273
|
+
['originalMessage', 'chat', 'api', 'client', 'redBot'].forEach(function (key) {
|
|
274
|
+
if (msg[key] === undefined && source && source[key] !== undefined) {
|
|
275
|
+
msg[key] = source[key];
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// A persisted camera watch can fire immediately after Node-RED restarts,
|
|
280
|
+
// before a new inbound RedBot message has recreated its conversation helpers.
|
|
281
|
+
// RedBot's Telegram Sender still needs transport metadata and a chat accessor,
|
|
282
|
+
// even when tracking is disabled. Supply the smallest compatible outbound
|
|
283
|
+
// envelope; the configured Sender remains responsible for choosing the bot.
|
|
284
|
+
if (!msg.originalMessage || typeof msg.originalMessage !== 'object') {
|
|
285
|
+
msg.originalMessage = {};
|
|
286
|
+
}
|
|
287
|
+
if (!msg.originalMessage.transport) msg.originalMessage.transport = sourcePayload.transport || 'telegram';
|
|
288
|
+
if (msg.originalMessage.chatId === undefined) msg.originalMessage.chatId = chatId;
|
|
289
|
+
if (sourcePayload.userId !== undefined && msg.originalMessage.userId === undefined) {
|
|
290
|
+
msg.originalMessage.userId = sourcePayload.userId;
|
|
291
|
+
}
|
|
292
|
+
if (typeof msg.chat !== 'function') {
|
|
293
|
+
const contextValues = {
|
|
294
|
+
chatId: chatId,
|
|
295
|
+
userId: sourcePayload.userId,
|
|
296
|
+
transport: sourcePayload.transport || 'telegram'
|
|
297
|
+
};
|
|
298
|
+
const syntheticContext = {
|
|
299
|
+
get: function () {
|
|
300
|
+
const keys = Array.prototype.slice.call(arguments);
|
|
301
|
+
if (keys.length === 0) return Object.assign({}, contextValues);
|
|
302
|
+
if (keys.length === 1) return contextValues[keys[0]];
|
|
303
|
+
return keys.reduce(function (result, key) {
|
|
304
|
+
result[key] = contextValues[key];
|
|
305
|
+
return result;
|
|
306
|
+
}, {});
|
|
307
|
+
},
|
|
308
|
+
set: function (key, value) {
|
|
309
|
+
if (key && typeof key === 'object') Object.assign(contextValues, key);
|
|
310
|
+
else if (key !== undefined) contextValues[key] = value;
|
|
311
|
+
return syntheticContext;
|
|
312
|
+
},
|
|
313
|
+
remove: function () {
|
|
314
|
+
Array.prototype.slice.call(arguments).forEach(function (key) { delete contextValues[key]; });
|
|
315
|
+
return syntheticContext;
|
|
316
|
+
},
|
|
317
|
+
clear: function () {
|
|
318
|
+
Object.keys(contextValues).forEach(function (key) { delete contextValues[key]; });
|
|
319
|
+
return syntheticContext;
|
|
320
|
+
},
|
|
321
|
+
all: function () { return Object.assign({}, contextValues); }
|
|
322
|
+
};
|
|
323
|
+
msg.chat = function () {
|
|
324
|
+
return syntheticContext;
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (typeof msg.get !== 'function') {
|
|
328
|
+
msg.get = function (key) {
|
|
329
|
+
if (key === 'userId') return sourcePayload.userId;
|
|
330
|
+
if (key === 'chatId') return chatId;
|
|
331
|
+
if (key === 'transport') return sourcePayload.transport || 'telegram';
|
|
332
|
+
return undefined;
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let content = msg.payload;
|
|
337
|
+
if (content && typeof content === 'object') {
|
|
338
|
+
content = content.error || content.message || JSON.stringify(content);
|
|
339
|
+
}
|
|
340
|
+
content = String(content === undefined || content === null ? '' : content);
|
|
341
|
+
|
|
342
|
+
const image = msg.knxAi && msg.knxAi.image;
|
|
343
|
+
if (image && Buffer.isBuffer(image.data)) {
|
|
344
|
+
msg.payload = {
|
|
345
|
+
transport: sourcePayload.transport || 'telegram',
|
|
346
|
+
chatId: chatId,
|
|
347
|
+
type: 'photo',
|
|
348
|
+
inbound: false,
|
|
349
|
+
content: image.data,
|
|
350
|
+
filename: image.filename || 'camera-snapshot.jpg',
|
|
351
|
+
mimeType: image.mediaType || 'image/jpeg',
|
|
352
|
+
caption: content.slice(0, 1024)
|
|
353
|
+
};
|
|
354
|
+
if (sourcePayload.userId !== undefined) msg.payload.userId = sourcePayload.userId;
|
|
355
|
+
return msg;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const confirmation = msg.knxAi && msg.knxAi.confirmationRequest;
|
|
359
|
+
const audio = msg.knxAi && msg.knxAi.audio;
|
|
360
|
+
if (audio && Buffer.isBuffer(audio.data) && !(confirmation && confirmation.required === true)) {
|
|
361
|
+
const voiceLanguage = String(msg.knxAi.language || (source && source.language) || '').trim().toLowerCase().split(/[-_]/)[0];
|
|
362
|
+
const disclosureLabels = {
|
|
363
|
+
de: 'KI-generierte Stimme',
|
|
364
|
+
en: 'AI-generated voice',
|
|
365
|
+
es: 'Voz generada por IA',
|
|
366
|
+
fr: 'Voix générée par l’IA',
|
|
367
|
+
it: 'Voce generata dall’IA',
|
|
368
|
+
zh: 'AI 生成的语音'
|
|
369
|
+
};
|
|
370
|
+
const disclosure = disclosureLabels[voiceLanguage] || disclosureLabels.en;
|
|
371
|
+
msg.payload = {
|
|
372
|
+
transport: sourcePayload.transport || 'telegram',
|
|
373
|
+
chatId: chatId,
|
|
374
|
+
type: 'audio',
|
|
375
|
+
inbound: false,
|
|
376
|
+
content: audio.data,
|
|
377
|
+
filename: audio.filename || 'knx-ai-reply.ogg',
|
|
378
|
+
mimeType: audio.mediaType || 'audio/ogg',
|
|
379
|
+
caption: [disclosure, content].filter(Boolean).join('\\n').slice(0, 1024)
|
|
380
|
+
};
|
|
381
|
+
if (sourcePayload.userId !== undefined) msg.payload.userId = sourcePayload.userId;
|
|
382
|
+
return msg;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const redbotPayload = {
|
|
386
|
+
transport: sourcePayload.transport || 'telegram',
|
|
387
|
+
chatId: chatId,
|
|
388
|
+
type: 'message',
|
|
389
|
+
inbound: false,
|
|
390
|
+
content: content
|
|
391
|
+
};
|
|
392
|
+
if (sourcePayload.userId !== undefined) redbotPayload.userId = sourcePayload.userId;
|
|
393
|
+
|
|
394
|
+
if (confirmation && confirmation.required === true && Array.isArray(confirmation.actions)) {
|
|
395
|
+
redbotPayload.type = 'inline-buttons';
|
|
396
|
+
redbotPayload.buttons = confirmation.actions.map(function (action) {
|
|
397
|
+
return {
|
|
398
|
+
type: 'postback',
|
|
399
|
+
label: action.label,
|
|
400
|
+
value: action.callbackData
|
|
401
|
+
};
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
msg.payload = redbotPayload;
|
|
406
|
+
return msg;`
|
|
407
|
+
}
|
|
408
|
+
]
|
|
409
|
+
|
|
410
|
+
if (root) {
|
|
411
|
+
root.KNXAIChatAdapterMappings = KNXAIChatAdapterMappings
|
|
412
|
+
}
|
|
413
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
414
|
+
module.exports = KNXAIChatAdapterMappings
|
|
415
|
+
}
|
|
416
|
+
})(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this))
|
|
@@ -115,12 +115,14 @@
|
|
|
115
115
|
if (RED && typeof RED.notify === 'function') {
|
|
116
116
|
RED.notify(translate('migration_unavailable', 'The HUE migration tool is unavailable. Restart Node-RED after updating the package.'), 'error')
|
|
117
117
|
}
|
|
118
|
+
if (typeof options.onClose === 'function') options.onClose()
|
|
118
119
|
return function () {}
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
const legacyNodes = migrationApi.collectLegacyHueNodes(RED)
|
|
122
123
|
if (legacyNodes.length === 0) {
|
|
123
124
|
if (typeof RED.notify === 'function') RED.notify(translate('migration_none', 'No legacy HUE nodes were found.'), 'warning')
|
|
125
|
+
if (typeof options.onClose === 'function') options.onClose()
|
|
124
126
|
return function () {}
|
|
125
127
|
}
|
|
126
128
|
|
|
@@ -158,6 +160,7 @@
|
|
|
158
160
|
if ($convert) $convert.off(EVENT_NAMESPACE)
|
|
159
161
|
try { $dialog.dialog('destroy') } catch (error) { /* already detached */ }
|
|
160
162
|
$dialog.remove()
|
|
163
|
+
if (typeof options.onClose === 'function') options.onClose()
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
const performMigration = () => {
|
|
@@ -210,6 +210,7 @@
|
|
|
210
210
|
const t = (key, fallback, values) => translate(RED, key, fallback, values)
|
|
211
211
|
if (legacyNodes.length === 0) {
|
|
212
212
|
RED.notify(t('migration_none', 'No compatible legacy KNX utility nodes were found.'), 'info')
|
|
213
|
+
if (typeof options.onClose === 'function') options.onClose()
|
|
213
214
|
return
|
|
214
215
|
}
|
|
215
216
|
if (activeNotification && typeof activeNotification.close === 'function') activeNotification.close()
|
|
@@ -217,8 +218,10 @@
|
|
|
217
218
|
let closed = false
|
|
218
219
|
let notification
|
|
219
220
|
const close = () => {
|
|
221
|
+
if (closed) return
|
|
220
222
|
closed = true
|
|
221
223
|
if (notification && typeof notification.close === 'function') notification.close()
|
|
224
|
+
if (typeof options.onClose === 'function') options.onClose()
|
|
222
225
|
}
|
|
223
226
|
notification = RED.notify(t('migration_confirm', 'Convert all {{count}} compatible legacy KNX utility nodes in every flow and subflow to KNX Utility? Before conversion, the browser automatically starts downloading a JSON backup of all flows. Protected credentials are excluded, as in the standard Node-RED export. Your browser may ask where to save the file. The current node editor will close and discard unsaved edits. IDs, settings, connections and groups are preserved. Conversion happens in this browser, can be undone, and takes effect when you Deploy.', { count: legacyNodes.length }), {
|
|
224
227
|
modal: true,
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
const api = factory()
|
|
3
|
+
if (typeof module === 'object' && module.exports) module.exports = api
|
|
4
|
+
if (root) root.KNXUltimateLegacyMigrationNotice = api
|
|
5
|
+
}(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const I18N_PREFIX = 'node-red-contrib-knx-ultimate/knxUltimateUtility:knxUltimateUtility.startup_migration.'
|
|
9
|
+
const installations = new WeakMap()
|
|
10
|
+
|
|
11
|
+
function install (RED, options = {}) {
|
|
12
|
+
if (installations.has(RED)) return installations.get(RED)
|
|
13
|
+
const environment = options.environment || globalThis
|
|
14
|
+
const $ = options.$ || environment.jQuery
|
|
15
|
+
const documentObject = environment.document
|
|
16
|
+
let notification
|
|
17
|
+
let timer
|
|
18
|
+
let dismissed = false
|
|
19
|
+
let disposed = false
|
|
20
|
+
let migrating = false
|
|
21
|
+
let closeMigration
|
|
22
|
+
let lastCounts = ''
|
|
23
|
+
|
|
24
|
+
function translate (key, fallback, values = {}) {
|
|
25
|
+
const qualifiedKey = I18N_PREFIX + key
|
|
26
|
+
let result
|
|
27
|
+
try { result = RED._(qualifiedKey, values) } catch (error) { /* use fallback */ }
|
|
28
|
+
result = result && result !== qualifiedKey ? result : fallback
|
|
29
|
+
return result.replace(/{{\s*(\w+)\s*}}/g, (match, name) => values[name] === undefined ? match : String(values[name]))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function closeNotice () {
|
|
33
|
+
if (notification) notification.close()
|
|
34
|
+
notification = undefined
|
|
35
|
+
lastCounts = ''
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Defer scans until the current import, undo or conversion batch finishes.
|
|
39
|
+
// Node events also cover initial loading in Node-RED's safe mode.
|
|
40
|
+
function schedule () {
|
|
41
|
+
if (disposed) return
|
|
42
|
+
if (timer !== undefined) environment.clearTimeout(timer)
|
|
43
|
+
timer = environment.setTimeout(refresh, 150)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function migrationClosed () {
|
|
47
|
+
migrating = false
|
|
48
|
+
closeMigration = undefined
|
|
49
|
+
schedule()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function openMigration (kind) {
|
|
53
|
+
if (disposed || migrating) return
|
|
54
|
+
closeNotice()
|
|
55
|
+
migrating = true
|
|
56
|
+
try {
|
|
57
|
+
if (kind === 'hue') {
|
|
58
|
+
closeMigration = environment.KNXUltimateHueControllerMigrationDialog.open({
|
|
59
|
+
RED, $, windowObject: environment, documentObject, onClose: migrationClosed
|
|
60
|
+
})
|
|
61
|
+
} else {
|
|
62
|
+
closeMigration = environment.KNXUltimateUtilityMigration.migrate(RED, {
|
|
63
|
+
environment, $, onClose: migrationClosed
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
migrationClosed()
|
|
68
|
+
RED.notify(translate('unavailable', 'The migration tool is unavailable. Reload the editor after restarting Node-RED.'), 'error')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function refresh () {
|
|
73
|
+
timer = undefined
|
|
74
|
+
if (disposed || dismissed || migrating) return
|
|
75
|
+
const hueApi = environment.KNXUltimateHueControllerMigration
|
|
76
|
+
const knxApi = environment.KNXUltimateUtilityMigration
|
|
77
|
+
if (!hueApi || !knxApi) return
|
|
78
|
+
const hue = hueApi.collectLegacyHueNodes(RED).length
|
|
79
|
+
const knx = knxApi.collectLegacyUtilityNodes(RED).length
|
|
80
|
+
if (!hue && !knx) {
|
|
81
|
+
closeNotice()
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
const counts = `${hue}:${knx}`
|
|
85
|
+
if (notification && counts === lastCounts) return
|
|
86
|
+
lastCounts = counts
|
|
87
|
+
|
|
88
|
+
const message = $('<div></div>')
|
|
89
|
+
$('<p></p>').append($('<strong></strong>').text(translate('title', 'Legacy KNX Ultimate nodes'))).appendTo(message)
|
|
90
|
+
$('<p></p>').text(translate('message', 'Legacy nodes found in your flows and subflows: HUE: {{hue}}; KNX utility: {{knx}}. These nodes still work, but are hidden from the palette. Migrate them to HUE Controller and KNX Utility. Each migration downloads a flow backup; review the result before deploying.', { hue, knx })).appendTo(message)
|
|
91
|
+
const buttons = []
|
|
92
|
+
if (hue) {
|
|
93
|
+
buttons.push({
|
|
94
|
+
text: translate('hue_button', 'Migrate HUE ({{count}})', { count: hue }),
|
|
95
|
+
click: () => openMigration('hue')
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
if (knx) {
|
|
99
|
+
buttons.push({
|
|
100
|
+
text: translate('knx_button', 'Migrate KNX ({{count}})', { count: knx }),
|
|
101
|
+
click: () => openMigration('knx')
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
buttons.push({
|
|
105
|
+
text: translate('later', 'Later'),
|
|
106
|
+
click () {
|
|
107
|
+
dismissed = true
|
|
108
|
+
closeNotice()
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
const settings = { type: 'warning', fixed: true, modal: false, buttons, silent: true }
|
|
112
|
+
if (notification) notification.update(message, settings)
|
|
113
|
+
else notification = RED.notify(message, settings)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function reset () {
|
|
117
|
+
dismissed = false
|
|
118
|
+
closeNotice()
|
|
119
|
+
if (closeMigration) closeMigration()
|
|
120
|
+
migrating = false
|
|
121
|
+
schedule()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const events = ['nodes:add', 'nodes:remove', 'nodes:change', 'workspace:dirty', 'deploy']
|
|
125
|
+
events.forEach(event => RED.events.on(event, schedule))
|
|
126
|
+
RED.events.on('workspace:clear', reset)
|
|
127
|
+
const dispose = () => {
|
|
128
|
+
disposed = true
|
|
129
|
+
if (timer !== undefined) environment.clearTimeout(timer)
|
|
130
|
+
events.forEach(event => RED.events.off(event, schedule))
|
|
131
|
+
RED.events.off('workspace:clear', reset)
|
|
132
|
+
if (closeMigration) closeMigration()
|
|
133
|
+
closeNotice()
|
|
134
|
+
installations.delete(RED)
|
|
135
|
+
}
|
|
136
|
+
const installation = { dispose }
|
|
137
|
+
installations.set(RED, installation)
|
|
138
|
+
schedule()
|
|
139
|
+
return installation
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { install }
|
|
143
|
+
}))
|