uid: jm:cuevox label: Cuevox - A Rule Based Voice Interpreter description: Cuevox is a simple but extensible voice control tool for openHAB. The OH Android App can be used to send voice commands to OH and interpret them using voice command rules that can be easily extended in the rule script. configDescriptions: - name: VoiceCommandItem label: Voice Command Item description: The item that stores the voice command type: TEXT context: item filterCriteria: - name: type value: String required: true triggers: - id: '1' configuration: itemName: '{{VoiceCommandItem}}' type: core.ItemStateUpdateTrigger conditions: [] actions: - inputs: {} id: '2' configuration: type: application/javascript script: | (function (data) { // *** // INTERPRETER // *** const { items } = require('openhab'); let logger = require('openhab').log('cuevox'); class Expression { /** * @param {string[]} tokens * @returns {EvaluationResult} */ evaluate(tokens) { return new EvaluationResult(false, tokens); } checkAndNormalizeExpression(expression) { if (typeof expression === 'string') { let tokens = tokenizeUtterance(normalizeString(expression)); if (tokens.length == 0) { logger.warn(`Empty expression found after normalizing: ${expression}`); return ""; } else if (tokens.length > 1) { logger.warn(`Found expression with multiple tokens, only 1 token is supported: ${expression}`); } return tokens[0]; } return expression; } } class MultiExpression extends Expression { /** * @param {...Expression} expressions */ constructor(...expressions) { super(); this.multiExpressions = expressions.map(expr => this.checkAndNormalizeExpression(expr)); } } class SingleExpression extends Expression { /** * @param {Expression} expression */ constructor(expression) { super(); this.singleExpression = this.checkAndNormalizeExpression(expression); } } /** * Creates an alternative expression. All given expressions can be used alternatively, i.e. using an OR logic. * @param {...Expression} expressions Any expression types. */ function alt(...expressions) {return new AlternativeExp(...expressions);} class AlternativeExp extends MultiExpression { evaluate(tokens) { logger.debug("eval alt: " + stringify(this.multiExpressions)); logger.debug("for tokens: " + stringify(tokens)); var success = false; var executeFunction = null; var remainingTokens = tokens; var executeParameter = null; let groupItems = []; for (var index = 0; index < this.multiExpressions.length; index++) { var subexp = this.multiExpressions[index]; logger.debug("alt index: " + index + "; subexp: " + stringify(subexp)); var result = evaluateExpressionOrString(subexp, tokens.slice()); if (result.success) { success = true; remainingTokens = result.remainingTokens; executeFunction = result.executeFunction || executeFunction; executeParameter = mergeExecuteParameters(executeParameter, result.executeParameter); // collect group items from result if (result.groupItems && result.groupItems.length > 0) { groupItems = groupItems.concat(result.groupItems); } break; } } logger.debug("eval alt: " + success) return new EvaluationResult(success, remainingTokens, executeFunction, executeParameter, groupItems); } } /** * Creates a sequential expression. All given expressions are processed sequentially in that order. * @param {...Expression} expressions Any expression types. */ function seq(...expressions) {return new SequenceExp(...expressions);} class SequenceExp extends MultiExpression { evaluate(tokens) { logger.debug("eval seq: " + stringify(this.multiExpressions)); var success = true; var executeFunction = null; var executeParameter = null; let groupItems = []; var remainingTokens = tokens.slice(); for (var index = 0; index < this.multiExpressions.length; index++) { var subexp = this.multiExpressions[index]; logger.debug("eval subexp " + index + "; subexp: " + stringify(subexp)) var result = evaluateExpressionOrString(subexp, remainingTokens); if (!result.success) { logger.debug("eval subexp " + index + "failed"); success = false; break; } remainingTokens = result.remainingTokens; executeFunction = result.executeFunction || executeFunction; executeParameter = mergeExecuteParameters(executeParameter, result.executeParameter); // collect group items from result if (result.groupItems && result.groupItems.length > 0) { groupItems = groupItems.concat(result.groupItems); } } logger.debug("eval seq: " + success) return new EvaluationResult(success, remainingTokens, executeFunction, executeParameter, groupItems); } } /** * Creates an optional expression. The given expression is not mandatory for a match. * @param {Expression} expression */ function opt(expression) {return new OptionalExp(expression);} class OptionalExp extends SingleExpression { evaluate(tokens) { logger.debug("eval opt: " + stringify(this.singleExpression)) var result = evaluateExpressionOrString(this.singleExpression, tokens.slice()); if (result.success) { logger.debug("eval opt success") // only return the reduced token array and other parameters if optional expression was successful. return new EvaluationResult(true, result.remainingTokens, result.executeFunction, result.executeParameter, result.groupItems || []); } logger.debug("eval opt fail") // otherwise still return successful, but nothing from the optional expression result return new EvaluationResult(true, tokens, null, null); } } /** * Creates a command expression. * If the given expression is matched, the given command is sent to all found items of that rule. * @param {Expression} expression * @param {string} command */ function cmd(expression, command) {return new CommandExp(expression, command);} class CommandExp extends SingleExpression { /** * @param {Expression} expression * @param {string} command */ constructor(expression, command) { super(expression); this.command = command; } evaluate(tokens) { logger.debug("eval cmd: " + stringify(this.singleExpression)); logger.debug("eval cmd tokens: " + stringify(tokens)); var result = evaluateExpressionOrString(this.singleExpression, tokens); logger.debug("eval cmd result: " + stringify(result)); if (!result.success) { return new EvaluationResult(false, tokens, null, null); } let commandToExecute = this.command; var executeFunction = function(parameter) { if (!parameter || typeof(parameter) != "object") { logger.debug("Trying to send a command, but no proper object parameter found") return; } if (!parameter.items) { logger.debug("Trying to send a command, but no items parameter found") return; } parameter.items.forEach(item => { item.sendCommand(commandToExecute); }); } // Keep result's executeParameter but add our own function return new EvaluationResult(true, result.remainingTokens, executeFunction, result.executeParameter, result.groupItems || []); } } /** * Creates an item label expression. * It will try to match an item's label or its synonyms to the tokens at this point. * Only a single item must be matched. * The found item can be included in the final execution parameter, e.g. to send a command to that item. * @param {boolean} includeInExecuteParameter Default: true. * @param {boolean} isGroup Default: false. If true, only group items (type: "Group") are matched. * @returns */ function itemLabel(includeInExecuteParameter, isGroup) {return item({ include: includeInExecuteParameter, type: isGroup ? "Group" : null, matchMultiple: false });} /** * Creates an item properties expression. * It tries to filter items according to their properties: tags, item type or parent group. * If none of filter properties are given, then all items in the registry will be matched. * All matched items will be included in the final execution parameter, e.g. to send a command to these items. * @param {Expression} expression The expression to match. * @param {string[]} tags Default: []. Only items that have all the given tags will be matched. * @param {boolean} groupRequired Default: true. If true, the expression must contain a group Item Label Expression. Only descendants of that group will be matched. * @param {string} itemType Default: null. If a type is given, then only items of that type will be matched. */ function itemProperties(expression, tags, groupRequired, itemType) {return item({ type: itemType, tag: tags, tagMode: "all", matchMultiple: true }, { expr: expression, groupContext: groupRequired ? "last" : null });} /** * Match one or more items by label, synonym, type, tag, or expression. * * @param {Object} [options={}] * @param {boolean} [options.include=true] * Whether to include matched items in execution parameters. * @param {boolean} [options.matchMultiple=false] * Whether to allow multiple item matches (default: false = match only one item). * @param {string|string[]} [options.type=null] * Restrict to specific item type(s). Single string = exact match, array = match any. * @param {string|string[]} [options.tag=[]] * Restrict to items with these tag(s). * @param {"all"|"any"} [options.tagMode="all"] * Whether all tags or any tag must match. * * @param {Object} [expressionOptions={}] * @param {Expression|null} [expressionOptions.expr=null] * If provided, tokens are matched against this expression instead of the item label. * @param {null|"last"|"one"|"all"} [expressionOptions.groupContext=null] * - `null`: No group required (default). * - `"last"`: Require a group and use the last matched one. * - `"one"`: Require exactly one matched group; log error if multiple are matched. * - `"all"`: Require one or more groups; return descendants of all matched groups. * * @returns {Expression} * @throws Logs an error and fails the matching if invalid parameters are provided. */ function item(options = {}, expressionOptions = {}) { return new ItemExp(options, expressionOptions); } class ItemExp extends Expression { constructor(options = {}, expressionOptions = {}) { super(); // options this.include = (options.include === undefined) ? true : options.include; this.matchMultiple = options.matchMultiple ?? false; this.type = options.type ?? null; // string or array this.tag = options.tag ?? options.tag === null ? options.tag : options.tag ?? null; // allow null this.tagMode = options.tagMode ?? "all"; // expression options this.expr = expressionOptions.expr ? this.checkAndNormalizeExpression(expressionOptions.expr) : null; this.groupContext = expressionOptions.groupContext ?? null; // null | "last" | "one" | "all" // Get filtered items based on tag/type criteria this.getFilteredItems = () => { // Log initial filter criteria logger.debug("Filtering items with criteria:", { tags: this.tag, tagMode: this.tagMode, type: this.type }); let remainingItems = []; const tags = (this.tag == null) ? [] : (Array.isArray(this.tag) ? this.tag : [this.tag]); if (!tags || tags.length === 0) { remainingItems = items.getItems(); logger.debug("eval item: get all items"); } else if (this.tagMode === "all") { remainingItems = items.getItemsByTag(...tags); logger.debug("eval item: filter items by tags (all): " + tags + "; remaining: " + remainingItems.length); } else { // any // union of items for any tag let set = new Map(); tags.forEach(t => { let subset = items.getItemsByTag(t) || []; subset.forEach(it => set.set(it.name || JSON.stringify(it), it)); }); remainingItems = Array.from(set.values()); logger.debug("eval item: filter items by tags (any): " + tags + "; remaining: " + remainingItems.length); } if (this.type != null) { const types = Array.isArray(this.type) ? this.type : [this.type]; remainingItems = remainingItems.filter(item => types.includes(item.type)); logger.debug("eval item: filter items by type: " + types.join(",") + "; remaining: " + remainingItems.length); } // Log final results with item names for easier debugging logger.debug("Final filtered items:", remainingItems.map(i => i.name)); return remainingItems; }; // basic validation // surface detailed, non-throwing validation warnings if (typeof this.include !== "boolean") { logger.warn(`item(): 'include' must be boolean - got: ${JSON.stringify(options.include)}`); } if (typeof this.matchMultiple !== "boolean") { logger.warn(`item(): 'matchMultiple' must be boolean - got: ${JSON.stringify(options.matchMultiple)}`); } if (this.tag != null && !(Array.isArray(this.tag) || typeof this.tag === 'string')) { logger.warn(`item(): 'tag' must be string or array of strings or null - got: ${JSON.stringify(options.tag)}`); } if (this.type != null && !(Array.isArray(this.type) || typeof this.type === 'string')) { logger.warn(`item(): 'type' must be string or array of strings or null - got: ${JSON.stringify(options.type)}`); } if (this.tagMode !== "all" && this.tagMode !== "any") { logger.warn(`item(): 'tagMode' must be 'all' or 'any' - got: ${JSON.stringify(options.tagMode)}; defaulting to 'all'`); this.tagMode = "all"; } } evaluate(tokens) { logger.debug("eval item unified with tokens: " + stringify(tokens)); if (tokens.length < 1) { logger.debug("no tokens, eval item fail"); return new EvaluationResult(false, tokens, null, null); } // If an inner expression is provided, evaluate it first if (this.expr) { const expResult = evaluateExpressionOrString(this.expr, tokens.slice()); if (!expResult.success) { logger.debug("eval item: inner expression failed"); return new EvaluationResult(false, tokens, null, null); } // group context requirements if (this.groupContext && !(expResult.groupItems && expResult.groupItems.length > 0) && !(expResult.executeParameter && expResult.executeParameter.items && expResult.executeParameter.items.length > 0)) { logger.warn(`eval item fail: group context required but no group found; groupContext=${this.groupContext}; expr=${stringify(this.expr)}`); return new EvaluationResult(false, tokens, null, null); } // Apply common filtering logic let remainingItems = this.getFilteredItems(); // apply group filtering if requested // collect groups from expResult.groupItems let groups = []; if (expResult.groupItems && expResult.groupItems.length > 0) { groups = groups.concat(expResult.groupItems); } if (this.groupContext) { if (groups.length === 0) { logger.debug("eval item fail: group required but none available"); return new EvaluationResult(false, tokens, null, null); } if (this.groupContext === "one" && groups.length > 1) { logger.warn(`eval item fail: groupContext 'one' but multiple groups matched (${groups.length}); groups=[${groups.map(g => g.name).join(", ")}]`); return new EvaluationResult(false, tokens, null, null); } // collect items in subgroup(s) // depending on groupContext, decide which groups to apply let groupsToUse = groups; if (this.groupContext === 'last') { groupsToUse = [groups[groups.length - 1]]; } else if (this.groupContext === 'one') { groupsToUse = [groups[0]]; } let filtered = []; groupsToUse.forEach(g => { filtered = filtered.concat(remainingItems.filter(item => itemIsInSubGroup(item, g))); }); remainingItems = filtered; logger.debug("eval item: filter items by group context; remaining: " + remainingItems.length); } if (!expResult.executeParameter) { expResult.executeParameter = { items: [] }; } if (!expResult.executeParameter.items) { expResult.executeParameter.items = []; } // apply matchMultiple logic if (!this.matchMultiple) { if (remainingItems.length === 0) { logger.warn(`eval item fail: no items matched; filters={type:${JSON.stringify(this.type)}, tag:${JSON.stringify(this.tag)}, tagMode:${this.tagMode}}`); return new EvaluationResult(false, tokens, null, null); } if (remainingItems.length > 1) { logger.warn(`eval item fail: multiple items matched (${remainingItems.length}) but matchMultiple is false; items=[${remainingItems.map(i=>i.name||i.label||'').join(', ')}]`); return new EvaluationResult(false, tokens, null, null); } // single item if (this.include) { expResult.executeParameter.items = expResult.executeParameter.items.concat(remainingItems); } } else { // allow multiple if (this.include) { expResult.executeParameter.items = expResult.executeParameter.items.concat(remainingItems); } } return new EvaluationResult(true, expResult.remainingTokens, expResult.executeFunction, expResult.executeParameter, expResult.groupItems || []); } // No inner expression: behave like label matching const remainingItems = this.getFilteredItems(); const matchResults = getItemByLabelOrSynonym(remainingItems, tokens, this.matchMultiple); if (!matchResults || !matchResults.matchedItems || matchResults.matchedItems.length === 0) { logger.debug("eval item label fail: no items matched"); return new EvaluationResult(false, tokens, null, null); } // For label matching, apply matchMultiple requirements if (!this.matchMultiple && matchResults.matchedItems.length > 1) { logger.warn(`eval item label fail: multiple items matched (${matchResults.matchedItems.length}) but matchMultiple is false; items=[${matchResults.matchedItems.map(i=>i.name||i.label||'').join(', ')}]`); return new EvaluationResult(false, tokens, null, null); } let executeParameter = null; if (this.include) { executeParameter = { items: matchResults.matchedItems }; } const groupItems = matchResults.matchedItems.filter(item => item.type === 'Group'); return new EvaluationResult(true, matchResults.remainingTokens, null, executeParameter, groupItems); } } class EvaluationResult { /** * * @param {boolean} success if evaluation was successful or not * @param {string[]} remainingTokens * @param {function} executeFunction the function to execute in the end * @param {object} executeParameter the parameter inserted in the executeFunction. Should be a single object that can hold multiple parameters in its key/value pairs. * @param {object[]} groupItems optional array of group items related to this result */ constructor(success, remainingTokens, executeFunction, executeParameter, groupItems) { this.success = success; this.remainingTokens = remainingTokens || []; this.executeFunction = executeFunction; this.executeParameter = executeParameter; this.groupItems = groupItems || []; } } class AnnotationResult { /** * * @param {boolean} success if evaluation was successful or not * @param {string} input the original user input * @param {string[]} remainingTokens * @param {string} executeFunction the function name or its source * @param {object} executeParameter the parameter inserted in the executeFunction. Should be a single object that can hold multiple parameters in its key/value pairs. */ constructor(success, input, remainingTokens, executeFunction, executeParameter) { this.success = success; this.input = input || ""; this.remainingTokens = remainingTokens || []; this.executeFunction = executeFunction; this.executeParameter = executeParameter; } } class RuleBasedInterpreter { constructor() { this.rules = []; } // *** // FUNCTIONS // *** /** * Adds a rule. * Either the expression must contain a function to execute (e.g. send a command) or a specific function must be given. * @param {Expression} expression * @param {function} executeFunction If a specific function is given, it will override any function from the expression. */ addRule(expression, executeFunction) { this.rules.push({ expression: expression, executeFunction: executeFunction }); } /** * Clears all saved rules. */ clearRules() { this.rules = []; } /** * Tries to interpret the given utterance by matching all saved rules. * @param {string} utterance * @returns ${AnnotationResult} */ interpretUtterance(utterance) { var result = new EvaluationResult(false, [], null); if (!utterance) { return; } var normalizedUtterance = normalizeString(utterance); var tokens = tokenizeUtterance(normalizedUtterance); result.remainingTokens = tokens; logger.debug("input normalized utterance: " + normalizedUtterance); logger.debug("input tokens: " + stringify(tokens)); for (var index = 0; index < this.rules.length; index++) { logger.debug("check rule " + index); var rule = this.rules[index]; logger.debug(stringify(rule)); result = evaluateExpressionOrString(rule.expression, tokens.slice()); if (result.success) { result.executeFunction = rule.executeFunction || result.executeFunction; if (!result.executeFunction) { logger.debug("rule matched, but no function to execute found, continue"); continue; } result.success = result.executeFunction(result.executeParameter); break; } } return annotateResult(result, normalizedUtterance); } } /** * Convert EvaluationResult to AnnotationResult * @param {EvaluationResult} result * @param {string} input * @returns {AnnotationResult} */ function annotateResult(result, input) { var annotation = new AnnotationResult( result.success, input, result.remainingTokens, "", result.executeParameter ); if(!result.executeFunction) { return annotation; } if(!result.executeFunction.name) { annotation.executeFunction = result.executeFunction.toString(); } else { annotation.executeFunction = result.executeFunction.name; } return annotation; } /** * * @param {Expression} expression * @param {string[]} tokens * @returns {EvaluationResult} */ function evaluateExpressionOrString(expression, tokens) { if (tokens.length < 1) { return new EvaluationResult(true, tokens, null); } if (typeof(expression) == "string") { return evaluateStringExpression(expression, tokens); } return expression.evaluate(tokens); } function evaluateStringExpression(expression, tokens) { if (tokens.length < 1) { return new EvaluationResult(false, tokens, null, null); } logger.debug("eval string: " + expression) logger.debug("token: " + tokens[0]); var hasMatch = tokens[0] === expression; //tokens[0].match(expression) != null; logger.debug("hasMatch: " + hasMatch) return new EvaluationResult(hasMatch, tokens.slice(1), null, null); } function getItemsBySemanticType(itemList, semanticType) { return itemList.filter(item => item.semantics.semanticType == semanticType); } /** * Find an item by exact label or synonym match against the start of the given tokens. * * Contract (current behavior): * - Only exact matches (label or synonyms) are considered. * - For each item we determine the shortest exact match length (number of tokens) it * provides for the given input tokens (from label or any synonym). * - The function selects the single item whose shortest exact match length is the * smallest among all items. If multiple items share the same smallest length the * result is considered ambiguous and the function returns null. * - The returned remainingTokens slice corresponds to tokens after consuming the * matched label/synonym (i.e. tokens.slice(matchLength)). * * Note on backtracking: an alternative strategy would be to prefer the shortest * match but automatically backtrack to longer matches if the rest of the expression * (following tokens) fails to match. That would require adding a backtracking * mechanism across expression evaluation and is more invasive; it may be implemented * in the future if needed. For now we keep the simpler, deterministic shortest-match * contract described above. */ // --- Matching helpers ----------------------------------------------------- function tokenizeNormalized(str) { return tokenizeUtterance(normalizeString(str || "")); } function checkTokens(tokensToCheck, tokensTarget) { if (tokensToCheck.length > tokensTarget.length) { return false; } for (let index = 0; index < tokensToCheck.length; index++) { if (tokensToCheck[index] != tokensTarget[index]) { return false; } } return true; } function getSynonymTokens(item) { // Returns an array of token arrays for each synonym. Handles missing metadata. try { const syns = getSynonyms(item); return syns.map(s => tokenizeNormalized(s)); } catch (e) { logger.warn("Failed to get synonyms for item " + (item && item.name)); return []; } } function shortestExactMatchForItem(item, tokens) { const labelTokens = tokenizeNormalized(item.label); let shortest = null; if (labelTokens.length > 0 && checkTokens(labelTokens, tokens)) { shortest = { matchLength: labelTokens.length, source: 'label', matchedTokens: labelTokens }; } const synTokens = getSynonymTokens(item); synTokens.forEach(st => { if (st.length > 0 && checkTokens(st, tokens)) { if (!shortest || st.length < shortest.matchLength) { shortest = { matchLength: st.length, source: 'synonym', matchedTokens: st }; } } }); return shortest; // null if none } function collectAllMatches(itemList, tokens) { return itemList.map(item => { const shortest = shortestExactMatchForItem(item, tokens); return shortest ? { item: item, matchLength: shortest.matchLength, source: shortest.source, matchedTokens: shortest.matchedTokens } : null; }).filter(m => m !== null); } function pickShortestMatches(matches) { if (!matches || matches.length === 0) return null; const shortestLength = Math.min(...matches.map(m => m.matchLength)); const shortestMatches = matches.filter(m => m.matchLength === shortestLength); return shortestMatches; if (shortestMatches.length === 1) return shortestMatches[0]; return null; // ambiguous } function getItemByLabelOrSynonym(itemList, tokens, returnAllMatches = false) { // Use helper pipeline: collect matches const matches = collectAllMatches(itemList, tokens); if (matches.length === 0) { logger.debug("get item by label: no matches found"); return null; } const shortestMatches = pickShortestMatches(matches); if (returnAllMatches) { logger.debug(`get item by label: returning all ${shortestMatches.length} matches of length ${shortestMatches[0].matchLength}`); return { matchedItems: shortestMatches.map(m => m.item), remainingTokens: tokens.slice(shortestMatches[0].matchLength) }; } // Single best match required if (shortestMatches.length === 1) { logger.debug("get item by label: success - single shortest match"); return { matchedItems: [shortestMatches[0].item], remainingTokens: tokens.slice(shortestMatches[0].matchLength) }; } logger.debug("get item by label: fail - multiple shortest matches but only single match required"); return null; } function normalizeString(str) { // allow only unicode letters, apostrophes and spaces return str.toLowerCase().replace(/[^\p{L}' ]/gu, ""); } function tokenizeUtterance(utterance) { return utterance.split(" ").filter(token => token !== ""); } /** * Merges executeParameter objects, combining their properties * @param {object} current Current executeParameter object * @param {object} additional Additional executeParameter object to merge * @returns {object} Merged executeParameter object */ function mergeExecuteParameters(current, additional) { if (!current && !additional) return null; if (!current) return additional; if (!additional) return current; const result = { ...current }; // Special handling for items array if (additional.items) { result.items = result.items || []; result.items = result.items.concat(additional.items); } // Merge any other properties Object.keys(additional).forEach(key => { if (key !== 'items') { result[key] = additional[key]; } }); return result; } function stringify(obj) { return JSON.stringify(obj, null, 2); } function stringIsNullOrEmpty(str) { return str === undefined || str === null || str === ""; } function getSynonyms(item) { if (typeof item.getMetadata !== 'function') { // gracefully handle missing getMetadata method, e.g. in unit tests, but log a warning logger.warn("Item " + item.name + " does not implement getMetadata method - skipping synonyms"); return []; } var meta = item.getMetadata("synonyms"); if (!meta || stringIsNullOrEmpty(meta.value)) { return []; } return meta.value.split(","); } function itemIsInSubGroup(item, targetGroupItem) { // Don't match the target group itself if (item.name === targetGroupItem.name) { return false; } let checkedGroupNames = new Set(); let groupStack = [...(item.groupNames || [])]; while (groupStack.length > 0) { const groupName = groupStack.pop(); // Direct parent group matches if (groupName === targetGroupItem.name) { return true; } // Add ancestor groups if not already checked if (!checkedGroupNames.has(groupName)) { checkedGroupNames.add(groupName); const group = items.getItem(groupName); if (group && Array.isArray(group.groupNames)) { groupStack.push(...group.groupNames); } } } return false; } // *** // RULES // *** const { ON, OFF, UP, DOWN } = require("@runtime"); let rbi = new RuleBasedInterpreter(); function interpretUtterance(utterance) { return JSON.stringify(rbi.interpretUtterance(utterance)); } // ** ENGLISH ************************** let onOff = alt(cmd("on", ON), cmd("off", OFF)); let turn = alt("turn", "switch"); let put = alt("put", "bring", "pull"); let the = opt("the"); let inOfThe = seq(alt("in", "of"), the); let allThe = alt(seq("all", the), the); let upDown = alt(cmd("up", UP), cmd("down", DOWN)); let lowerRaise = alt(cmd("lower", DOWN),cmd("raise", UP)); // turn lights on off in location let lights = alt("light", "lights"); rbi.addRule(seq( turn, opt(onOff), allThe, item({ type: "Switch", tag: "Light", matchMultiple: true }, { expr: seq(lights, opt(onOff), inOfThe, item({ type: "Group", include: false })), groupContext: "last" }), opt(onOff)) ); rbi.addRule(seq( turn, opt(onOff), allThe, item({ type: "Switch", tag: "Light", matchMultiple: true }, { expr: seq(item({ type: "Group", include: false }), lights), groupContext: "last" }), opt(onOff)) ); // rollershutters up/down in location let rollershutters = alt("rollershutter", "rollershutters", seq("roller", alt("shutter", "blind")), seq("roller", alt("shutters", "blinds")), "shutter", "shutters", "blind", "blinds"); rbi.addRule( seq( put, opt(upDown), allThe, item({ type: "Rollershutter", matchMultiple: true }, { expr: seq(rollershutters, opt(upDown), inOfThe, item({ type: "Group", include: false })), groupContext: "last" }), opt(upDown), ) ); rbi.addRule( seq( lowerRaise, allThe, item({ type: "Rollershutter", matchMultiple: true }, { expr: seq(rollershutters, inOfThe, item({ type: "Group", include: false })), groupContext: "last" }) ) ); rbi.addRule( seq( put, opt(upDown), allThe, item({ type: "Rollershutter", matchMultiple: true }, { expr: seq(item({ type: "Group", include: false }), rollershutters), groupContext: "last" }), opt(upDown), ) ); rbi.addRule( seq( lowerRaise, allThe, item({ type: "Rollershutter", matchMultiple: true }, { expr: seq(item({ type: "Group", include: false }), rollershutters), groupContext: "last" }) ) ); // ON OFF type rbi.addRule(seq(turn, opt(onOff), the, item(), opt(onOff))); // UP DOWN type rbi.addRule(seq(put, opt(upDown), the, item(), opt(upDown))); rbi.addRule(seq(lowerRaise, the, item())); // ************************************* // ** GERMAN *************************** var denDieDas = opt(alt("den", "die", "das")); var einAnAus = alt(cmd(alt("ein", "an"), ON), cmd("aus", OFF)); var schalte = alt("schalte", "mache", "schalt", "mach"); var fahre = alt("fahre", "fahr", "mache", "mach"); var hochRunter = alt(cmd(alt("hoch", "auf"), UP), cmd(alt("runter", "herunter", "zu"), DOWN)); let imIn = alt("im", seq("in", opt(alt("der", "dem")))); // ON OFF type rbi.addRule(seq(schalte, denDieDas, item(), einAnAus)); // UP DOWN type rbi.addRule(seq(fahre, denDieDas, item(), hochRunter)); let alleDie = alt("alle", denDieDas); // turn lights on off in location let lichter = alt("licht", "lichter", "lampen"); rbi.addRule(seq( schalte, alleDie, item({ type: "Switch", tag: "Light", matchMultiple: true }, { expr: seq(lichter, opt(einAnAus), imIn, item({ type: "Group", include: false })), groupContext: "last" }), opt(einAnAus)) ); // rollershutters up/down in location let rollos = alt("rollo", "rollos", "rolladen", "rolläden"); rbi.addRule( seq( fahre, denDieDas, item({ type: "Rollershutter", matchMultiple: true }, { expr: seq(rollos, imIn, item({ type: "Group", include: false })), groupContext: "last" }), hochRunter ) ); // ************************************* // ** CUSTOM RULES ********************* // Add your rules here // ************************************* // *** // EXECUTION // *** let vcString = items.getItem("{{VoiceCommandItem}}").state; interpretUtterance(vcString); })(this.event); type: script.ScriptAction