iobroker.assistant 1.0.3 → 1.2.0
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/README.md +65 -0
- package/admin/custom/.vite/manifest.json +62 -62
- package/admin/custom/assets/Components-BSewCBfS.js +2 -0
- package/admin/custom/assets/{_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-A4gakomR.js → _virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-C5NPM0Of.js} +1 -1
- package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-CwqmfMvn.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-IaosaymG.js} +1 -1
- package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-BMuLYe6f.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-Cdyjt5KT.js} +39 -39
- package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare__shared-BxjZmnpc.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare__shared-Dsw5zWS8.js} +1 -1
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-SgSagTOv.js +122 -0
- package/admin/custom/assets/{hostInit-4roC4hEj.js → hostInit-jJe8bIfV.js} +1 -1
- package/admin/custom/assets/{lib-CchzgTUs.js → lib-CL8mXvI6.js} +1 -1
- package/admin/custom/assets/{mf-entry-bootstrap-0-4806bbe4.js → mf-entry-bootstrap-0-ab227aa6.js} +2 -2
- package/admin/custom/assets/{pendingShares-DLlv8yFY.js → pendingShares-wrGtfliQ.js} +1 -1
- package/admin/custom/customComponents.js +2 -2
- package/admin/custom/i18n/de.json +40 -0
- package/admin/custom/i18n/en.json +40 -0
- package/admin/custom/i18n/es.json +40 -0
- package/admin/custom/i18n/fr.json +40 -0
- package/admin/custom/i18n/it.json +40 -0
- package/admin/custom/i18n/nl.json +40 -0
- package/admin/custom/i18n/pl.json +40 -0
- package/admin/custom/i18n/pt.json +40 -0
- package/admin/custom/i18n/ru.json +40 -0
- package/admin/custom/i18n/uk.json +40 -0
- package/admin/custom/i18n/zh-cn.json +40 -0
- package/admin/custom/index.html +2 -2
- package/admin/custom/mf-manifest.json +1 -1
- package/admin/custom/mf-stats.json +1 -1
- package/admin/i18n/de.json +13 -3
- package/admin/i18n/en.json +13 -3
- package/admin/i18n/es.json +13 -3
- package/admin/i18n/fr.json +13 -3
- package/admin/i18n/it.json +13 -3
- package/admin/i18n/nl.json +13 -3
- package/admin/i18n/pl.json +13 -3
- package/admin/i18n/pt.json +13 -3
- package/admin/i18n/ru.json +13 -3
- package/admin/i18n/uk.json +13 -3
- package/admin/i18n/zh-cn.json +13 -3
- package/admin/jsonConfig.json +61 -2
- package/build/lib/journal.js +298 -2
- package/build/lib/journal.js.map +1 -1
- package/build/lib/llm.js +105 -4
- package/build/lib/llm.js.map +1 -1
- package/build/lib/localLlm.js +47 -10
- package/build/lib/localLlm.js.map +1 -1
- package/build/lib/nlu.js +124 -5
- package/build/lib/nlu.js.map +1 -1
- package/build/lib/tools.js +3 -0
- package/build/lib/tools.js.map +1 -1
- package/build/lib/voice/esphome.js +81 -7
- package/build/lib/voice/esphome.js.map +1 -1
- package/build/lib/voice/esphomeProto.js.map +1 -1
- package/build/lib/voice/mediaServer.js +29 -1
- package/build/lib/voice/mediaServer.js.map +1 -1
- package/build/lib/voice/protocol.js.map +1 -1
- package/build/lib/voice/voiceServer.js +81 -7
- package/build/lib/voice/voiceServer.js.map +1 -1
- package/build/lib/voice/wakeWords.js +284 -0
- package/build/lib/voice/wakeWords.js.map +1 -0
- package/build/lib/voice/wyoming.js +12 -4
- package/build/lib/voice/wyoming.js.map +1 -1
- package/build/lib/weather.js +160 -0
- package/build/lib/weather.js.map +1 -1
- package/build/lib/youtube.js +625 -0
- package/build/lib/youtube.js.map +1 -0
- package/build/main.js +1152 -76
- package/build/main.js.map +1 -1
- package/io-package.json +30 -14
- package/package.json +6 -6
- package/admin/custom/assets/Components-C6qcpaRC.js +0 -1
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-TM4l0OYm.js +0 -122
- /package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-BkH7fzDw.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-BkH7fzDw.js} +0 -0
- /package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__-M61rjtmp.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__-M61rjtmp.js} +0 -0
package/build/lib/nlu.js
CHANGED
|
@@ -188,6 +188,39 @@ const CATEGORY_WORDS = [
|
|
|
188
188
|
];
|
|
189
189
|
/** Words that ask about every room at once ("wie warm ist es überall"). */
|
|
190
190
|
const EVERYWHERE_WORDS = ['ueberall', 'alle', 'allen', 'everywhere', 'all', 'везде', 'всюду'];
|
|
191
|
+
/** Words (stemmed) that make a question a weather question on their own. */
|
|
192
|
+
const WEATHER_WORDS = ['wetter', 'wetterbericht', 'vorhersage', 'weather', 'forecast', 'погода', 'прогноз'];
|
|
193
|
+
/**
|
|
194
|
+
* Precipitation words, matched as whole tokens: stemming "regen" would also catch "Regelung", and nobody
|
|
195
|
+
* gives a smart home the order "rain" — these only ever start a question.
|
|
196
|
+
*/
|
|
197
|
+
const RAIN_TOKENS = new Set([
|
|
198
|
+
'regnet',
|
|
199
|
+
'regnen',
|
|
200
|
+
'regen',
|
|
201
|
+
'schneit',
|
|
202
|
+
'schnee',
|
|
203
|
+
'gewitter',
|
|
204
|
+
'rain',
|
|
205
|
+
'raining',
|
|
206
|
+
'snow',
|
|
207
|
+
'snowing',
|
|
208
|
+
'дождь',
|
|
209
|
+
'дождя',
|
|
210
|
+
'снег',
|
|
211
|
+
'гроза',
|
|
212
|
+
]);
|
|
213
|
+
/** A weather question about some other place or a later day is left to the LLM and its `get_weather` tool. */
|
|
214
|
+
const WEATHER_ELSEWHERE = new Set(['in', 'fuer', 'for', 'в', 'во', 'для']);
|
|
215
|
+
const WEATHER_LATER_RE = /(?<![\p{L}])(woche\p{L}*|wochenende|week\p{L}*|недел\p{L}*|выходн\p{L}*|montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag|monday|tuesday|wednesday|thursday|friday|saturday|sunday|понедельник|вторник|среду|четверг|пятницу|субботу|воскресенье)(?![\p{L}])/u;
|
|
216
|
+
/** Words (stemmed) that name the playback volume. */
|
|
217
|
+
const VOLUME_WORDS = ['lautstaerke', 'volume', 'громкость'];
|
|
218
|
+
const LOUDER_TOKENS = new Set(['lauter', 'louder', 'громче', 'погромче']);
|
|
219
|
+
const QUIETER_TOKENS = new Set(['leiser', 'quieter', 'softer', 'тише', 'потише']);
|
|
220
|
+
const VOLUME_MAX_TOKENS = new Set(['maximum', 'max', 'voll', 'full', 'максимум', 'полную']);
|
|
221
|
+
const VOLUME_MIN_TOKENS = new Set(['minimum', 'min', 'минимум']);
|
|
222
|
+
/** Step of a bare "lauter"/"leiser", in percentage points. */
|
|
223
|
+
const VOLUME_STEP = 10;
|
|
191
224
|
/** Words (de/en/ru, stemmed) that mean "open". */
|
|
192
225
|
const OPEN_WORDS = ['offen', 'geoeffnet', 'geoffnet', 'auf', 'open', 'открыт', 'открыта', 'открыты', 'открытые'];
|
|
193
226
|
/** Words (de/en/ru, stemmed) that make a query an on/off question ("is the light on?"). */
|
|
@@ -345,9 +378,10 @@ const LABEL_MARKERS = new Set(['an', 'fuer', 'for', 'to', 'about', 'про']);
|
|
|
345
378
|
/**
|
|
346
379
|
* Marks a command as being about an alarm / wake-up ("Wecker", "weck mich", "alarm", "будильник"). Like
|
|
347
380
|
* {@link TIMER_RE}, "alarms?"/"wecker" carry no leading boundary so compounds ("Küchenwecker") and the
|
|
348
|
-
* plural still match; the verb-prefix forms ("weck…", "разбуд…") keep a boundary.
|
|
381
|
+
* plural still match; the verb-prefix forms ("weck…", "разбуд…") keep a boundary. "буди…" and not just
|
|
382
|
+
* "буд…": that would also catch "будет" (will be), and turn "будет ли дождь?" into an alarm query.
|
|
349
383
|
*/
|
|
350
|
-
const ALARM_RE = /(?:(?<![\p{L}])(weck\p{L}*|wake
|
|
384
|
+
const ALARM_RE = /(?:(?<![\p{L}])(weck\p{L}*|wake|буди\p{L}*|разбуд\p{L}*)|alarms?|wecker)(?![\p{L}])/iu;
|
|
351
385
|
/** Hour number words → digits (no fractions — those are handled by the clock regexes). de/en/ru, 1–12. */
|
|
352
386
|
const HOUR_WORDS = {
|
|
353
387
|
ein: '1',
|
|
@@ -624,9 +658,11 @@ class Nlu {
|
|
|
624
658
|
devices;
|
|
625
659
|
/** Synonym dictionary as normalized [from, to] pairs, applied to the input before matching. */
|
|
626
660
|
aliases;
|
|
627
|
-
|
|
661
|
+
opts;
|
|
662
|
+
constructor(rooms, devices, aliases = [], opts = {}) {
|
|
628
663
|
this.rooms = rooms.filter(Boolean);
|
|
629
664
|
this.devices = devices;
|
|
665
|
+
this.opts = opts;
|
|
630
666
|
this.aliases = aliases
|
|
631
667
|
.map(a => [normalize(a.from || ''), normalize(a.to || '')])
|
|
632
668
|
.filter(([from]) => from);
|
|
@@ -739,8 +775,11 @@ class Nlu {
|
|
|
739
775
|
const roomName = this.findRoom(p.joined);
|
|
740
776
|
const device = this.findDevice(p.joined, roomName);
|
|
741
777
|
if (!device) {
|
|
742
|
-
// No device named —
|
|
743
|
-
|
|
778
|
+
// No device named — so "lauter" is meant for the speaker that heard it, "wie wird das Wetter" goes to
|
|
779
|
+
// the weather adapter, and a question may still be about a kind of measurement. A named device
|
|
780
|
+
// always wins: "Fernseher lauter" is about the TV.
|
|
781
|
+
const features = this.extractFeatures(p);
|
|
782
|
+
return this.parseVolume(p) ?? this.parseWeatherQuery(p, features) ?? this.parseCategoryQuery(p, features);
|
|
744
783
|
}
|
|
745
784
|
let features = this.extractFeatures(p);
|
|
746
785
|
if (inherited && this.namesOnly(p, device, roomName)) {
|
|
@@ -915,6 +954,9 @@ class Nlu {
|
|
|
915
954
|
*/
|
|
916
955
|
parseTimeQuery(text) {
|
|
917
956
|
const has = (w) => wordInText(w, text);
|
|
957
|
+
if (WEATHER_WORDS.some(has)) {
|
|
958
|
+
return null; // "what is the weather the day after tomorrow" asks about the weather, not the date
|
|
959
|
+
}
|
|
918
960
|
const qualifier = () => has('heute') ||
|
|
919
961
|
has('welch') ||
|
|
920
962
|
has('what') ||
|
|
@@ -982,6 +1024,83 @@ class Nlu {
|
|
|
982
1024
|
confidence: 0.8,
|
|
983
1025
|
};
|
|
984
1026
|
}
|
|
1027
|
+
/**
|
|
1028
|
+
* "Wie wird das Wetter morgen?", "regnet es heute?", "wie warm wird es übermorgen?" (de/en/ru) → the
|
|
1029
|
+
* configured weather adapter, read and spoken without an LLM. Only today, tomorrow and the day after
|
|
1030
|
+
* — other days, other places ("Wetter in Berlin") and the plain "wie warm ist es" (the house sensors,
|
|
1031
|
+
* see {@link parseCategoryQuery}) stay with their own paths.
|
|
1032
|
+
*/
|
|
1033
|
+
parseWeatherQuery(p, features) {
|
|
1034
|
+
if (!this.opts.weather) {
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
const t = p.joined;
|
|
1038
|
+
const tokens = p.tokens;
|
|
1039
|
+
const has = (w) => p.tokenSet.has(w);
|
|
1040
|
+
let when = 'now';
|
|
1041
|
+
if (has('uebermorgen') || t.includes('day after tomorrow') || has('послезавтра')) {
|
|
1042
|
+
when = 'dayAfter';
|
|
1043
|
+
}
|
|
1044
|
+
else if (has('heute') || has('today') || has('сегодня')) {
|
|
1045
|
+
when = 'today'; // also "heute Morgen"
|
|
1046
|
+
}
|
|
1047
|
+
else if (has('tomorrow') ||
|
|
1048
|
+
has('завтра') ||
|
|
1049
|
+
tokens.some((w, i) => w === 'morgen' && tokens[i - 1] !== 'guten')) {
|
|
1050
|
+
when = 'tomorrow';
|
|
1051
|
+
}
|
|
1052
|
+
const weatherWord = WEATHER_WORDS.some(w => wordInText(w, t));
|
|
1053
|
+
const rain = tokens.some(w => RAIN_TOKENS.has(w));
|
|
1054
|
+
const temperature = (CATEGORY_WORDS.find(c => c.category === 'temperature')?.words || []).some(w => wordInText(w, t));
|
|
1055
|
+
// A temperature question is about the forecast only when it names a day; without one it is about the house.
|
|
1056
|
+
if (!weatherWord && !rain && !(temperature && features.isQuery && when !== 'now')) {
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
// The filler list drops prepositions from the tokens, so look for them in the full text.
|
|
1060
|
+
if (WEATHER_LATER_RE.test(t) || p.norm.split(/\s+/).some(w => WEATHER_ELSEWHERE.has(w))) {
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
return { action: 'weatherQuery', when, confidence: 0.85 };
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* "Lautstärke 5", "Lautstärke auf 40 Prozent", "lauter", "etwas leiser", "volume 7", "громче" → the
|
|
1067
|
+
* playback volume of the satellite that heard it (or of the named room's satellite). A bare number up
|
|
1068
|
+
* to 10 is a step on a 1–10 scale, anything above is percent. "Wie ist die Lautstärke" is not a command.
|
|
1069
|
+
*/
|
|
1070
|
+
parseVolume(p) {
|
|
1071
|
+
const louder = p.tokens.some(w => LOUDER_TOKENS.has(w));
|
|
1072
|
+
const quieter = p.tokens.some(w => QUIETER_TOKENS.has(w));
|
|
1073
|
+
const volumeWord = VOLUME_WORDS.some(w => wordInText(w, p.joined));
|
|
1074
|
+
if (!louder && !quieter && !volumeWord) {
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
const percent = /(?:prozent|percent|процент\p{L}*|%)/u.test(p.norm);
|
|
1078
|
+
// "ein bisschen lauter" is no step count — only digits and the number words from two upwards count.
|
|
1079
|
+
const num = p.tokens
|
|
1080
|
+
.map(w => (/^\d+$/.test(w) ? w : ['ein', 'eine', 'einen'].includes(w) ? '' : NUMBER_WORDS[w] || ''))
|
|
1081
|
+
.find(w => /^\d+$/.test(w));
|
|
1082
|
+
const n = num === undefined ? null : parseInt(num, 10);
|
|
1083
|
+
const scaled = n === null ? null : n <= 10 && !percent ? n * 10 : n;
|
|
1084
|
+
const room = this.findRoom(p.joined) || undefined;
|
|
1085
|
+
if (louder !== quieter) {
|
|
1086
|
+
const step = scaled ?? VOLUME_STEP;
|
|
1087
|
+
return { action: 'volumeSet', delta: louder ? step : -step, room, confidence: 0.85 };
|
|
1088
|
+
}
|
|
1089
|
+
if (!volumeWord) {
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
let level = scaled;
|
|
1093
|
+
if (p.tokens.some(w => VOLUME_MAX_TOKENS.has(w))) {
|
|
1094
|
+
level = 100;
|
|
1095
|
+
}
|
|
1096
|
+
else if (p.tokens.some(w => VOLUME_MIN_TOKENS.has(w))) {
|
|
1097
|
+
level = VOLUME_STEP;
|
|
1098
|
+
}
|
|
1099
|
+
if (level === null) {
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
return { action: 'volumeSet', value: Math.min(100, Math.max(0, level)), room, confidence: 0.85 };
|
|
1103
|
+
}
|
|
985
1104
|
parseWindowsOpen(text) {
|
|
986
1105
|
if (!WINDOW_WORDS.some(w => wordInText(w, text)) || !OPEN_WORDS.some(w => wordInText(w, text))) {
|
|
987
1106
|
return null;
|