iobroker.assistant 0.1.2 → 0.1.6

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 (61) hide show
  1. package/README.md +15 -2
  2. package/admin/custom/.vite/manifest.json +38 -61
  3. package/admin/custom/assets/Components-xWdtqvTH.js +1 -0
  4. package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-dybYZgsu.js +1 -0
  5. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-BnSUsPHa.js +1 -0
  6. package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-BEFjTs9S.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-DlSVT49A.js} +1 -1
  7. package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-C4r3udVz.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-Cfsgderv.js} +78 -78
  8. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_json_mf_2_config__loadShare__.js-ByJf4zv_.js +1494 -0
  9. package/admin/custom/assets/hostInit-CZawMH9B.js +1 -0
  10. package/admin/custom/assets/{index-B8wW0kQx.js → index-JB7g1jgx.js} +1 -1
  11. package/admin/custom/assets/{lib-DpE8sc4h.js → lib-bB-LHRhZ.js} +1 -1
  12. package/admin/custom/assets/{mf-entry-bootstrap-0-e7fa582d.js → mf-entry-bootstrap-0-87f15505.js} +7 -2
  13. package/admin/custom/assets/virtualExposes-brKKZvi0.js +1 -0
  14. package/admin/custom/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__ConfigCustomAssistant__customComponents_js-CRhwj12f.js +1 -0
  15. package/admin/custom/assets/virtual_mf-exposes___mfe_internal__ConfigCustomAssistant__customComponents_js-C5XHUc1J.js +2 -0
  16. package/admin/custom/customComponents.js +12 -1
  17. package/admin/custom/i18n/de.json +23 -23
  18. package/admin/custom/i18n/en.json +23 -23
  19. package/admin/custom/i18n/es.json +24 -0
  20. package/admin/custom/i18n/fr.json +24 -0
  21. package/admin/custom/i18n/it.json +24 -0
  22. package/admin/custom/i18n/nl.json +24 -0
  23. package/admin/custom/i18n/pl.json +24 -0
  24. package/admin/custom/i18n/pt.json +24 -0
  25. package/admin/custom/i18n/ru.json +24 -0
  26. package/admin/custom/i18n/uk.json +24 -0
  27. package/admin/custom/i18n/zh-cn.json +24 -0
  28. package/admin/custom/index.html +2 -2
  29. package/admin/custom/mf-manifest.json +1 -1
  30. package/admin/custom/mf-stats.json +1 -1
  31. package/admin/i18n/de.json +21 -21
  32. package/admin/i18n/en.json +21 -21
  33. package/admin/i18n/es.json +24 -0
  34. package/admin/i18n/fr.json +24 -0
  35. package/admin/i18n/it.json +24 -0
  36. package/admin/i18n/nl.json +24 -0
  37. package/admin/i18n/pl.json +24 -0
  38. package/admin/i18n/pt.json +24 -0
  39. package/admin/i18n/ru.json +24 -0
  40. package/admin/i18n/uk.json +24 -0
  41. package/admin/i18n/zh-cn.json +24 -0
  42. package/build/lib/credentials.js +38 -8
  43. package/build/lib/credentials.js.map +1 -1
  44. package/build/lib/nlu.js +173 -27
  45. package/build/lib/nlu.js.map +1 -1
  46. package/build/lib/weather.js +132 -0
  47. package/build/lib/weather.js.map +1 -1
  48. package/build/main.js +129 -25
  49. package/build/main.js.map +1 -1
  50. package/io-package.json +28 -3
  51. package/package.json +6 -6
  52. package/admin/custom/assets/Components-DcnuVFM6.js +0 -1
  53. package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-DogEjv3U.js +0 -1
  54. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-Dw5TkU1t.js +0 -1
  55. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_json_mf_2_config__loadShare__.js-DlOBHPL0.js +0 -1494
  56. package/admin/custom/assets/hostInit-DKMI-sLp.js +0 -1
  57. package/admin/custom/assets/virtualExposes-DOTO22UK.js +0 -1
  58. package/admin/custom/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__ConfigCustomAssistant__customComponents_js-CQa59yVi.js +0 -1
  59. package/admin/custom/assets/virtual_mf-exposes-ssr___mfe_internal__ConfigCustomAssistant__customComponents_js-9g2Fj8tk.js +0 -2
  60. package/admin/custom/assets/virtual_mf-exposes___mfe_internal__ConfigCustomAssistant__customComponents_js-BTUQTbYo.js +0 -2
  61. package/admin/custom/customComponents.ssr.js +0 -1
@@ -1,7 +1,37 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
5
35
  Object.defineProperty(exports, "__esModule", { value: true });
6
36
  exports.readCentralKey = readCentralKey;
7
37
  exports.resolveApiKey = resolveApiKey;
@@ -20,18 +50,18 @@ exports.resolveVoiceCredentials = resolveVoiceCredentials;
20
50
  * js-controller >= 7.2 / a recent \@iobroker/adapter-core. We access it defensively so the
21
51
  * adapter still compiles and runs (in `manual` mode) on older controllers.
22
52
  */
23
- const credentials_1 = __importDefault(require("@iobroker/adapter-core/credentials"));
53
+ const Credentials = __importStar(require("@iobroker/adapter-core/credentials"));
24
54
  /** Read and decrypt a single credential's key from the central store; '' (and a warning) on error. */
25
55
  async function readCentralKey(adapter, id) {
26
56
  if (!id) {
27
57
  return '';
28
58
  }
29
- if (!credentials_1.default?.getCredentials) {
59
+ if (!Credentials?.getCredentials) {
30
60
  adapter.log.warn(`Cannot read credential "${id}": the central Credentials API needs js-controller >= 7.2`);
31
61
  return '';
32
62
  }
33
63
  try {
34
- const cred = await credentials_1.default.getCredentials(adapter, id);
64
+ const cred = await Credentials.getCredentials(adapter, id);
35
65
  return (cred?.values?.key || '').trim();
36
66
  }
37
67
  catch (e) {
@@ -57,12 +87,12 @@ async function readCentralValues(adapter, id) {
57
87
  if (!id) {
58
88
  return {};
59
89
  }
60
- if (!credentials_1.default?.getCredentials) {
90
+ if (!Credentials?.getCredentials) {
61
91
  adapter.log.warn(`Cannot read credential "${id}": the central Credentials API needs js-controller >= 7.2`);
62
92
  return {};
63
93
  }
64
94
  try {
65
- const cred = await credentials_1.default.getCredentials(adapter, id);
95
+ const cred = await Credentials.getCredentials(adapter, id);
66
96
  return cred?.values || {};
67
97
  }
68
98
  catch (e) {
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/lib/credentials.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Resolve the LLM API key from the adapter config.\n *\n * Two modes (`config.credentialType`):\n * - 'manual': the key is stored directly in the (encrypted) adapter config (`config.apiKey`).\n * - 'manager': the config only stores the ID of a credential in the central ioBroker\n * credential store (`system.credentials.*`); the real key is resolved at runtime\n * via the Credentials API.\n *\n * The central Credentials API (`Credentials.getCredentials`) is only available with\n * js-controller >= 7.2 / a recent \\@iobroker/adapter-core. We access it defensively so the\n * adapter still compiles and runs (in `manual` mode) on older controllers.\n */\nimport Credentials from '@iobroker/adapter-core/credentials';\nimport type { AdapterConfig } from '../types';\nimport type { VoiceCredentials } from './voice/engines';\n\n/** Read and decrypt a single credential's key from the central store; '' (and a warning) on error. */\nexport async function readCentralKey(adapter: ioBroker.Adapter, id: string): Promise<string> {\n if (!id) {\n return '';\n }\n if (!Credentials?.getCredentials) {\n adapter.log.warn(`Cannot read credential \"${id}\": the central Credentials API needs js-controller >= 7.2`);\n return '';\n }\n try {\n const cred = await Credentials.getCredentials(adapter, id);\n return ((cred?.values?.key as string) || '').trim();\n } catch (e) {\n adapter.log.warn(`Cannot read credential \"${id}\": ${(e as Error).message}`);\n return '';\n }\n}\n\n/**\n * Resolve the effective API key for the configured provider.\n * `overrideKey`/`overrideCredentialId` win over the stored config (used by the settings-dialog\n * Test button, where unsaved form values should be used).\n */\nexport async function resolveApiKey(\n adapter: ioBroker.Adapter,\n config: AdapterConfig,\n override?: { credentialType?: 'manual' | 'manager'; apiKey?: string; credentialId?: string },\n): Promise<string> {\n const mode = override?.credentialType || config.credentialType || 'manual';\n if (mode === 'manager') {\n const id = (override?.credentialId || config.credentialIdApiKey || '').trim();\n return readCentralKey(adapter, id);\n }\n return (override?.apiKey || config.apiKey || '').trim();\n}\n\n/** Read and decrypt all fields of a credential from the central store; {} (and a warning) on error. */\nexport async function readCentralValues(adapter: ioBroker.Adapter, id: string): Promise<Record<string, unknown>> {\n if (!id) {\n return {};\n }\n if (!Credentials?.getCredentials) {\n adapter.log.warn(`Cannot read credential \"${id}\": the central Credentials API needs js-controller >= 7.2`);\n return {};\n }\n try {\n const cred = await Credentials.getCredentials(adapter, id);\n return cred?.values || {};\n } catch (e) {\n adapter.log.warn(`Cannot read credential \"${id}\": ${(e as Error).message}`);\n return {};\n }\n}\n\n/** Safe stringify of a credential/config field (values are strings/numbers/booleans). */\nfunction str(v: unknown): string {\n if (typeof v === 'string') {\n return v.trim();\n }\n if (typeof v === 'number' || typeof v === 'boolean') {\n return String(v);\n }\n return '';\n}\n\n/**\n * Resolve the speech (STT/TTS) credentials for OpenAI, Azure and AWS.\n * - 'manual': the encrypted key/region config fields.\n * - 'manager': the central credential store, keyed by the *CredentialId fields\n * (azure values: { key, region }; aws values: { accessKeyId, secretAccessKey, region }).\n * `mainApiKey` is reused for OpenAI speech when no dedicated OpenAI speech credential is set and the\n * LLM provider is already OpenAI.\n */\nexport async function resolveVoiceCredentials(\n adapter: ioBroker.Adapter,\n config: AdapterConfig,\n mainApiKey: string,\n): Promise<VoiceCredentials> {\n const openaiFallback = config.provider === 'openai' ? mainApiKey : '';\n\n if (config.voiceCredentialType === 'manager') {\n const openaiKey = config.voiceCredentialId\n ? await readCentralKey(adapter, config.voiceCredentialId)\n : openaiFallback;\n const azure = config.azureCredentialId ? await readCentralValues(adapter, config.azureCredentialId) : {};\n const aws = config.awsCredentialId ? await readCentralValues(adapter, config.awsCredentialId) : {};\n return {\n openaiKey,\n azureKey: str(azure.key),\n azureRegion: str(azure.region),\n aws: {\n accessKeyId: str(aws.accessKeyId),\n secretAccessKey: str(aws.secretAccessKey),\n region: str(aws.region),\n },\n };\n }\n\n return {\n openaiKey: str(config.voiceApiKey) || openaiFallback,\n azureKey: str(config.azureSpeechKey),\n azureRegion: str(config.azureSpeechRegion),\n aws: {\n accessKeyId: str(config.awsAccessKeyId),\n secretAccessKey: str(config.awsSecretAccessKey),\n region: str(config.awsRegion),\n },\n };\n}\n"]}
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/lib/credentials.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Resolve the LLM API key from the adapter config.\n *\n * Two modes (`config.credentialType`):\n * - 'manual': the key is stored directly in the (encrypted) adapter config (`config.apiKey`).\n * - 'manager': the config only stores the ID of a credential in the central ioBroker\n * credential store (`system.credentials.*`); the real key is resolved at runtime\n * via the Credentials API.\n *\n * The central Credentials API (`Credentials.getCredentials`) is only available with\n * js-controller >= 7.2 / a recent \\@iobroker/adapter-core. We access it defensively so the\n * adapter still compiles and runs (in `manual` mode) on older controllers.\n */\nimport * as Credentials from '@iobroker/adapter-core/credentials';\nimport type { AdapterConfig } from '../types';\nimport type { VoiceCredentials } from './voice/engines';\n\n/** Read and decrypt a single credential's key from the central store; '' (and a warning) on error. */\nexport async function readCentralKey(adapter: ioBroker.Adapter, id: string): Promise<string> {\n if (!id) {\n return '';\n }\n if (!Credentials?.getCredentials) {\n adapter.log.warn(`Cannot read credential \"${id}\": the central Credentials API needs js-controller >= 7.2`);\n return '';\n }\n try {\n const cred = await Credentials.getCredentials(adapter, id);\n return ((cred?.values?.key as string) || '').trim();\n } catch (e) {\n adapter.log.warn(`Cannot read credential \"${id}\": ${(e as Error).message}`);\n return '';\n }\n}\n\n/**\n * Resolve the effective API key for the configured provider.\n * `overrideKey`/`overrideCredentialId` win over the stored config (used by the settings-dialog\n * Test button, where unsaved form values should be used).\n */\nexport async function resolveApiKey(\n adapter: ioBroker.Adapter,\n config: AdapterConfig,\n override?: { credentialType?: 'manual' | 'manager'; apiKey?: string; credentialId?: string },\n): Promise<string> {\n const mode = override?.credentialType || config.credentialType || 'manual';\n if (mode === 'manager') {\n const id = (override?.credentialId || config.credentialIdApiKey || '').trim();\n return readCentralKey(adapter, id);\n }\n return (override?.apiKey || config.apiKey || '').trim();\n}\n\n/** Read and decrypt all fields of a credential from the central store; {} (and a warning) on error. */\nexport async function readCentralValues(adapter: ioBroker.Adapter, id: string): Promise<Record<string, unknown>> {\n if (!id) {\n return {};\n }\n if (!Credentials?.getCredentials) {\n adapter.log.warn(`Cannot read credential \"${id}\": the central Credentials API needs js-controller >= 7.2`);\n return {};\n }\n try {\n const cred = await Credentials.getCredentials(adapter, id);\n return cred?.values || {};\n } catch (e) {\n adapter.log.warn(`Cannot read credential \"${id}\": ${(e as Error).message}`);\n return {};\n }\n}\n\n/** Safe stringify of a credential/config field (values are strings/numbers/booleans). */\nfunction str(v: unknown): string {\n if (typeof v === 'string') {\n return v.trim();\n }\n if (typeof v === 'number' || typeof v === 'boolean') {\n return String(v);\n }\n return '';\n}\n\n/**\n * Resolve the speech (STT/TTS) credentials for OpenAI, Azure and AWS.\n * - 'manual': the encrypted key/region config fields.\n * - 'manager': the central credential store, keyed by the *CredentialId fields\n * (azure values: { key, region }; aws values: { accessKeyId, secretAccessKey, region }).\n * `mainApiKey` is reused for OpenAI speech when no dedicated OpenAI speech credential is set and the\n * LLM provider is already OpenAI.\n */\nexport async function resolveVoiceCredentials(\n adapter: ioBroker.Adapter,\n config: AdapterConfig,\n mainApiKey: string,\n): Promise<VoiceCredentials> {\n const openaiFallback = config.provider === 'openai' ? mainApiKey : '';\n\n if (config.voiceCredentialType === 'manager') {\n const openaiKey = config.voiceCredentialId\n ? await readCentralKey(adapter, config.voiceCredentialId)\n : openaiFallback;\n const azure = config.azureCredentialId ? await readCentralValues(adapter, config.azureCredentialId) : {};\n const aws = config.awsCredentialId ? await readCentralValues(adapter, config.awsCredentialId) : {};\n return {\n openaiKey,\n azureKey: str(azure.key),\n azureRegion: str(azure.region),\n aws: {\n accessKeyId: str(aws.accessKeyId),\n secretAccessKey: str(aws.secretAccessKey),\n region: str(aws.region),\n },\n };\n }\n\n return {\n openaiKey: str(config.voiceApiKey) || openaiFallback,\n azureKey: str(config.azureSpeechKey),\n azureRegion: str(config.azureSpeechRegion),\n aws: {\n accessKeyId: str(config.awsAccessKeyId),\n secretAccessKey: str(config.awsSecretAccessKey),\n region: str(config.awsRegion),\n },\n };\n}\n"]}
package/build/lib/nlu.js CHANGED
@@ -16,6 +16,7 @@ exports.parseWeekdays = parseWeekdays;
16
16
  exports.parseDurationSeconds = parseDurationSeconds;
17
17
  exports.isStopCommand = isStopCommand;
18
18
  exports.normalize = normalize;
19
+ exports.splitCommands = splitCommands;
19
20
  const TURN_ON = new Set([
20
21
  // de/en
21
22
  'an',
@@ -521,6 +522,63 @@ function wordInText(word, text) {
521
522
  return text.includes(stem);
522
523
  }
523
524
  }
525
+ /**
526
+ * Directionless command verbs: they mark an imperative but carry no on/off/level of their own. A segment
527
+ * holding nothing but such a verb and a device name ("schalte das Licht" in "schalte das Licht und die
528
+ * Lampe an") may therefore borrow the action from its sibling segment.
529
+ */
530
+ const NEUTRAL_VERBS = new Set([
531
+ // de
532
+ 'schalte',
533
+ 'schalt',
534
+ 'schalten',
535
+ 'stelle',
536
+ 'stell',
537
+ 'stellen',
538
+ 'setze',
539
+ 'setz',
540
+ 'setzen',
541
+ 'drehe',
542
+ 'dreh',
543
+ 'fahre',
544
+ 'fahr',
545
+ 'mache',
546
+ 'machen',
547
+ // en
548
+ 'set',
549
+ 'put',
550
+ 'turn',
551
+ 'switch',
552
+ 'change',
553
+ 'adjust',
554
+ 'make',
555
+ // ru
556
+ 'поставь',
557
+ 'установи',
558
+ 'выстави',
559
+ 'сделай',
560
+ ]);
561
+ /** Conjunctions that join two commands in one utterance (de/en/ru). */
562
+ const CONJUNCTIONS = 'und|sowie|dann|danach|and|then|plus|и|затем|потом';
563
+ /**
564
+ * Separators between two commands: a run of conjunctions ("und dann"), a comma or a semicolon. A comma
565
+ * directly followed by a digit is NOT a separator (decimal values like "50,5 %").
566
+ */
567
+ const SPLIT_RE = new RegExp(`\\s*[;,](?!\\d)\\s*|\\s+(?:${CONJUNCTIONS})(?:\\s+(?:${CONJUNCTIONS}))*\\s+`, 'giu');
568
+ /**
569
+ * Split a combined command ("Schalte A an und setze B auf 30 %") into its parts; a single-element array
570
+ * when there is nothing to split.
571
+ */
572
+ function splitCommands(text) {
573
+ return text
574
+ .split(SPLIT_RE)
575
+ .map(s => s.trim())
576
+ .filter(Boolean);
577
+ }
578
+ /** True when a segment states no action at all ("… und die Lampe") and may inherit its sibling's. */
579
+ function isBare(f) {
580
+ return !f.action && f.level === null && !f.color && !f.isQuery;
581
+ }
524
582
  class Nlu {
525
583
  rooms;
526
584
  devices;
@@ -550,52 +608,139 @@ class Nlu {
550
608
  }
551
609
  return out;
552
610
  }
553
- /** Parse a command; returns a structured intent or null if nothing could be resolved confidently. */
611
+ /** Parse a single command; returns a structured intent or null if nothing could be resolved confidently. */
554
612
  parse(text) {
555
- const raw = text;
613
+ const p = this.prepare(text);
614
+ return p ? this.parsePrepared(p, null) : null;
615
+ }
616
+ /**
617
+ * Parse a possibly **combined** command into the intents to execute, in order:
618
+ * - several commands joined by a conjunction — "Schalte A an und setze B auf 30 %",
619
+ * - one verb driving several devices — "Schalte A und B an", where the segment that names only a
620
+ * device inherits its sibling's action.
621
+ * Falls back to the single-command reading unless the split produces at least two intents, so a device
622
+ * or room name containing "und"/"and" — or "1 Stunde und 30 Minuten" — still parses as one command.
623
+ */
624
+ parseAll(text) {
625
+ const combined = this.parseCombined(text);
626
+ if (combined.length > 1) {
627
+ return combined;
628
+ }
629
+ const single = this.parse(text);
630
+ return single ? [single] : [];
631
+ }
632
+ /** Segment-wise parse of a combined command; `[]` unless it yields at least two intents. */
633
+ parseCombined(text) {
634
+ const segments = splitCommands(text);
635
+ if (segments.length < 2) {
636
+ return [];
637
+ }
638
+ const parts = [];
639
+ for (const segment of segments) {
640
+ const p = this.prepare(segment);
641
+ if (p) {
642
+ parts.push(p);
643
+ }
644
+ }
645
+ if (parts.length < 2) {
646
+ return [];
647
+ }
648
+ const features = parts.map(p => this.extractFeatures(p));
649
+ const intents = [];
650
+ for (let i = 0; i < parts.length; i++) {
651
+ // A segment stating no action borrows the nearest sibling's — looking ahead first, because the
652
+ // verb usually comes last in this shape ("schalte A | und B **an**").
653
+ let inherited = null;
654
+ if (isBare(features[i])) {
655
+ inherited = features.slice(i + 1).find(f => !isBare(f)) || null;
656
+ for (let j = i - 1; !inherited && j >= 0; j--) {
657
+ inherited = isBare(features[j]) ? null : features[j];
658
+ }
659
+ }
660
+ const intent = this.parsePrepared(parts[i], inherited);
661
+ if (intent) {
662
+ intents.push(intent);
663
+ }
664
+ }
665
+ return intents.length > 1 ? intents : [];
666
+ }
667
+ /** Normalized views of a command (aliases applied, filler dropped); null when nothing remains to match. */
668
+ prepare(text) {
556
669
  const norm = this.applyAliases(normalize(text.replace(STRIP, ' ')));
557
670
  const tokens = norm.split(/\s+/).filter(t => t && !FILLER.has(t));
558
671
  if (!tokens.length) {
559
672
  return null;
560
673
  }
561
- const joined = tokens.join(' ');
562
- const tokenSet = new Set(tokens);
674
+ return { raw: text, norm, tokens, joined: tokens.join(' '), tokenSet: new Set(tokens) };
675
+ }
676
+ /**
677
+ * Resolve one prepared command. Device-independent intents (timer/alarm/time/aggregate query) are tried
678
+ * first and never inherit anything; `inherited` supplies the action for a segment that names only a
679
+ * device (see {@link parseCombined}).
680
+ */
681
+ parsePrepared(p, inherited) {
563
682
  // Timer / alarm intents ("timer 5 minuten", "weck mich um 7 Uhr", "wie lange noch") — before
564
683
  // window/device matching, and device-independent.
565
- const schedule = this.parseSchedule(raw, norm, joined);
684
+ const schedule = this.parseSchedule(p.raw, p.norm, p.joined);
566
685
  if (schedule) {
567
686
  return schedule;
568
687
  }
569
688
  // Time / date query ("what time is it", "which day is today") — device-independent, answered from
570
689
  // the host clock. After the schedule parse so a clock phrase with a timer/alarm keyword still wins.
571
- const timeQuery = this.parseTimeQuery(joined);
690
+ const timeQuery = this.parseTimeQuery(p.joined);
572
691
  if (timeQuery) {
573
692
  return timeQuery;
574
693
  }
575
694
  // Aggregate query first ("which windows are open") — before single-device matching.
576
- const windowsQuery = this.parseWindowsOpen(joined);
695
+ const windowsQuery = this.parseWindowsOpen(p.joined);
577
696
  if (windowsQuery) {
578
697
  return windowsQuery;
579
698
  }
580
- const roomName = this.findRoom(joined);
581
- const device = this.findDevice(joined, roomName);
699
+ const roomName = this.findRoom(p.joined);
700
+ const device = this.findDevice(p.joined, roomName);
582
701
  if (!device) {
583
702
  return null; // no device → let the LLM handle it
584
703
  }
585
- const action = this.findAction(tokenSet); // 'on' | 'off' | null
586
- const level = this.findLevel(norm); // number | null
587
- const color = this.findColor(joined);
588
- const isQuery = tokens.some(t => QUERY_WORDS.has(t)) || raw.trim().endsWith('?');
589
- // A question ("ist das Licht an?") must never trigger a write — only the query branch below.
704
+ let features = this.extractFeatures(p);
705
+ if (inherited && this.namesOnly(p, device, roomName)) {
706
+ features = inherited;
707
+ }
708
+ return this.buildDeviceIntent(device, roomName, features);
709
+ }
710
+ /** The action half of a command: on/off, level, color, and whether it is a question. */
711
+ extractFeatures(p) {
712
+ return {
713
+ action: this.findAction(p.tokenSet), // 'on' | 'off' | null
714
+ level: this.findLevel(p.norm),
715
+ color: this.findColor(p.joined),
716
+ // A question ("ist das Licht an?") must never trigger a write — only the query branch does.
717
+ isQuery: p.tokens.some(t => QUERY_WORDS.has(t)) || p.raw.trim().endsWith('?'),
718
+ onOffQuery: ON_OFF_QUERY_WORDS.some(w => wordInText(w, p.joined)),
719
+ };
720
+ }
721
+ /**
722
+ * True when the segment holds nothing but the device name, its room and a directionless verb — the only
723
+ * case in which borrowing a sibling's action is safe. Any unmatched word ("mach die Musik *lauter*")
724
+ * means the segment carries an intent of its own that the rules do not cover: leave it to the LLM.
725
+ */
726
+ namesOnly(p, device, roomName) {
727
+ const known = [device.name, roomName]
728
+ .filter(Boolean)
729
+ .flatMap(n => normalize(n).split(/\s+/))
730
+ .filter(Boolean);
731
+ return p.tokens.every(t => NEUTRAL_VERBS.has(t) || known.some(w => wordInText(w, t) || wordInText(t, w)));
732
+ }
733
+ /** Turn a resolved device plus the command's action half into an intent (branches in priority order). */
734
+ buildDeviceIntent(device, roomName, f) {
590
735
  // 1. Set level (dimmer/blind …) — needs a numeric level-like control.
591
- if (level !== null && !isQuery) {
736
+ if (f.level !== null && !f.isQuery) {
592
737
  const stateId = this.pickControl(device, ['level', 'brightness', 'dimmer'], true, 'number');
593
738
  if (stateId) {
594
739
  const intent = {
595
740
  action: 'level',
596
741
  device,
597
742
  stateId,
598
- value: level,
743
+ value: f.level,
599
744
  room: roomName,
600
745
  confidence: 0.9,
601
746
  };
@@ -604,30 +749,30 @@ class Nlu {
604
749
  // unaffected. The switch is found by role/type, not by name (not every device names it ON_SET).
605
750
  const sw = this.findSwitch(device, stateId);
606
751
  if (sw) {
607
- intent.also = { stateId: sw, value: level > 0 };
752
+ intent.also = { stateId: sw, value: f.level > 0 };
608
753
  }
609
754
  return intent;
610
755
  }
611
756
  }
612
757
  // 2. Set color — only a hex color on a color-capable device (warm/cold color-temp is out of scope for v1).
613
- if (color && color.startsWith('#') && COLOR_TYPES.has(device.type) && !isQuery) {
758
+ if (f.color && f.color.startsWith('#') && COLOR_TYPES.has(device.type) && !f.isQuery) {
614
759
  const stateId = this.pickControl(device, ['rgb', 'color', 'hue', 'cie', 'ct'], true);
615
760
  if (stateId) {
616
- return { action: 'color', device, stateId, value: color, room: roomName, confidence: 0.85 };
761
+ return { action: 'color', device, stateId, value: f.color, room: roomName, confidence: 0.85 };
617
762
  }
618
763
  }
619
764
  // 3. Turn on / off — prefer a writable boolean switch (never a metering state like CONSUMPTION).
620
- if (action && !isQuery) {
765
+ if (f.action && !f.isQuery) {
621
766
  const stateId = this.pickControl(device, ['power', 'switch', 'on', 'level'], true, 'boolean');
622
767
  if (stateId) {
623
768
  // If only a numeric (dimmer/level) control is available, on/off means full/zero level, not a
624
769
  // boolean 1/0 (writing `true` to a 0–100 state would land on ~1%).
625
770
  const numeric = this.valueTypeOf(device, stateId) === 'number';
626
771
  return {
627
- action,
772
+ action: f.action,
628
773
  device,
629
774
  stateId,
630
- value: numeric ? (action === 'on' ? 100 : 0) : action === 'on',
775
+ value: numeric ? (f.action === 'on' ? 100 : 0) : f.action === 'on',
631
776
  room: roomName,
632
777
  confidence: 0.9,
633
778
  };
@@ -635,10 +780,11 @@ class Nlu {
635
780
  }
636
781
  // 4. Status query — read the right control: an on/off question ("is the light on?") reads the
637
782
  // boolean switch/feedback; a value question ("what temperature?") reads the actual value.
638
- if (isQuery) {
639
- const onOff = ON_OFF_QUERY_WORDS.some(w => wordInText(w, joined));
640
- const order = onOff ? ['power', 'switch', 'on', 'actual', 'level'] : ['actual', 'value', 'level', 'power'];
641
- const stateId = this.pickControl(device, order, false, onOff ? 'boolean' : undefined);
783
+ if (f.isQuery) {
784
+ const order = f.onOffQuery
785
+ ? ['power', 'switch', 'on', 'actual', 'level']
786
+ : ['actual', 'value', 'level', 'power'];
787
+ const stateId = this.pickControl(device, order, false, f.onOffQuery ? 'boolean' : undefined);
642
788
  if (stateId) {
643
789
  return { action: 'query', device, stateId, room: roomName, confidence: 0.7 };
644
790
  }
@@ -844,7 +990,7 @@ class Nlu {
844
990
  return null;
845
991
  }
846
992
  findLevel(text) {
847
- const m = text.match(/(\d+(?:[.,]\d+)?)\s*(?:prozent|процент\w*|%)/u);
993
+ const m = text.match(/(\d+(?:[.,]\d+)?)\s*(?:prozent|percent|процент\w*|%)/u);
848
994
  return m ? parseFloat(m[1].replace(',', '.')) : null;
849
995
  }
850
996
  findColor(text) {