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.
Files changed (73) hide show
  1. package/README.md +65 -0
  2. package/admin/custom/.vite/manifest.json +62 -62
  3. package/admin/custom/assets/Components-BSewCBfS.js +2 -0
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. package/admin/custom/assets/{hostInit-4roC4hEj.js → hostInit-jJe8bIfV.js} +1 -1
  10. package/admin/custom/assets/{lib-CchzgTUs.js → lib-CL8mXvI6.js} +1 -1
  11. package/admin/custom/assets/{mf-entry-bootstrap-0-4806bbe4.js → mf-entry-bootstrap-0-ab227aa6.js} +2 -2
  12. package/admin/custom/assets/{pendingShares-DLlv8yFY.js → pendingShares-wrGtfliQ.js} +1 -1
  13. package/admin/custom/customComponents.js +2 -2
  14. package/admin/custom/i18n/de.json +40 -0
  15. package/admin/custom/i18n/en.json +40 -0
  16. package/admin/custom/i18n/es.json +40 -0
  17. package/admin/custom/i18n/fr.json +40 -0
  18. package/admin/custom/i18n/it.json +40 -0
  19. package/admin/custom/i18n/nl.json +40 -0
  20. package/admin/custom/i18n/pl.json +40 -0
  21. package/admin/custom/i18n/pt.json +40 -0
  22. package/admin/custom/i18n/ru.json +40 -0
  23. package/admin/custom/i18n/uk.json +40 -0
  24. package/admin/custom/i18n/zh-cn.json +40 -0
  25. package/admin/custom/index.html +2 -2
  26. package/admin/custom/mf-manifest.json +1 -1
  27. package/admin/custom/mf-stats.json +1 -1
  28. package/admin/i18n/de.json +13 -3
  29. package/admin/i18n/en.json +13 -3
  30. package/admin/i18n/es.json +13 -3
  31. package/admin/i18n/fr.json +13 -3
  32. package/admin/i18n/it.json +13 -3
  33. package/admin/i18n/nl.json +13 -3
  34. package/admin/i18n/pl.json +13 -3
  35. package/admin/i18n/pt.json +13 -3
  36. package/admin/i18n/ru.json +13 -3
  37. package/admin/i18n/uk.json +13 -3
  38. package/admin/i18n/zh-cn.json +13 -3
  39. package/admin/jsonConfig.json +61 -2
  40. package/build/lib/journal.js +298 -2
  41. package/build/lib/journal.js.map +1 -1
  42. package/build/lib/llm.js +105 -4
  43. package/build/lib/llm.js.map +1 -1
  44. package/build/lib/localLlm.js +47 -10
  45. package/build/lib/localLlm.js.map +1 -1
  46. package/build/lib/nlu.js +124 -5
  47. package/build/lib/nlu.js.map +1 -1
  48. package/build/lib/tools.js +3 -0
  49. package/build/lib/tools.js.map +1 -1
  50. package/build/lib/voice/esphome.js +81 -7
  51. package/build/lib/voice/esphome.js.map +1 -1
  52. package/build/lib/voice/esphomeProto.js.map +1 -1
  53. package/build/lib/voice/mediaServer.js +29 -1
  54. package/build/lib/voice/mediaServer.js.map +1 -1
  55. package/build/lib/voice/protocol.js.map +1 -1
  56. package/build/lib/voice/voiceServer.js +81 -7
  57. package/build/lib/voice/voiceServer.js.map +1 -1
  58. package/build/lib/voice/wakeWords.js +284 -0
  59. package/build/lib/voice/wakeWords.js.map +1 -0
  60. package/build/lib/voice/wyoming.js +12 -4
  61. package/build/lib/voice/wyoming.js.map +1 -1
  62. package/build/lib/weather.js +160 -0
  63. package/build/lib/weather.js.map +1 -1
  64. package/build/lib/youtube.js +625 -0
  65. package/build/lib/youtube.js.map +1 -0
  66. package/build/main.js +1152 -76
  67. package/build/main.js.map +1 -1
  68. package/io-package.json +30 -14
  69. package/package.json +6 -6
  70. package/admin/custom/assets/Components-C6qcpaRC.js +0 -1
  71. 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
  72. /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
  73. /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|буд\p{L}*|разбуд\p{L}*)|alarms?|wecker)(?![\p{L}])/iu;
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
- constructor(rooms, devices, aliases = []) {
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 — but the question may still be about a kind of measurement.
743
- return this.parseCategoryQuery(p, this.extractFeatures(p));
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;