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.
- package/README.md +15 -2
- package/admin/custom/.vite/manifest.json +38 -61
- package/admin/custom/assets/Components-xWdtqvTH.js +1 -0
- package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-dybYZgsu.js +1 -0
- 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
- 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
- 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
- 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
- package/admin/custom/assets/hostInit-CZawMH9B.js +1 -0
- package/admin/custom/assets/{index-B8wW0kQx.js → index-JB7g1jgx.js} +1 -1
- package/admin/custom/assets/{lib-DpE8sc4h.js → lib-bB-LHRhZ.js} +1 -1
- package/admin/custom/assets/{mf-entry-bootstrap-0-e7fa582d.js → mf-entry-bootstrap-0-87f15505.js} +7 -2
- package/admin/custom/assets/virtualExposes-brKKZvi0.js +1 -0
- package/admin/custom/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__ConfigCustomAssistant__customComponents_js-CRhwj12f.js +1 -0
- package/admin/custom/assets/virtual_mf-exposes___mfe_internal__ConfigCustomAssistant__customComponents_js-C5XHUc1J.js +2 -0
- package/admin/custom/customComponents.js +12 -1
- package/admin/custom/i18n/de.json +23 -23
- package/admin/custom/i18n/en.json +23 -23
- package/admin/custom/i18n/es.json +24 -0
- package/admin/custom/i18n/fr.json +24 -0
- package/admin/custom/i18n/it.json +24 -0
- package/admin/custom/i18n/nl.json +24 -0
- package/admin/custom/i18n/pl.json +24 -0
- package/admin/custom/i18n/pt.json +24 -0
- package/admin/custom/i18n/ru.json +24 -0
- package/admin/custom/i18n/uk.json +24 -0
- package/admin/custom/i18n/zh-cn.json +24 -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 +21 -21
- package/admin/i18n/en.json +21 -21
- package/admin/i18n/es.json +24 -0
- package/admin/i18n/fr.json +24 -0
- package/admin/i18n/it.json +24 -0
- package/admin/i18n/nl.json +24 -0
- package/admin/i18n/pl.json +24 -0
- package/admin/i18n/pt.json +24 -0
- package/admin/i18n/ru.json +24 -0
- package/admin/i18n/uk.json +24 -0
- package/admin/i18n/zh-cn.json +24 -0
- package/build/lib/credentials.js +38 -8
- package/build/lib/credentials.js.map +1 -1
- package/build/lib/nlu.js +173 -27
- package/build/lib/nlu.js.map +1 -1
- package/build/lib/weather.js +132 -0
- package/build/lib/weather.js.map +1 -1
- package/build/main.js +129 -25
- package/build/main.js.map +1 -1
- package/io-package.json +28 -3
- package/package.json +6 -6
- package/admin/custom/assets/Components-DcnuVFM6.js +0 -1
- package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-DogEjv3U.js +0 -1
- 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
- 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
- package/admin/custom/assets/hostInit-DKMI-sLp.js +0 -1
- package/admin/custom/assets/virtualExposes-DOTO22UK.js +0 -1
- package/admin/custom/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__ConfigCustomAssistant__customComponents_js-CQa59yVi.js +0 -1
- package/admin/custom/assets/virtual_mf-exposes-ssr___mfe_internal__ConfigCustomAssistant__customComponents_js-9g2Fj8tk.js +0 -2
- package/admin/custom/assets/virtual_mf-exposes___mfe_internal__ConfigCustomAssistant__customComponents_js-BTUQTbYo.js +0 -2
- package/admin/custom/customComponents.ssr.js +0 -1
package/build/lib/credentials.js
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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
|
|
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":"
|
|
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
|
|
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
|
-
|
|
562
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
|
640
|
-
|
|
641
|
-
|
|
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) {
|