mirror of
https://github.com/danieldemus/openhab-core.git
synced 2025-02-09 18:36:50 +01:00
[voice] Support custom rules on item metadata (#3897)
* [voice] Support custom rules on item metadata * fix isTemplate functionality and test * fix filter location based for non labeled rules Signed-off-by: Miguel Álvarez <miguelwork92@gmail.com>
This commit is contained in:
parent
76b10ac1c1
commit
075bcea8c2
@ -64,9 +64,9 @@ import org.slf4j.LoggerFactory;
|
|||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
@Component(service = HumanLanguageInterpreter.class)
|
@Component(service = HumanLanguageInterpreter.class)
|
||||||
public class StandardInterpreter extends AbstractRuleBasedInterpreter {
|
public class StandardInterpreter extends AbstractRuleBasedInterpreter {
|
||||||
|
public static final String VOICE_SYSTEM_NAMESPACE = "voice-system";
|
||||||
private Logger logger = LoggerFactory.getLogger(StandardInterpreter.class);
|
private Logger logger = LoggerFactory.getLogger(StandardInterpreter.class);
|
||||||
private final ItemRegistry itemRegistry;
|
private final ItemRegistry itemRegistry;
|
||||||
private final String metadataNamespace = "voice-system";
|
|
||||||
private final MetadataRegistry metadataRegistry;
|
private final MetadataRegistry metadataRegistry;
|
||||||
|
|
||||||
@Activate
|
@Activate
|
||||||
@ -164,7 +164,7 @@ public class StandardInterpreter extends AbstractRuleBasedInterpreter {
|
|||||||
);
|
);
|
||||||
/* Item description commands */
|
/* Item description commands */
|
||||||
addRules(Locale.ENGLISH, createItemDescriptionRules( //
|
addRules(Locale.ENGLISH, createItemDescriptionRules( //
|
||||||
(allowedItemNames, labeledCmd) -> restrictedItemRule(allowedItemNames, //
|
(allowedItems, labeledCmd) -> restrictedItemRule(allowedItems, //
|
||||||
seq(alt("set", "change"), opt(the)), /* item */ seq(to, labeledCmd)//
|
seq(alt("set", "change"), opt(the)), /* item */ seq(to, labeledCmd)//
|
||||||
), //
|
), //
|
||||||
Locale.ENGLISH).toArray(Rule[]::new));
|
Locale.ENGLISH).toArray(Rule[]::new));
|
||||||
@ -233,7 +233,7 @@ public class StandardInterpreter extends AbstractRuleBasedInterpreter {
|
|||||||
/* Item description commands */
|
/* Item description commands */
|
||||||
|
|
||||||
addRules(Locale.GERMAN, createItemDescriptionRules( //
|
addRules(Locale.GERMAN, createItemDescriptionRules( //
|
||||||
(allowedItemNames, labeledCmd) -> restrictedItemRule(allowedItemNames, //
|
(allowedItems, labeledCmd) -> restrictedItemRule(allowedItems, //
|
||||||
seq(schalte, denDieDas), /* item */ seq(opt("auf"), labeledCmd)//
|
seq(schalte, denDieDas), /* item */ seq(opt("auf"), labeledCmd)//
|
||||||
), //
|
), //
|
||||||
Locale.GERMAN).toArray(Rule[]::new));
|
Locale.GERMAN).toArray(Rule[]::new));
|
||||||
@ -301,7 +301,7 @@ public class StandardInterpreter extends AbstractRuleBasedInterpreter {
|
|||||||
/* Item description commands */
|
/* Item description commands */
|
||||||
|
|
||||||
addRules(Locale.FRENCH, createItemDescriptionRules( //
|
addRules(Locale.FRENCH, createItemDescriptionRules( //
|
||||||
(allowedItemNames, labeledCmd) -> restrictedItemRule(allowedItemNames, //
|
(allowedItems, labeledCmd) -> restrictedItemRule(allowedItems, //
|
||||||
seq("mets", lela), /* item */ seq(poursurdude, lela, labeledCmd)//
|
seq("mets", lela), /* item */ seq(poursurdude, lela, labeledCmd)//
|
||||||
), //
|
), //
|
||||||
Locale.FRENCH).toArray(Rule[]::new));
|
Locale.FRENCH).toArray(Rule[]::new));
|
||||||
@ -351,11 +351,18 @@ public class StandardInterpreter extends AbstractRuleBasedInterpreter {
|
|||||||
|
|
||||||
/* NextPreviousType */
|
/* NextPreviousType */
|
||||||
|
|
||||||
itemRule(cambiar,
|
itemRule(seq(cambiar, opt(articulo)),
|
||||||
/* item */ seq(opt("a"),
|
/* item */ seq(opt("a"),
|
||||||
alt(cmd("siguiente", NextPreviousType.NEXT),
|
alt(cmd("siguiente", NextPreviousType.NEXT),
|
||||||
cmd("anterior", NextPreviousType.PREVIOUS)))),
|
cmd("anterior", NextPreviousType.PREVIOUS)))),
|
||||||
|
|
||||||
|
itemRule(
|
||||||
|
seq(opt(poner),
|
||||||
|
alt(cmd("siguiente", NextPreviousType.NEXT),
|
||||||
|
cmd("anterior", NextPreviousType.PREVIOUS)),
|
||||||
|
"en"),
|
||||||
|
opt(articulo) /* item */ ),
|
||||||
|
|
||||||
/* PlayPauseType */
|
/* PlayPauseType */
|
||||||
|
|
||||||
itemRule(seq(cmd(alt("continuar", "continúa", "reanudar", "reanuda", "play"), PlayPauseType.PLAY),
|
itemRule(seq(cmd(alt("continuar", "continúa", "reanudar", "reanuda", "play"), PlayPauseType.PLAY),
|
||||||
@ -392,7 +399,7 @@ public class StandardInterpreter extends AbstractRuleBasedInterpreter {
|
|||||||
/* Item description commands */
|
/* Item description commands */
|
||||||
|
|
||||||
addRules(localeES, createItemDescriptionRules( //
|
addRules(localeES, createItemDescriptionRules( //
|
||||||
(allowedItemNames, labeledCmd) -> restrictedItemRule(allowedItemNames, //
|
(allowedItems, labeledCmd) -> restrictedItemRule(allowedItems, //
|
||||||
seq(alt(cambiar, poner), opt(articulo)), /* item */ seq(preposicion, labeledCmd)//
|
seq(alt(cambiar, poner), opt(articulo)), /* item */ seq(preposicion, labeledCmd)//
|
||||||
), //
|
), //
|
||||||
localeES).toArray(Rule[]::new));
|
localeES).toArray(Rule[]::new));
|
||||||
@ -409,12 +416,12 @@ public class StandardInterpreter extends AbstractRuleBasedInterpreter {
|
|||||||
return "Built-in Interpreter";
|
return "Built-in Interpreter";
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Rule> createItemDescriptionRules(CreateItemDescriptionRule creator, @Nullable Locale locale) {
|
private List<Rule> createItemDescriptionRules(CreateItemDescriptionRule creator, Locale locale) {
|
||||||
// Map different item state/command labels with theirs values by item
|
// Map different item state/command labels with theirs values by item
|
||||||
HashMap<String, HashMap<Item, String>> options = new HashMap<>();
|
HashMap<String, HashMap<Item, String>> options = new HashMap<>();
|
||||||
List<Rule> customRules = new ArrayList<>();
|
List<Rule> customRules = new ArrayList<>();
|
||||||
for (var item : itemRegistry.getItems()) {
|
for (var item : itemRegistry.getItems()) {
|
||||||
customRules.addAll(createItemCustomRules(item));
|
customRules.addAll(createItemMetadataRules(locale, item));
|
||||||
var stateDesc = item.getStateDescription(locale);
|
var stateDesc = item.getStateDescription(locale);
|
||||||
if (stateDesc != null) {
|
if (stateDesc != null) {
|
||||||
stateDesc.getOptions().forEach(op -> {
|
stateDesc.getOptions().forEach(op -> {
|
||||||
@ -445,34 +452,28 @@ public class StandardInterpreter extends AbstractRuleBasedInterpreter {
|
|||||||
.map(entry -> {
|
.map(entry -> {
|
||||||
String label = entry.getKey();
|
String label = entry.getKey();
|
||||||
Map<Item, String> commandByItem = entry.getValue();
|
Map<Item, String> commandByItem = entry.getValue();
|
||||||
List<String> itemNames = commandByItem.keySet().stream().map(Item::getName).toList();
|
|
||||||
String[] labelParts = Arrays.stream(label.split("\\s")).filter(p -> !p.isBlank())
|
String[] labelParts = Arrays.stream(label.split("\\s")).filter(p -> !p.isBlank())
|
||||||
.toArray(String[]::new);
|
.toArray(String[]::new);
|
||||||
Expression labeledCmd = cmd(seq((Object[]) labelParts),
|
Expression labeledCmd = cmd(seq((Object[]) labelParts),
|
||||||
new ItemStateCommandSupplier(label, commandByItem));
|
new ItemStateCommandSupplier(label, commandByItem));
|
||||||
return creator.itemDescriptionRule(itemNames, labeledCmd);
|
return creator.itemDescriptionRule(commandByItem.keySet(), labeledCmd);
|
||||||
})) //
|
})) //
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Rule> createItemCustomRules(Item item) {
|
private List<Rule> createItemMetadataRules(Locale locale, Item item) {
|
||||||
var interpreterMetadata = metadataRegistry.get(new MetadataKey(metadataNamespace, item.getName()));
|
var interpreterMetadata = metadataRegistry.get(new MetadataKey(VOICE_SYSTEM_NAMESPACE, item.getName()));
|
||||||
if (interpreterMetadata == null) {
|
if (interpreterMetadata == null) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
List<Rule> list = new ArrayList<>();
|
return Arrays.stream(interpreterMetadata.getValue().split("\n")) //
|
||||||
for (String s : interpreterMetadata.getValue().split("\n")) {
|
.map(line -> this.parseItemCustomRules(locale, item, line.trim(), interpreterMetadata)) //
|
||||||
String line = s.trim();
|
.flatMap(List::stream) //
|
||||||
Rule rule = this.parseItemCustomRule(item, line);
|
.collect(Collectors.toList());
|
||||||
if (rule != null) {
|
|
||||||
list.add(rule);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private interface CreateItemDescriptionRule {
|
private interface CreateItemDescriptionRule {
|
||||||
Rule itemDescriptionRule(List<String> allowedItemNames, Expression labeledCmd);
|
Rule itemDescriptionRule(Set<Item> allowedItemNames, Expression labeledCmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ItemStateCommandSupplier(String label,
|
private record ItemStateCommandSupplier(String label,
|
||||||
|
@ -21,14 +21,17 @@ import java.util.List;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNull;
|
import org.eclipse.jdt.annotation.NonNull;
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.core.common.registry.RegistryChangeListener;
|
import org.openhab.core.common.registry.RegistryChangeListener;
|
||||||
|
import org.openhab.core.config.core.ConfigParser;
|
||||||
import org.openhab.core.events.EventPublisher;
|
import org.openhab.core.events.EventPublisher;
|
||||||
import org.openhab.core.items.GroupItem;
|
import org.openhab.core.items.GroupItem;
|
||||||
import org.openhab.core.items.Item;
|
import org.openhab.core.items.Item;
|
||||||
@ -73,15 +76,43 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
|
|
||||||
private static final String CMD = "cmd";
|
private static final String CMD = "cmd";
|
||||||
private static final String NAME = "name";
|
private static final String NAME = "name";
|
||||||
private static final String VALUE = "name";
|
private static final String VALUE = "value";
|
||||||
|
|
||||||
|
public static final String IS_TEMPLATE_CONFIGURATION = "isTemplate";
|
||||||
|
public static final String IS_SILENT_CONFIGURATION = "isSilent";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reserved token to use in custom item rules.
|
||||||
|
* Represents the name place in the phrase.
|
||||||
|
*/
|
||||||
|
private static final String NAME_TOKEN = "$name$";
|
||||||
|
/**
|
||||||
|
* Reserved token to use in custom item rules.
|
||||||
|
* Represents the command place in the phrase,
|
||||||
|
* which possible values are defined by the command description
|
||||||
|
* of the item registering these rule.
|
||||||
|
*/
|
||||||
|
private static final String CMD_TOKEN = "$cmd$";
|
||||||
|
/**
|
||||||
|
* Reserved token to use in custom item rules.
|
||||||
|
* Represents the command place in the phrase,
|
||||||
|
* allows capturing multiple tokens.
|
||||||
|
*/
|
||||||
|
private static final String DYN_CMD_TOKEN = "$*$";
|
||||||
|
/**
|
||||||
|
* Set of reserved tokens in custom item rules.
|
||||||
|
*/
|
||||||
|
private static final Set<String> CUSTOM_RULE_TOKENS = Set.of(NAME_TOKEN, CMD_TOKEN, DYN_CMD_TOKEN);
|
||||||
|
|
||||||
private static final String LANGUAGE_SUPPORT = "LanguageSupport";
|
private static final String LANGUAGE_SUPPORT = "LanguageSupport";
|
||||||
|
|
||||||
private static final String SYNONYMS_NAMESPACE = "synonyms";
|
private static final String SYNONYMS_NAMESPACE = "synonyms";
|
||||||
|
|
||||||
|
private static final String SEMANTICS_NAMESPACE = "semantics";
|
||||||
|
|
||||||
private final MetadataRegistry metadataRegistry;
|
private final MetadataRegistry metadataRegistry;
|
||||||
|
|
||||||
private Logger logger = LoggerFactory.getLogger(AbstractRuleBasedInterpreter.class);
|
private final Logger logger = LoggerFactory.getLogger(AbstractRuleBasedInterpreter.class);
|
||||||
|
|
||||||
private final Map<Locale, List<Rule>> languageRules = new HashMap<>();
|
private final Map<Locale, List<Rule>> languageRules = new HashMap<>();
|
||||||
private final Map<Locale, Set<String>> allItemTokens = new HashMap<>();
|
private final Map<Locale, Set<String>> allItemTokens = new HashMap<>();
|
||||||
@ -90,7 +121,7 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
private final ItemRegistry itemRegistry;
|
private final ItemRegistry itemRegistry;
|
||||||
private final EventPublisher eventPublisher;
|
private final EventPublisher eventPublisher;
|
||||||
|
|
||||||
private RegistryChangeListener<Item> registryChangeListener = new RegistryChangeListener<Item>() {
|
private final RegistryChangeListener<Item> registryChangeListener = new RegistryChangeListener<>() {
|
||||||
@Override
|
@Override
|
||||||
public void added(Item element) {
|
public void added(Item element) {
|
||||||
invalidate();
|
invalidate();
|
||||||
@ -106,7 +137,7 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private RegistryChangeListener<Metadata> synonymsChangeListener = new RegistryChangeListener<Metadata>() {
|
private final RegistryChangeListener<Metadata> synonymsChangeListener = new RegistryChangeListener<>() {
|
||||||
@Override
|
@Override
|
||||||
public void added(Metadata element) {
|
public void added(Metadata element) {
|
||||||
invalidateIfSynonymsMetadata(element);
|
invalidateIfSynonymsMetadata(element);
|
||||||
@ -178,11 +209,10 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lastResult == null) {
|
if (lastResult != null && lastResult.getException() != null) {
|
||||||
throw new InterpretationException(language.getString(SORRY));
|
|
||||||
} else {
|
|
||||||
throw lastResult.getException();
|
throw lastResult.getException();
|
||||||
}
|
}
|
||||||
|
throw new InterpretationException(language.getString(SORRY));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void invalidate() {
|
private void invalidate() {
|
||||||
@ -288,6 +318,17 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
return tag(NAME, star(new ExpressionIdentifier(this, stopper)));
|
return tag(NAME, star(new ExpressionIdentifier(this, stopper)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an item value placeholder expression. This expression is greedy: Only use it, if you are able to pass in
|
||||||
|
* all possible stop tokens as excludes.
|
||||||
|
* It's safer to use {@link #itemRule} instead.
|
||||||
|
*
|
||||||
|
* @return Expression that represents a name of an item.
|
||||||
|
*/
|
||||||
|
private Expression value() {
|
||||||
|
return value(null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an item value placeholder expression. This expression is greedy: Only use it, if you are able to pass in
|
* Creates an item value placeholder expression. This expression is greedy: Only use it, if you are able to pass in
|
||||||
* all possible stop tokens as excludes.
|
* all possible stop tokens as excludes.
|
||||||
@ -343,13 +384,8 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
* @param rules Rules to add.
|
* @param rules Rules to add.
|
||||||
*/
|
*/
|
||||||
protected void addRules(Locale locale, Rule... rules) {
|
protected void addRules(Locale locale, Rule... rules) {
|
||||||
List<Rule> ruleSet = languageRules.get(locale);
|
List<Rule> ruleSet = languageRules.computeIfAbsent(locale, k -> new ArrayList<>());
|
||||||
if (ruleSet == null) {
|
ruleSet.addAll(Arrays.asList(rules));
|
||||||
languageRules.put(locale, ruleSet = new ArrayList<>());
|
|
||||||
}
|
|
||||||
for (Rule rule : rules) {
|
|
||||||
ruleSet.add(rule);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -376,7 +412,23 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
* @return The created rule.
|
* @return The created rule.
|
||||||
*/
|
*/
|
||||||
protected Rule itemRule(Object headExpression, @Nullable Object tailExpression) {
|
protected Rule itemRule(Object headExpression, @Nullable Object tailExpression) {
|
||||||
return restrictedItemRule(List.of(), headExpression, tailExpression);
|
return restrictedItemRule(ItemFilter.all(), headExpression, tailExpression, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an item rule on base of a head and a tail expression, where the middle part of the new rule's expression
|
||||||
|
* will consist of an item
|
||||||
|
* name expression. Either the head expression or the tail expression should contain at least one {@link #cmd}
|
||||||
|
* generated expression.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param allowedItems Allowed item targets.
|
||||||
|
* @param headExpression The head expression.
|
||||||
|
* @param tailExpression The tail expression.
|
||||||
|
* @return The created rule.
|
||||||
|
*/
|
||||||
|
protected Rule restrictedItemRule(Set<Item> allowedItems, Object headExpression, @Nullable Object tailExpression) {
|
||||||
|
return restrictedItemRule(ItemFilter.forItems(allowedItems), headExpression, tailExpression, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -384,18 +436,18 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
* will consist of an item
|
* will consist of an item
|
||||||
* name expression. Either the head expression or the tail expression should contain at least one {@link #cmd}
|
* name expression. Either the head expression or the tail expression should contain at least one {@link #cmd}
|
||||||
* generated expression.
|
* generated expression.
|
||||||
* Rule will be restricted to the provided item names if any.
|
* Rule will be restricted by the provided filter.
|
||||||
*
|
*
|
||||||
* @param allowedItemNames List of allowed item names, empty for disabled.
|
* @param itemFilter Filters allowed items.
|
||||||
* @param headExpression The head expression.
|
* @param headExpression The head expression.
|
||||||
* @param tailExpression The tail expression.
|
* @param tailExpression The tail expression.
|
||||||
* @return The created rule.
|
* @return The created rule.
|
||||||
*/
|
*/
|
||||||
protected Rule restrictedItemRule(List<String> allowedItemNames, Object headExpression,
|
protected Rule restrictedItemRule(ItemFilter itemFilter, Object headExpression, @Nullable Object tailExpression,
|
||||||
@Nullable Object tailExpression) {
|
boolean isSilent) {
|
||||||
Expression tail = exp(tailExpression);
|
Expression tail = exp(tailExpression);
|
||||||
Expression expression = tail == null ? seq(headExpression, name()) : seq(headExpression, name(tail), tail);
|
Expression expression = tail == null ? seq(headExpression, name()) : seq(headExpression, name(tail), tail);
|
||||||
return new Rule(expression, allowedItemNames) {
|
return new Rule(expression, itemFilter, isSilent) {
|
||||||
@Override
|
@Override
|
||||||
public InterpretationResult interpretAST(ResourceBundle language, ASTNode node,
|
public InterpretationResult interpretAST(ResourceBundle language, ASTNode node,
|
||||||
InterpretationContext context) {
|
InterpretationContext context) {
|
||||||
@ -424,11 +476,54 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a custom rule on base of a head and a tail expression, where the middle part of the new rule's
|
* Creates an item rule which two dynamic capture values on base of a head a middle and an optional tail expression,
|
||||||
* expression
|
* where one of the values is an item name expression and the other a free captured value.
|
||||||
* will consist of a free command to be captured. Either the head expression or the tail expression should contain
|
* Rule will be restricted by the provided filter.
|
||||||
* at least one {@link #cmd}
|
*
|
||||||
* generated expression.
|
* @param item Item registering the rule.
|
||||||
|
* @param itemFilter Filters allowed items.
|
||||||
|
* @param headExpression The head expression.
|
||||||
|
* @param midExpression The middle expression.
|
||||||
|
* @param tailExpression The optional tail expression.
|
||||||
|
* @param isNameFirst Indicates whether the name goes between the head and the middle expressions.
|
||||||
|
* @return The created rule.
|
||||||
|
*/
|
||||||
|
protected Rule restrictedDynamicItemRule(Item item, ItemFilter itemFilter, Object headExpression,
|
||||||
|
Object midExpression, @Nullable Object tailExpression, boolean isNameFirst, boolean isSilent) {
|
||||||
|
Expression head = Objects.requireNonNull(exp(headExpression));
|
||||||
|
Expression mid = Objects.requireNonNull(exp(midExpression));
|
||||||
|
@Nullable
|
||||||
|
Expression tail = exp(tailExpression);
|
||||||
|
Expression firstValue = isNameFirst ? name(mid) : value(mid);
|
||||||
|
Expression secondValue = tail != null ? (isNameFirst ? value(tail) : name(tail))
|
||||||
|
: (isNameFirst ? value() : name());
|
||||||
|
Expression expression = tail == null ? //
|
||||||
|
seq(head, firstValue, mid, secondValue) : //
|
||||||
|
seq(head, firstValue, mid, secondValue, tail);
|
||||||
|
Map<String, String> itemValuesByLabel = getItemValuesByLabel(item);
|
||||||
|
return new Rule(expression, itemFilter, isSilent) {
|
||||||
|
@Override
|
||||||
|
public InterpretationResult interpretAST(ResourceBundle language, ASTNode node,
|
||||||
|
InterpretationContext context) {
|
||||||
|
String[] name = node.findValueAsStringArray(NAME);
|
||||||
|
String[] value = node.findValueAsStringArray(VALUE);
|
||||||
|
if (name != null && value != null) {
|
||||||
|
try {
|
||||||
|
ItemCommandSupplier commandSupplier = new TextCommandSupplier(String.join(" ", value),
|
||||||
|
item.getAcceptedCommandTypes(), itemValuesByLabel);
|
||||||
|
return new InterpretationResult(true, executeSingle(language, name, commandSupplier, context));
|
||||||
|
} catch (InterpretationException ex) {
|
||||||
|
return new InterpretationResult(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return InterpretationResult.SEMANTIC_ERROR;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a custom rule on base of a head and a tail expression,
|
||||||
|
* where the middle part of the new rule's expression will consist of a free command to be captured.
|
||||||
* Rule will be restricted to the provided item name.
|
* Rule will be restricted to the provided item name.
|
||||||
*
|
*
|
||||||
* @param item Item target
|
* @param item Item target
|
||||||
@ -436,11 +531,32 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
* @param tailExpression The tail expression.
|
* @param tailExpression The tail expression.
|
||||||
* @return The created rule.
|
* @return The created rule.
|
||||||
*/
|
*/
|
||||||
protected Rule customItemRule(Item item, Object headExpression, @Nullable Object tailExpression) {
|
protected Rule customDynamicRule(Item item, ItemFilter itemFilter, Object headExpression,
|
||||||
|
@Nullable Object tailExpression, boolean isSilent) {
|
||||||
Expression tail = exp(tailExpression);
|
Expression tail = exp(tailExpression);
|
||||||
Expression expression = tail == null ? seq(headExpression, value(null))
|
Expression expression = tail == null ? seq(headExpression, value(null))
|
||||||
: seq(headExpression, value(tail), tail);
|
: seq(headExpression, value(tail), tail);
|
||||||
|
HashMap<String, String> valuesByLabel = getItemValuesByLabel(item);
|
||||||
|
return new Rule(expression, itemFilter, isSilent) {
|
||||||
|
@Override
|
||||||
|
public InterpretationResult interpretAST(ResourceBundle language, ASTNode node,
|
||||||
|
InterpretationContext context) {
|
||||||
|
String[] commandParts = node.findValueAsStringArray(VALUE);
|
||||||
|
if (commandParts != null && commandParts.length > 0) {
|
||||||
|
try {
|
||||||
|
ItemCommandSupplier commandSupplier = new TextCommandSupplier(
|
||||||
|
String.join(" ", commandParts).trim(), item.getAcceptedCommandTypes(), valuesByLabel);
|
||||||
|
return new InterpretationResult(true, executeCustom(language, commandSupplier, context));
|
||||||
|
} catch (InterpretationException ex) {
|
||||||
|
return new InterpretationResult(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return InterpretationResult.SEMANTIC_ERROR;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashMap<String, String> getItemValuesByLabel(Item item) {
|
||||||
HashMap<String, String> valuesByLabel = new HashMap<>();
|
HashMap<String, String> valuesByLabel = new HashMap<>();
|
||||||
var stateDescription = item.getStateDescription();
|
var stateDescription = item.getStateDescription();
|
||||||
if (stateDescription != null) {
|
if (stateDescription != null) {
|
||||||
@ -460,20 +576,38 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return new Rule(expression, List.of(item.getName())) {
|
return valuesByLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a custom rule on base of a expression.
|
||||||
|
* The expression should contain at least one {@link #cmd} generated expression.
|
||||||
|
*
|
||||||
|
* @param itemFilter Filters the allowed items.
|
||||||
|
* @param cmdExpression The expression.
|
||||||
|
* @return The created rule.
|
||||||
|
*/
|
||||||
|
protected Rule customCommandRule(ItemFilter itemFilter, Object cmdExpression, boolean isSilent) {
|
||||||
|
return new Rule(Objects.requireNonNull(exp(cmdExpression)), itemFilter, isSilent) {
|
||||||
@Override
|
@Override
|
||||||
public InterpretationResult interpretAST(ResourceBundle language, ASTNode node,
|
public InterpretationResult interpretAST(ResourceBundle language, ASTNode node,
|
||||||
InterpretationContext context) {
|
InterpretationContext context) {
|
||||||
String[] commandParts = node.findValueAsStringArray(VALUE);
|
ASTNode cmdNode = node.findNode(CMD);
|
||||||
if (commandParts != null && commandParts.length > 0) {
|
Object tag = cmdNode.getTag();
|
||||||
try {
|
Object value = cmdNode.getValue();
|
||||||
return new InterpretationResult(true,
|
ItemCommandSupplier commandSupplier;
|
||||||
executeCustom(language, item, String.join(" ", commandParts).trim(), valuesByLabel));
|
if (tag instanceof ItemCommandSupplier supplier) {
|
||||||
} catch (InterpretationException ex) {
|
commandSupplier = supplier;
|
||||||
return new InterpretationResult(ex);
|
} else if (value instanceof Number number) {
|
||||||
}
|
commandSupplier = new SingleCommandSupplier(new DecimalType(number.longValue()));
|
||||||
|
} else {
|
||||||
|
commandSupplier = new SingleCommandSupplier(new StringType(cmdNode.getValueAsString()));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new InterpretationResult(true, executeCustom(language, commandSupplier, context));
|
||||||
|
} catch (InterpretationException ex) {
|
||||||
|
return new InterpretationResult(ex);
|
||||||
}
|
}
|
||||||
return InterpretationResult.SEMANTIC_ERROR;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -669,36 +803,50 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
logger.warn("Failed resolving item command");
|
logger.warn("Failed resolving item command");
|
||||||
return language.getString(ERROR);
|
return language.getString(ERROR);
|
||||||
}
|
}
|
||||||
return trySendCommand(language, item, command);
|
return trySendCommand(language, item, command, context.isSilent());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a custom rule command.
|
* Executes a custom rule command.
|
||||||
*
|
*
|
||||||
* @param item the rule target.
|
|
||||||
* @param options replacement values from item description.
|
|
||||||
* @param language resource bundle used for producing localized response texts
|
* @param language resource bundle used for producing localized response texts
|
||||||
* @param commandText label fragments that are used to match an item's label.
|
* @param itemCommandSupplier the rule command supplier.
|
||||||
* For a positive match, the item's label has to contain every fragment - independently of their order.
|
* @param context to propagate the interpretation context.
|
||||||
* They are treated case-insensitive.
|
|
||||||
* @return response text
|
* @return response text
|
||||||
* @throws InterpretationException in case that there is no or more than on item matching the fragments
|
* @throws InterpretationException in case that there is no or more than on item matching the fragments
|
||||||
*/
|
*/
|
||||||
protected String executeCustom(ResourceBundle language, Item item, String commandText,
|
protected String executeCustom(ResourceBundle language, ItemCommandSupplier itemCommandSupplier,
|
||||||
HashMap<String, String> options) throws InterpretationException {
|
Rule.InterpretationContext context) throws InterpretationException {
|
||||||
@Nullable
|
Map<Item, ItemInterpretationMetadata> itemsMap = getItemTokens(language.getLocale());
|
||||||
String commandReplacement = options.get(commandText);
|
Set<Entry<Item, ItemInterpretationMetadata>> compatibleItemEntries = itemsMap.entrySet().stream() //
|
||||||
Command command = TypeParser.parseCommand(item.getAcceptedCommandTypes(),
|
.filter(itemEntry -> context.itemFilter().filterItem(itemEntry.getKey(), metadataRegistry)) //
|
||||||
commandReplacement != null ? commandReplacement : commandText);
|
.collect(Collectors.toSet());
|
||||||
|
if (compatibleItemEntries.isEmpty()) {
|
||||||
|
throw new InterpretationException(language.getString(NO_OBJECTS));
|
||||||
|
}
|
||||||
|
if (compatibleItemEntries.size() > 1 && context.locationItem() != null) {
|
||||||
|
var filteredCompatibleEntries = compatibleItemEntries.stream() //
|
||||||
|
.filter(itemEntry -> itemEntry.getValue().locationParentNames.contains(context.locationItem())) //
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if (filteredCompatibleEntries.size() == 1) {
|
||||||
|
logger.debug("Collision resolved based on location context");
|
||||||
|
compatibleItemEntries = filteredCompatibleEntries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (compatibleItemEntries.size() > 1) {
|
||||||
|
throw new InterpretationException(language.getString(MULTIPLE_OBJECTS));
|
||||||
|
}
|
||||||
|
Item item = compatibleItemEntries.stream().map(Entry::getKey).findFirst().get();
|
||||||
|
Command command = itemCommandSupplier.getItemCommand(item);
|
||||||
if (command == null) {
|
if (command == null) {
|
||||||
logger.warn("Failed creating command for {} from {}", item, commandText);
|
logger.warn("Failed creating command");
|
||||||
return language.getString(ERROR);
|
return language.getString(ERROR);
|
||||||
}
|
}
|
||||||
return trySendCommand(language, item, command);
|
return trySendCommand(language, item, command, context.isSilent());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String trySendCommand(ResourceBundle language, Item item, Command command) {
|
private String trySendCommand(ResourceBundle language, Item item, Command command, boolean isSilent) {
|
||||||
if (command instanceof State newState) {
|
if (command instanceof State newState) {
|
||||||
try {
|
try {
|
||||||
State oldState = item.getStateAs(newState.getClass());
|
State oldState = item.getStateAs(newState.getClass());
|
||||||
@ -719,7 +867,7 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
eventPublisher.post(ItemEventFactory.createCommandEvent(item.getName(), command));
|
eventPublisher.post(ItemEventFactory.createCommandEvent(item.getName(), command));
|
||||||
return language.getString(OK);
|
return !isSilent ? language.getString(OK) : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -745,31 +893,40 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
@Nullable ItemCommandSupplier commandSupplier, Rule.InterpretationContext context) {
|
@Nullable ItemCommandSupplier commandSupplier, Rule.InterpretationContext context) {
|
||||||
Map<Item, ItemInterpretationMetadata> itemsData = new HashMap<>();
|
Map<Item, ItemInterpretationMetadata> itemsData = new HashMap<>();
|
||||||
Map<Item, ItemInterpretationMetadata> exactMatchItemsData = new HashMap<>();
|
Map<Item, ItemInterpretationMetadata> exactMatchItemsData = new HashMap<>();
|
||||||
|
Map<Item, ItemInterpretationMetadata> exactMatchOnTargetItemsData = new HashMap<>();
|
||||||
Map<Item, ItemInterpretationMetadata> map = getItemTokens(language.getLocale());
|
Map<Item, ItemInterpretationMetadata> map = getItemTokens(language.getLocale());
|
||||||
for (Entry<Item, ItemInterpretationMetadata> entry : map.entrySet()) {
|
for (Entry<Item, ItemInterpretationMetadata> entry : map.entrySet()) {
|
||||||
Item item = entry.getKey();
|
Item item = entry.getKey();
|
||||||
ItemInterpretationMetadata interpretationMetadata = entry.getValue();
|
ItemInterpretationMetadata interpretationMetadata = entry.getValue();
|
||||||
if (!context.allowedItems().isEmpty() && !context.allowedItems().contains(item.getName())) {
|
if (!context.itemFilter().filterItem(item, metadataRegistry)) {
|
||||||
logger.trace("Item {} discarded, not allowed for this rule", item.getName());
|
logger.trace("Item {} discarded, not allowed for this rule", item.getName());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (List<List<String>> itemLabelFragmentsPath : interpretationMetadata.pathToItem) {
|
for (List<List<String>> itemLabelFragmentsPath : interpretationMetadata.pathToItem) {
|
||||||
boolean exactMatch = false;
|
boolean exactMatch = false;
|
||||||
|
boolean exactMatchOnTarget = false;
|
||||||
logger.trace("Checking tokens {} against the item tokens {}", labelFragments, itemLabelFragmentsPath);
|
logger.trace("Checking tokens {} against the item tokens {}", labelFragments, itemLabelFragmentsPath);
|
||||||
List<String> lowercaseLabelFragments = Arrays.stream(labelFragments)
|
List<String> lowercaseLabelFragments = Arrays.stream(labelFragments)
|
||||||
.map(lf -> lf.toLowerCase(language.getLocale())).toList();
|
.map(lf -> lf.toLowerCase(language.getLocale())).toList();
|
||||||
List<String> unmatchedFragments = new ArrayList<>(lowercaseLabelFragments);
|
List<String> unmatchedFragments = new ArrayList<>(lowercaseLabelFragments);
|
||||||
for (List<String> itemLabelFragments : itemLabelFragmentsPath) {
|
if (itemLabelFragmentsPath.get(itemLabelFragmentsPath.size() - 1).equals(lowercaseLabelFragments)) {
|
||||||
if (itemLabelFragments.equals(lowercaseLabelFragments)) {
|
exactMatch = true;
|
||||||
exactMatch = true;
|
exactMatchOnTarget = true;
|
||||||
unmatchedFragments.clear();
|
unmatchedFragments.clear();
|
||||||
break;
|
} else {
|
||||||
|
for (List<String> itemLabelFragments : itemLabelFragmentsPath) {
|
||||||
|
if (itemLabelFragments.equals(lowercaseLabelFragments)) {
|
||||||
|
exactMatch = true;
|
||||||
|
unmatchedFragments.clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
unmatchedFragments.removeAll(itemLabelFragments);
|
||||||
}
|
}
|
||||||
unmatchedFragments.removeAll(itemLabelFragments);
|
|
||||||
}
|
}
|
||||||
boolean allMatched = unmatchedFragments.isEmpty();
|
boolean allMatched = unmatchedFragments.isEmpty();
|
||||||
logger.trace("Matched: {}", allMatched);
|
logger.trace("Matched: {}", allMatched);
|
||||||
logger.trace("Exact match: {}", exactMatch);
|
logger.trace("Exact match: {}", exactMatch);
|
||||||
|
logger.trace("Exact match on target: {}", exactMatchOnTarget);
|
||||||
if (allMatched) {
|
if (allMatched) {
|
||||||
List<Class<? extends Command>> commandTypes = commandSupplier != null
|
List<Class<? extends Command>> commandTypes = commandSupplier != null
|
||||||
? commandSupplier.getCommandClasses(null)
|
? commandSupplier.getCommandClasses(null)
|
||||||
@ -780,11 +937,14 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
if (exactMatch) {
|
if (exactMatch) {
|
||||||
insertDiscardingMembers(exactMatchItemsData, item, interpretationMetadata);
|
insertDiscardingMembers(exactMatchItemsData, item, interpretationMetadata);
|
||||||
}
|
}
|
||||||
|
if (exactMatchOnTarget) {
|
||||||
|
insertDiscardingMembers(exactMatchOnTargetItemsData, item, interpretationMetadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (logger.isDebugEnabled()) {
|
if (logger.isTraceEnabled()) {
|
||||||
List<Class<? extends Command>> commandTypes = commandSupplier != null
|
List<Class<? extends Command>> commandTypes = commandSupplier != null
|
||||||
? commandSupplier.getCommandClasses(null)
|
? commandSupplier.getCommandClasses(null)
|
||||||
: List.of();
|
: List.of();
|
||||||
@ -792,30 +952,48 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
? " that accept " + commandTypes.stream().map(Class::getSimpleName).distinct()
|
? " that accept " + commandTypes.stream().map(Class::getSimpleName).distinct()
|
||||||
.collect(Collectors.joining(" or "))
|
.collect(Collectors.joining(" or "))
|
||||||
: "";
|
: "";
|
||||||
logger.debug("Partial matched items against {}{}: {}", labelFragments, typeDetails,
|
logger.trace("Partial matched items against {}{}: {}", labelFragments, typeDetails,
|
||||||
itemsData.keySet().stream().map(Item::getName).collect(Collectors.joining(", ")));
|
itemsData.keySet().stream().map(Item::getName).collect(Collectors.joining(", ")));
|
||||||
logger.debug("Exact matched items against {}{}: {}", labelFragments, typeDetails,
|
logger.trace("Exact matched items against {}{}: {}", labelFragments, typeDetails,
|
||||||
exactMatchItemsData.keySet().stream().map(Item::getName).collect(Collectors.joining(", ")));
|
exactMatchItemsData.keySet().stream().map(Item::getName).collect(Collectors.joining(", ")));
|
||||||
|
logger.trace("Exact matched on target items against {}{}: {}", labelFragments, typeDetails,
|
||||||
|
exactMatchOnTargetItemsData.keySet().stream().map(Item::getName).collect(Collectors.joining(", ")));
|
||||||
}
|
}
|
||||||
@Nullable
|
@Nullable
|
||||||
String locationContext = context.locationItem();
|
String locationContext = context.locationItem();
|
||||||
if (locationContext != null && itemsData.size() > 1) {
|
if (locationContext != null && itemsData.size() > 1) {
|
||||||
logger.debug("Filtering {} matched items based on location '{}'", itemsData.size(), locationContext);
|
logger.trace("Filtering {} matched items based on location '{}'", itemsData.size(), locationContext);
|
||||||
Item matchByLocation = filterMatchedItemsByLocation(itemsData, locationContext);
|
Item matchByLocation = filterMatchedItemsByLocation(itemsData, locationContext);
|
||||||
if (matchByLocation != null) {
|
if (matchByLocation != null) {
|
||||||
return List.of(matchByLocation);
|
return List.of(matchByLocation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (locationContext != null && exactMatchItemsData.size() > 1) {
|
if (locationContext != null && exactMatchItemsData.size() > 1) {
|
||||||
logger.debug("Filtering {} exact matched items based on location '{}'", exactMatchItemsData.size(),
|
logger.trace("Filtering {} exact matched items based on location '{}'", exactMatchItemsData.size(),
|
||||||
locationContext);
|
locationContext);
|
||||||
Item matchByLocation = filterMatchedItemsByLocation(exactMatchItemsData, locationContext);
|
Item matchByLocation = filterMatchedItemsByLocation(exactMatchItemsData, locationContext);
|
||||||
if (matchByLocation != null) {
|
if (matchByLocation != null) {
|
||||||
return List.of(matchByLocation);
|
return List.of(matchByLocation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new ArrayList<>(itemsData.size() != 1 && exactMatchItemsData.size() == 1 ? exactMatchItemsData.keySet()
|
if (locationContext != null && exactMatchOnTargetItemsData.size() > 1) {
|
||||||
: itemsData.keySet());
|
logger.trace("Filtering {} exact matched on target items based on location '{}'",
|
||||||
|
exactMatchOnTargetItemsData.size(), locationContext);
|
||||||
|
Item matchByLocation = filterMatchedItemsByLocation(exactMatchOnTargetItemsData, locationContext);
|
||||||
|
if (matchByLocation != null) {
|
||||||
|
return List.of(matchByLocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (itemsData.size() == 1) {
|
||||||
|
return new ArrayList<>(itemsData.keySet());
|
||||||
|
}
|
||||||
|
if (exactMatchItemsData.size() == 1) {
|
||||||
|
return new ArrayList<>(exactMatchItemsData.keySet());
|
||||||
|
}
|
||||||
|
if (exactMatchOnTargetItemsData.size() == 1) {
|
||||||
|
return new ArrayList<>(exactMatchOnTargetItemsData.keySet());
|
||||||
|
}
|
||||||
|
return new ArrayList<>(itemsData.keySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@ -825,7 +1003,7 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
if (itemsFilteredByLocation.size() != 1) {
|
if (itemsFilteredByLocation.size() != 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
logger.debug("Unique match by location found in '{}', taking prevalence", locationContext);
|
logger.trace("Unique match by location found in '{}', taking prevalence", locationContext);
|
||||||
return itemsFilteredByLocation.get(0).getKey();
|
return itemsFilteredByLocation.get(0).getKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -847,96 +1025,195 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
* @return resulting tokens
|
* @return resulting tokens
|
||||||
*/
|
*/
|
||||||
protected List<String> tokenize(Locale locale, @Nullable String text) {
|
protected List<String> tokenize(Locale locale, @Nullable String text) {
|
||||||
List<String> parts = new ArrayList<>();
|
|
||||||
if (text == null) {
|
if (text == null) {
|
||||||
return parts;
|
return List.of();
|
||||||
}
|
}
|
||||||
String[] split;
|
String specialCharactersRegex;
|
||||||
if (Locale.FRENCH.getLanguage().equalsIgnoreCase(locale.getLanguage())) {
|
if (Locale.FRENCH.getLanguage().equalsIgnoreCase(locale.getLanguage())) {
|
||||||
split = text.toLowerCase(locale).replaceAll("[\\']", " ").replaceAll("[^\\w\\sàâäçéèêëîïôùûü]", " ")
|
specialCharactersRegex = "[^\\w\\sàâäçéèêëîïôùûü$|?]";
|
||||||
.split("\\s");
|
|
||||||
} else if ("es".equalsIgnoreCase(locale.getLanguage())) {
|
} else if ("es".equalsIgnoreCase(locale.getLanguage())) {
|
||||||
split = text.toLowerCase(locale).replaceAll("[\\']", " ").replaceAll("[^\\w\\sáéíóúïüñç]", " ")
|
specialCharactersRegex = "[^\\w\\sáéíóúïüñç$|?]";
|
||||||
.split("\\s");
|
|
||||||
} else {
|
} else {
|
||||||
split = text.toLowerCase(locale).replaceAll("[\\']", "").replaceAll("[^\\w\\s]", " ").split("\\s");
|
specialCharactersRegex = "[^\\w\\s$|?]";
|
||||||
}
|
}
|
||||||
for (String s : split) {
|
return Arrays.stream(text.toLowerCase(locale) //
|
||||||
String part = s.trim();
|
.replaceAll("[\\']", "") //
|
||||||
if (part.length() > 0) {
|
.replaceAll(specialCharactersRegex, " ") //
|
||||||
parts.add(part);
|
.split("\\s")) //
|
||||||
}
|
.filter(i -> !i.isBlank()) //
|
||||||
}
|
.map(String::trim) //
|
||||||
return parts;
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a rule as text into a {@link Rule} instance.
|
* Parses a rule as text into a {@link Rule} instance.
|
||||||
*
|
* <p>
|
||||||
* The rule text should be a list of space separated expressions,
|
* The rule text should be a list of space separated expressions,
|
||||||
* one of them but not the first should be the character '*' (which indicates dynamic part to capture),
|
* one of them but not the first should be the character '*' (which indicates dynamic part to capture),
|
||||||
* the other expressions can be conformed by a single word, alternative words separated by '|',
|
* the other expressions can be conformed by a single word, alternative words separated by '|',
|
||||||
* and can be marked as optional by adding '?' at the end.
|
* and can be marked as optional by adding '?' at the end.
|
||||||
* There must be at least one non-optional expression at the beginning of the rule.
|
* There must be at least one non-optional expression at the beginning of the rule.
|
||||||
*
|
* <p>
|
||||||
* An example of a valid text will be 'watch * on|at? the tv'.
|
* An example of a valid text will be 'watch * on|at? the tv'.
|
||||||
*
|
*
|
||||||
* @param item will be the target of the rule.
|
* @param item will be the target of the rule.
|
||||||
* @param ruleText the text to parse into a {@link Rule}
|
* @param ruleText the text to parse into a {@link Rule}
|
||||||
*
|
* @param metadata voice-system metadata.
|
||||||
* @return The created rule.
|
* @return The created rule.
|
||||||
*/
|
*/
|
||||||
protected @Nullable Rule parseItemCustomRule(Item item, String ruleText) {
|
protected List<Rule> parseItemCustomRules(Locale locale, Item item, String ruleText, Metadata metadata) {
|
||||||
String[] ruleParts = ruleText.split("\\*");
|
boolean isTemplate = ConfigParser.valueAsOrElse(metadata.getConfiguration().get(IS_TEMPLATE_CONFIGURATION),
|
||||||
Expression headExpression;
|
Boolean.class, false);
|
||||||
@Nullable
|
boolean isSilent = ConfigParser.valueAsOrElse(metadata.getConfiguration().get(IS_SILENT_CONFIGURATION),
|
||||||
Expression tailExpression = null;
|
Boolean.class, false);
|
||||||
|
boolean isItemRule = false;
|
||||||
|
boolean isCommandRule = false;
|
||||||
|
boolean isDynamicRule = false;
|
||||||
|
if (ruleText.startsWith(NAME_TOKEN) || ruleText.startsWith(DYN_CMD_TOKEN)) {
|
||||||
|
logger.warn("Rule can not start with {} or {}: {}", NAME_TOKEN, DYN_CMD_TOKEN, ruleText);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
boolean containsMultiple = CUSTOM_RULE_TOKENS.stream().anyMatch(token -> {
|
||||||
|
int firstIndex = token.indexOf(token);
|
||||||
|
return firstIndex != -1 && token.indexOf(token, firstIndex + 1) != -1;
|
||||||
|
});
|
||||||
|
if (containsMultiple) {
|
||||||
|
logger.warn("Rule can not contains {}, {} or {} multiple times: {}", NAME_TOKEN, CMD_TOKEN, DYN_CMD_TOKEN,
|
||||||
|
ruleText);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
if (ruleText.contains(NAME_TOKEN)) {
|
||||||
|
isItemRule = true;
|
||||||
|
}
|
||||||
|
if (ruleText.contains(CMD_TOKEN)) {
|
||||||
|
isCommandRule = true;
|
||||||
|
}
|
||||||
|
if (ruleText.contains(DYN_CMD_TOKEN)) {
|
||||||
|
isDynamicRule = true;
|
||||||
|
}
|
||||||
|
if (isCommandRule && isDynamicRule) {
|
||||||
|
logger.warn("Rule can not contain {} and {}: {}", CMD_TOKEN, DYN_CMD_TOKEN, ruleText);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
if (!isCommandRule && !isDynamicRule) {
|
||||||
|
logger.warn("Rule should contain {} or {}: {}", CMD_TOKEN, DYN_CMD_TOKEN, ruleText);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (ruleText.startsWith("*") || !ruleText.contains(" *") || ruleParts.length > 2) {
|
ItemFilter itemsFilter = isTemplate
|
||||||
throw new ParseException("Incorrect usage of character '*'", 0);
|
? ItemFilter.forSimilarItem(item,
|
||||||
}
|
metadataRegistry.get(new MetadataKey(SEMANTICS_NAMESPACE, item.getName())))
|
||||||
List<Expression> headExpressions = new ArrayList<>();
|
: ItemFilter.forItem(item);
|
||||||
boolean headHasNonOptional = true;
|
if (isItemRule && isCommandRule) {
|
||||||
for (String s : ruleParts[0].split("\\s")) {
|
String[] ruleParts = ruleText.split(Pattern.quote(NAME_TOKEN));
|
||||||
if (!s.isBlank()) {
|
String headPart = ruleParts[0];
|
||||||
String trim = s.trim();
|
@Nullable
|
||||||
Expression expression = parseItemRuleTokenText(trim);
|
String tailPart = ruleParts.length > 1 ? ruleParts[1] : null;
|
||||||
if (expression instanceof ExpressionCardinality expressionCardinality) {
|
var itemCMDs = item.getCommandDescription();
|
||||||
if (!expressionCardinality.isAtLeastOne()) {
|
if (itemCMDs == null || itemCMDs.getCommandOptions().isEmpty()) {
|
||||||
headHasNonOptional = false;
|
throw new ParseException("Missing item " + item.getName() + " command description.", 0);
|
||||||
}
|
}
|
||||||
} else {
|
ArrayList<Rule> rules = new ArrayList<>();
|
||||||
headHasNonOptional = false;
|
for (var cmd : itemCMDs.getCommandOptions()) {
|
||||||
|
String label = cmd.getLabel();
|
||||||
|
if (label == null) {
|
||||||
|
label = cmd.getCommand();
|
||||||
}
|
}
|
||||||
headExpressions.add(expression);
|
String value = cmd.getCommand();
|
||||||
|
String[] cmdInfo = new String[] { label, value };
|
||||||
|
var head = Objects.requireNonNull(parseCustomRuleSegment(locale, headPart, false, item, cmdInfo));
|
||||||
|
var tail = tailPart != null ? parseCustomRuleSegment(locale, tailPart, true, item, cmdInfo) : null;
|
||||||
|
rules.add(restrictedItemRule(itemsFilter, head, tail, isSilent));
|
||||||
}
|
}
|
||||||
}
|
return rules;
|
||||||
if (headHasNonOptional) {
|
} else if (isItemRule && isDynamicRule) {
|
||||||
throw new ParseException("Rule head only contains optional expressions", 0);
|
String[] ruleParts = Arrays.stream(ruleText.split(Pattern.quote(NAME_TOKEN))) //
|
||||||
}
|
.map(s -> s.split(Pattern.quote(DYN_CMD_TOKEN))) //
|
||||||
headExpression = seq(headExpressions.toArray());
|
.flatMap(Arrays::stream).toArray(String[]::new);
|
||||||
if (ruleParts.length == 2) {
|
if (ruleParts.length > 3) {
|
||||||
List<Expression> tailExpressions = new ArrayList<>();
|
throw new ParseException("Incorrectly rule segments: " + ruleText, 0);
|
||||||
for (String s : ruleParts[1].split("\\s")) {
|
}
|
||||||
if (!s.isBlank()) {
|
Expression head = Objects
|
||||||
String trim = s.trim();
|
.requireNonNull(parseCustomRuleSegment(locale, ruleParts[0], false, item, null));
|
||||||
Expression expression = parseItemRuleTokenText(trim);
|
Expression mid = Objects
|
||||||
tailExpressions.add(expression);
|
.requireNonNull(parseCustomRuleSegment(locale, ruleParts[1], false, item, null));
|
||||||
|
@Nullable
|
||||||
|
Expression tail = ruleParts.length > 2 ? parseCustomRuleSegment(locale, ruleParts[2], true, item, null)
|
||||||
|
: null;
|
||||||
|
boolean isNameFirst = ruleText.indexOf(NAME_TOKEN) < ruleText.indexOf(DYN_CMD_TOKEN);
|
||||||
|
return List.of(restrictedDynamicItemRule(item, itemsFilter, head, mid, tail, isNameFirst, isSilent));
|
||||||
|
} else if (isDynamicRule) {
|
||||||
|
String[] ruleParts = ruleText.split(Pattern.quote(DYN_CMD_TOKEN));
|
||||||
|
if (ruleParts.length > 2) {
|
||||||
|
throw new ParseException("Incorrectly rule segments: " + ruleText, 0);
|
||||||
|
}
|
||||||
|
Expression head = Objects
|
||||||
|
.requireNonNull(parseCustomRuleSegment(locale, ruleParts[0], false, item, null));
|
||||||
|
@Nullable
|
||||||
|
Expression tail = ruleParts.length > 1 ? parseCustomRuleSegment(locale, ruleParts[1], true, item, null)
|
||||||
|
: null;
|
||||||
|
return List.of(customDynamicRule(item, itemsFilter, head, tail, isSilent));
|
||||||
|
} else if (isCommandRule) {
|
||||||
|
var itemCMDs = item.getCommandDescription();
|
||||||
|
if (itemCMDs == null || itemCMDs.getCommandOptions().isEmpty()) {
|
||||||
|
throw new ParseException("Missing item " + item.getName() + " command description.", 0);
|
||||||
|
}
|
||||||
|
ArrayList<Rule> rules = new ArrayList<>();
|
||||||
|
for (var cmd : itemCMDs.getCommandOptions()) {
|
||||||
|
String label = cmd.getLabel();
|
||||||
|
if (label == null) {
|
||||||
|
label = cmd.getCommand();
|
||||||
}
|
}
|
||||||
|
String value = cmd.getCommand();
|
||||||
|
String[] cmdInfo = new String[] { label, value };
|
||||||
|
var expression = Objects
|
||||||
|
.requireNonNull(parseCustomRuleSegment(locale, ruleText, false, item, cmdInfo));
|
||||||
|
rules.add(customCommandRule(itemsFilter, expression, isSilent));
|
||||||
}
|
}
|
||||||
if (!tailExpressions.isEmpty()) {
|
return rules;
|
||||||
tailExpression = seq(tailExpressions.toArray());
|
} else {
|
||||||
}
|
throw new ParseException("Unable to parse rule: " + ruleText, 0);
|
||||||
}
|
}
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
logger.warn("Unable to parse item {} rule '{}': {}", item.getName(), ruleText, e.getMessage());
|
logger.warn("Unable to parse item {} rule '{}': {}", item.getName(), ruleText, e.getMessage());
|
||||||
return null;
|
return List.of();
|
||||||
}
|
}
|
||||||
return customItemRule(item, headExpression, tailExpression);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Expression parseItemRuleTokenText(String tokenText) throws ParseException {
|
private @Nullable Expression parseCustomRuleSegment(Locale locale, String text, boolean allowEmpty, Item item,
|
||||||
|
String @Nullable [] cmdData) throws ParseException {
|
||||||
|
List<Expression> subExpressions = new ArrayList<>();
|
||||||
|
Expression sequenceExpression;
|
||||||
|
boolean headHasNonOptional = true;
|
||||||
|
for (String s : tokenize(locale, text)) {
|
||||||
|
String trim = s.trim();
|
||||||
|
Expression expression = parseItemRuleTokenText(locale, trim, item, cmdData);
|
||||||
|
if (expression instanceof ExpressionCardinality expressionCardinality) {
|
||||||
|
if (!expressionCardinality.isAtLeastOne()) {
|
||||||
|
headHasNonOptional = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headHasNonOptional = false;
|
||||||
|
}
|
||||||
|
subExpressions.add(expression);
|
||||||
|
}
|
||||||
|
if (headHasNonOptional) {
|
||||||
|
if (allowEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new ParseException("Rule segment contains only optional elements: " + text, 0);
|
||||||
|
}
|
||||||
|
sequenceExpression = seq(subExpressions.toArray());
|
||||||
|
return sequenceExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression parseItemRuleTokenText(Locale locale, String tokenText, Item item, String @Nullable [] cmdData)
|
||||||
|
throws ParseException {
|
||||||
boolean optional = false;
|
boolean optional = false;
|
||||||
|
if (tokenText.equals(CMD_TOKEN) && cmdData != null) {
|
||||||
|
Expression cmdExpression = seq(tokenize(locale, cmdData[0]).toArray());
|
||||||
|
return cmd(cmdExpression, TypeParser.parseCommand(item.getAcceptedCommandTypes(), cmdData[1]));
|
||||||
|
}
|
||||||
if (tokenText.endsWith("?")) {
|
if (tokenText.endsWith("?")) {
|
||||||
tokenText = tokenText.substring(0, tokenText.length() - 1);
|
tokenText = tokenText.substring(0, tokenText.length() - 1);
|
||||||
optional = true;
|
optional = true;
|
||||||
@ -1143,7 +1420,7 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String getGrammar() {
|
protected String getGrammar() {
|
||||||
Rule[] rules = getRules(language.getLocale());
|
Rule[] rules = getRules(language.getLocale());
|
||||||
identifiers.addAll(getAllItemTokens(language.getLocale()));
|
identifiers.addAll(getAllItemTokens(language.getLocale()));
|
||||||
for (Rule rule : rules) {
|
for (Rule rule : rules) {
|
||||||
@ -1224,4 +1501,64 @@ public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInter
|
|||||||
return List.of(command.getClass());
|
return List.of(command.getClass());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record TextCommandSupplier(String text, List<Class<? extends Command>> allowedCommands,
|
||||||
|
Map<String, String> transformations) implements ItemCommandSupplier {
|
||||||
|
@Override
|
||||||
|
public @Nullable Command getItemCommand(Item item) {
|
||||||
|
return TypeParser.parseCommand(item.getAcceptedCommandTypes(), transformations.getOrDefault(text, text));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCommandLabel() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Class<? extends Command>> getCommandClasses(@Nullable Item ignored) {
|
||||||
|
return allowedCommands;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ItemFilter(Set<String> itemNames, Set<String> excludedItemNames, Set<String> itemTags,
|
||||||
|
Set<String> itemSemantics) {
|
||||||
|
|
||||||
|
private static final ItemFilter allInstance = new ItemFilter(Set.of(), Set.of(), Set.of(), Set.of());
|
||||||
|
public boolean filterItem(Item item, MetadataRegistry metadataRegistry) {
|
||||||
|
if (!itemNames.isEmpty() && !itemNames.contains(item.getName())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!excludedItemNames.isEmpty() && excludedItemNames.contains(item.getName())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!itemTags.isEmpty()
|
||||||
|
&& (item.getTags().size() != itemTags.size() || !item.getTags().containsAll(itemTags))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Metadata semanticsMetadata = metadataRegistry.get(new MetadataKey(SEMANTICS_NAMESPACE, item.getName()));
|
||||||
|
if (!itemSemantics.isEmpty()
|
||||||
|
&& (semanticsMetadata == null || !itemSemantics.contains(semanticsMetadata.getValue()))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ItemFilter all() {
|
||||||
|
return allInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ItemFilter forItem(Item item) {
|
||||||
|
return new ItemFilter(Set.of(item.getName()), Set.of(), Set.of(), Set.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ItemFilter forItems(Set<Item> item) {
|
||||||
|
return new ItemFilter(item.stream().map(Item::getName).collect(Collectors.toSet()), Set.of(), Set.of(),
|
||||||
|
Set.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ItemFilter forSimilarItem(Item item, @Nullable Metadata semantic) {
|
||||||
|
return new ItemFilter(Set.of(), Set.of(item.getName()), item.getTags(),
|
||||||
|
semantic != null ? Set.of(semantic.getValue()) : Set.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.core.voice.text;
|
package org.openhab.core.voice.text;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
@ -28,17 +27,20 @@ import org.eclipse.jdt.annotation.Nullable;
|
|||||||
public abstract class Rule {
|
public abstract class Rule {
|
||||||
|
|
||||||
private final Expression expression;
|
private final Expression expression;
|
||||||
private final List<String> allowedItemNames;
|
private final AbstractRuleBasedInterpreter.ItemFilter itemFilter;
|
||||||
|
private final boolean isSilent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new instance.
|
* Constructs a new instance.
|
||||||
*
|
*
|
||||||
* @param expression the expression that has to parse successfully, before {@link #interpretAST} is called
|
* @param expression the expression that has to parse successfully, before {@link #interpretAST} is called
|
||||||
* @param allowedItemNames List of allowed items or empty for disabled.
|
* @param itemFilter Filters allowed items for rule.
|
||||||
|
* @param isSilent Rule will emit no response on success.
|
||||||
*/
|
*/
|
||||||
public Rule(Expression expression, List<String> allowedItemNames) {
|
public Rule(Expression expression, AbstractRuleBasedInterpreter.ItemFilter itemFilter, boolean isSilent) {
|
||||||
this.expression = expression;
|
this.expression = expression;
|
||||||
this.allowedItemNames = allowedItemNames;
|
this.itemFilter = itemFilter;
|
||||||
|
this.isSilent = isSilent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -55,7 +57,7 @@ public abstract class Rule {
|
|||||||
InterpretationResult execute(ResourceBundle language, TokenList list, @Nullable String locationItem) {
|
InterpretationResult execute(ResourceBundle language, TokenList list, @Nullable String locationItem) {
|
||||||
ASTNode node = expression.parse(language, list);
|
ASTNode node = expression.parse(language, list);
|
||||||
if (node.isSuccess() && node.getRemainingTokens().eof()) {
|
if (node.isSuccess() && node.getRemainingTokens().eof()) {
|
||||||
return interpretAST(language, node, new InterpretationContext(this.allowedItemNames, locationItem));
|
return interpretAST(language, node, new InterpretationContext(this.itemFilter, isSilent, locationItem));
|
||||||
}
|
}
|
||||||
return InterpretationResult.SYNTAX_ERROR;
|
return InterpretationResult.SYNTAX_ERROR;
|
||||||
}
|
}
|
||||||
@ -72,9 +74,10 @@ public abstract class Rule {
|
|||||||
*
|
*
|
||||||
* @author Miguel Álvarez - Initial contribution
|
* @author Miguel Álvarez - Initial contribution
|
||||||
*
|
*
|
||||||
* @param allowedItems List of item names to restrict rule compatibility to, empty for disabled.
|
* @param itemFilter Restricts rule compatibility to allowed items.
|
||||||
* @param locationItem Location item to prioritize item matches or null.
|
* @param locationItem Location item to prioritize item matches or null.
|
||||||
*/
|
*/
|
||||||
public record InterpretationContext(List<String> allowedItems, @Nullable String locationItem) {
|
public record InterpretationContext(AbstractRuleBasedInterpreter.ItemFilter itemFilter, boolean isSilent,
|
||||||
|
@Nullable String locationItem) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,11 @@ import static org.mockito.Mockito.reset;
|
|||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.openhab.core.voice.internal.text.StandardInterpreter.VOICE_SYSTEM_NAMESPACE;
|
||||||
|
import static org.openhab.core.voice.text.AbstractRuleBasedInterpreter.IS_SILENT_CONFIGURATION;
|
||||||
|
import static org.openhab.core.voice.text.AbstractRuleBasedInterpreter.IS_TEMPLATE_CONFIGURATION;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -136,7 +140,8 @@ public class StandardInterpreterTest {
|
|||||||
MetadataKey computerMetadataKey = new MetadataKey("synonyms", computerItem.getName());
|
MetadataKey computerMetadataKey = new MetadataKey("synonyms", computerItem.getName());
|
||||||
when(metadataRegistryMock.get(computerMetadataKey))
|
when(metadataRegistryMock.get(computerMetadataKey))
|
||||||
.thenReturn(new Metadata(computerMetadataKey, "PC,Bedroom PC", null));
|
.thenReturn(new Metadata(computerMetadataKey, "PC,Bedroom PC", null));
|
||||||
when(metadataRegistryMock.get(new MetadataKey("voice-system", computerItem.getName()))).thenReturn(null);
|
when(metadataRegistryMock.get(new MetadataKey(VOICE_SYSTEM_NAMESPACE, computerItem.getName())))
|
||||||
|
.thenReturn(null);
|
||||||
List<Item> items = List.of(computerItem);
|
List<Item> items = List.of(computerItem);
|
||||||
when(itemRegistryMock.getItems()).thenReturn(items);
|
when(itemRegistryMock.getItems()).thenReturn(items);
|
||||||
assertEquals(OK_RESPONSE, standardInterpreter.interpret(Locale.ENGLISH, "turn off computer"));
|
assertEquals(OK_RESPONSE, standardInterpreter.interpret(Locale.ENGLISH, "turn off computer"));
|
||||||
@ -154,17 +159,11 @@ public class StandardInterpreterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void allowUseItemDescription() throws InterpretationException {
|
public void allowUseItemDescription() throws InterpretationException {
|
||||||
var cmdDescription = new CommandDescription() {
|
|
||||||
@Override
|
|
||||||
public List<CommandOption> getCommandOptions() {
|
|
||||||
return List.of(new CommandOption("10", "low"), new CommandOption("50", "medium"),
|
|
||||||
new CommandOption("90", "high"), new CommandOption("100", "high two"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var brightness = new DimmerItem("brightness") {
|
var brightness = new DimmerItem("brightness") {
|
||||||
@Override
|
@Override
|
||||||
public @Nullable CommandDescription getCommandDescription() {
|
public @Nullable CommandDescription getCommandDescription() {
|
||||||
return cmdDescription;
|
return () -> List.of(new CommandOption("10", "low"), new CommandOption("50", "medium"),
|
||||||
|
new CommandOption("90", "high"), new CommandOption("100", "high two"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -193,14 +192,116 @@ public class StandardInterpreterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void allowUseCustomCommands() throws InterpretationException {
|
public void allowUseCustomItemCommands() throws InterpretationException {
|
||||||
var virtualItem = new StringItem("virtual");
|
var tvItem = new StringItem("virtual") {
|
||||||
MetadataKey voiceMetadataKey = new MetadataKey("voice-system", virtualItem.getName());
|
@Override
|
||||||
|
public @Nullable CommandDescription getCommandDescription() {
|
||||||
|
return () -> List.of(new CommandOption("KEY_4", "channel 4"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable CommandDescription getCommandDescription(@Nullable Locale locale) {
|
||||||
|
return getCommandDescription();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tvItem.setLabel("tv");
|
||||||
|
MetadataKey voiceMetadataKey = new MetadataKey(VOICE_SYSTEM_NAMESPACE, tvItem.getName());
|
||||||
when(metadataRegistryMock.get(voiceMetadataKey))
|
when(metadataRegistryMock.get(voiceMetadataKey))
|
||||||
.thenReturn(new Metadata(voiceMetadataKey, "watch|play * on|at? the? tv", null));
|
.thenReturn(new Metadata(voiceMetadataKey, "watch|play $cmd$ on|at the? $name$", null));
|
||||||
List<Item> items = List.of(virtualItem);
|
List<Item> items = List.of(tvItem);
|
||||||
when(itemRegistryMock.getItems()).thenReturn(items);
|
when(itemRegistryMock.getItems()).thenReturn(items);
|
||||||
assertEquals(OK_RESPONSE, standardInterpreter.interpret(Locale.ENGLISH, "watch channel 4 on the tv"));
|
assertEquals(OK_RESPONSE, standardInterpreter.interpret(Locale.ENGLISH, "watch channel 4 on the tv"));
|
||||||
|
verify(eventPublisherMock, times(1))
|
||||||
|
.post(ItemEventFactory.createCommandEvent(tvItem.getName(), new StringType("KEY_4")));
|
||||||
|
reset(eventPublisherMock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowUseCustomCommandCommands() throws InterpretationException {
|
||||||
|
var tvItem = new StringItem("tv") {
|
||||||
|
@Override
|
||||||
|
public @Nullable CommandDescription getCommandDescription() {
|
||||||
|
return () -> List.of(new CommandOption("KEY_4", "channel 4"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable CommandDescription getCommandDescription(@Nullable Locale locale) {
|
||||||
|
return getCommandDescription();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
MetadataKey voiceMetadataKey = new MetadataKey(VOICE_SYSTEM_NAMESPACE, tvItem.getName());
|
||||||
|
when(metadataRegistryMock.get(voiceMetadataKey))
|
||||||
|
.thenReturn(new Metadata(voiceMetadataKey, "watch|play $cmd$ on|at the? tv", null));
|
||||||
|
List<Item> items = List.of(tvItem);
|
||||||
|
when(itemRegistryMock.getItems()).thenReturn(items);
|
||||||
|
assertEquals(OK_RESPONSE, standardInterpreter.interpret(Locale.ENGLISH, "watch channel 4 on the tv"));
|
||||||
|
verify(eventPublisherMock, times(1))
|
||||||
|
.post(ItemEventFactory.createCommandEvent(tvItem.getName(), new StringType("KEY_4")));
|
||||||
|
reset(eventPublisherMock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowUseCustomItemDynamicCommands() throws InterpretationException {
|
||||||
|
var tvItem = new StringItem("tv");
|
||||||
|
tvItem.setLabel("tv");
|
||||||
|
MetadataKey voiceMetadataKey = new MetadataKey(VOICE_SYSTEM_NAMESPACE, tvItem.getName());
|
||||||
|
when(metadataRegistryMock.get(voiceMetadataKey))
|
||||||
|
.thenReturn(new Metadata(voiceMetadataKey, "watch|play $*$ on|at the? $name$", null));
|
||||||
|
List<Item> items = List.of(tvItem);
|
||||||
|
when(itemRegistryMock.getItems()).thenReturn(items);
|
||||||
|
assertEquals(OK_RESPONSE, standardInterpreter.interpret(Locale.ENGLISH, "watch channel 4 on the tv"));
|
||||||
|
verify(eventPublisherMock, times(1))
|
||||||
|
.post(ItemEventFactory.createCommandEvent(tvItem.getName(), new StringType("channel 4")));
|
||||||
|
reset(eventPublisherMock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowUseCommandsWithoutAnswer() throws InterpretationException {
|
||||||
|
var tvItem = new StringItem("tv");
|
||||||
|
tvItem.setLabel("tv");
|
||||||
|
MetadataKey voiceMetadataKey = new MetadataKey(VOICE_SYSTEM_NAMESPACE, tvItem.getName());
|
||||||
|
HashMap<String, Object> configuration = new HashMap<>();
|
||||||
|
configuration.put(IS_SILENT_CONFIGURATION, true);
|
||||||
|
when(metadataRegistryMock.get(voiceMetadataKey))
|
||||||
|
.thenReturn(new Metadata(voiceMetadataKey, "watch|play $*$ on|at the? $name$", configuration));
|
||||||
|
List<Item> items = List.of(tvItem);
|
||||||
|
when(itemRegistryMock.getItems()).thenReturn(items);
|
||||||
|
assertEquals("", standardInterpreter.interpret(Locale.ENGLISH, "watch channel 4 on the tv"));
|
||||||
|
verify(eventPublisherMock, times(1))
|
||||||
|
.post(ItemEventFactory.createCommandEvent(tvItem.getName(), new StringType("channel 4")));
|
||||||
|
reset(eventPublisherMock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowUseCommandsFromTemplate() throws InterpretationException {
|
||||||
|
var virtualItem = new StringItem("virtual");
|
||||||
|
virtualItem.setLabel("tv rule");
|
||||||
|
virtualItem.addTag("tv");
|
||||||
|
var tvItem = new StringItem("tv");
|
||||||
|
tvItem.setLabel("tv");
|
||||||
|
tvItem.addTag("tv");
|
||||||
|
MetadataKey voiceMetadataKey = new MetadataKey(VOICE_SYSTEM_NAMESPACE, virtualItem.getName());
|
||||||
|
HashMap<String, Object> configuration = new HashMap<>();
|
||||||
|
configuration.put(IS_TEMPLATE_CONFIGURATION, true);
|
||||||
|
when(metadataRegistryMock.get(voiceMetadataKey))
|
||||||
|
.thenReturn(new Metadata(voiceMetadataKey, "watch|play $*$ on|at the? $name$", configuration));
|
||||||
|
List<Item> items = List.of(virtualItem, tvItem);
|
||||||
|
when(itemRegistryMock.getItems()).thenReturn(items);
|
||||||
|
assertEquals(OK_RESPONSE, standardInterpreter.interpret(Locale.ENGLISH, "watch channel 4 on the tv"));
|
||||||
|
verify(eventPublisherMock, times(1))
|
||||||
|
.post(ItemEventFactory.createCommandEvent(tvItem.getName(), new StringType("channel 4")));
|
||||||
|
reset(eventPublisherMock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void allowUseCustomDynamicCommands() throws InterpretationException {
|
||||||
|
var virtualItem = new StringItem("tv");
|
||||||
|
MetadataKey voiceMetadataKey = new MetadataKey(VOICE_SYSTEM_NAMESPACE, virtualItem.getName());
|
||||||
|
when(metadataRegistryMock.get(voiceMetadataKey))
|
||||||
|
.thenReturn(new Metadata(voiceMetadataKey, "watch|play $*$", null));
|
||||||
|
List<Item> items = List.of(virtualItem);
|
||||||
|
when(itemRegistryMock.getItems()).thenReturn(items);
|
||||||
|
assertEquals(OK_RESPONSE, standardInterpreter.interpret(Locale.ENGLISH, "watch channel 4"));
|
||||||
verify(eventPublisherMock, times(1))
|
verify(eventPublisherMock, times(1))
|
||||||
.post(ItemEventFactory.createCommandEvent(virtualItem.getName(), new StringType("channel 4")));
|
.post(ItemEventFactory.createCommandEvent(virtualItem.getName(), new StringType("channel 4")));
|
||||||
reset(eventPublisherMock);
|
reset(eventPublisherMock);
|
||||||
@ -225,9 +326,9 @@ public class StandardInterpreterTest {
|
|||||||
return getCommandDescription();
|
return getCommandDescription();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
MetadataKey voiceMetadataKey = new MetadataKey("voice-system", virtualItem.getName());
|
MetadataKey voiceMetadataKey = new MetadataKey(VOICE_SYSTEM_NAMESPACE, virtualItem.getName());
|
||||||
when(metadataRegistryMock.get(voiceMetadataKey))
|
when(metadataRegistryMock.get(voiceMetadataKey))
|
||||||
.thenReturn(new Metadata(voiceMetadataKey, "watch|play * on|at? the? tv", null));
|
.thenReturn(new Metadata(voiceMetadataKey, "watch|play $*$ on|at? the? tv", null));
|
||||||
List<Item> items = List.of(virtualItem);
|
List<Item> items = List.of(virtualItem);
|
||||||
when(itemRegistryMock.getItems()).thenReturn(items);
|
when(itemRegistryMock.getItems()).thenReturn(items);
|
||||||
assertEquals(OK_RESPONSE, standardInterpreter.interpret(Locale.ENGLISH, "watch channel 4 on the tv"));
|
assertEquals(OK_RESPONSE, standardInterpreter.interpret(Locale.ENGLISH, "watch channel 4 on the tv"));
|
||||||
|
Loading…
Reference in New Issue
Block a user