DSL Items Parser: Fix incorrect parsing of keywords encountered in the wrong context (#4928)

* DSL Items Parser: Fix incorrect parsing of keywords encountered in the wrong context

When specifying tag names that match one of the valid item types, e.g. `Switch`, the parser incorrectly treated it as a new start of an item definition.

Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
This commit is contained in:
jimtng
2025-09-21 14:14:24 +02:00
committed by GitHub
parent 00b6a476a7
commit 7c848bf891
16 changed files with 360 additions and 170 deletions
@@ -330,7 +330,14 @@ public class ModelRepositoryImpl implements ModelRepository {
final org.eclipse.emf.common.util.Diagnostic diagnostic = safeEmf
.call(() -> Diagnostician.INSTANCE.validate(resource.getContents().getFirst()));
for (org.eclipse.emf.common.util.Diagnostic d : diagnostic.getChildren()) {
warnings.add(d.getMessage());
if (d.getSeverity() == org.eclipse.emf.common.util.Diagnostic.ERROR) {
errors.add(d.getMessage());
} else {
warnings.add(d.getMessage());
}
}
if (!errors.isEmpty()) {
return false;
}
} catch (NullPointerException e) {
// see https://github.com/eclipse/smarthome/issues/3335
@@ -22,6 +22,7 @@ Import-Package: javax.measure,\
org.openhab.core.items,\
org.openhab.core.items.dto,\
org.openhab.core.items.fileconverter,\
org.openhab.core.library,\
org.openhab.core.library.items,\
org.openhab.core.library.types,\
org.openhab.core.thing.util,\
@@ -14,31 +14,18 @@ ItemModel:
;
ModelItem:
(ModelNormalItem | ModelGroupItem) name=ID
type=ModelItemType ('(' args+=(ID|STRING) (',' args+=(ID|STRING))* ')')?
name=ID
(label=STRING)?
('<' icon=Icon '>')?
('(' groups+=ID (',' groups+=ID)* ')')?
('(' groups+=ID (',' groups+=ID)* ')')?
('[' tags+=(ID|STRING) (',' tags+=(ID|STRING))* ']')?
('{' bindings+=ModelBinding (',' bindings+=ModelBinding)* '}')?
;
ModelGroupItem:
{ModelGroupItem} 'Group' (':' type=ModelItemType ( ':' function=ModelGroupFunction ('(' args+=(ID|STRING) (',' args+=(ID|STRING))* ')')?)?)?
;
enum ModelGroupFunction:
EQUALITY='EQUALITY' | AND='AND' | OR='OR' | NAND='NAND' | NOR='NOR' | XOR='XOR' | AVG='AVG' | MEDIAN='MEDIAN' | SUM='SUM' | MAX='MAX' | MIN='MIN' | COUNT='COUNT' | LATEST='LATEST' | EARLIEST='EARLIEST'
;
ModelNormalItem:
type=ModelItemType
('{' bindings+=ModelBinding (',' bindings+=ModelBinding)* '}')?
;
// Supports item types with up to 4 colon-separated segments, e.g., Group:Number:Dimension:MAX
ModelItemType:
BaseModelItemType | ('Number' (':' ID)?)
;
BaseModelItemType:
'Switch' | 'Rollershutter' | 'String' | 'Dimmer' | 'Contact' | 'DateTime' | 'Color' | 'Player' | 'Location' | 'Call' | 'Image'
ID (':' ID (':' ID (':' ID )?)?)?
;
ModelBinding:
@@ -56,7 +43,7 @@ ValueType returns ecore::EJavaObject:
STRING | NUMBER | BOOLEAN
;
BOOLEAN returns ecore::EBoolean:
BOOLEAN returns ecore::EBoolean:
'true' | 'false'
;
@@ -25,8 +25,10 @@ class ItemsFormatter extends AbstractDeclarativeFormatter {
@Inject extension ItemsGrammarAccess
override protected void configureFormatting(FormattingConfig c) {
c.setLinewrap(1, 1, 2).before(modelGroupItemRule)
c.setLinewrap(1, 1, 2).before(modelNormalItemRule)
c.setLinewrap(1, 1, 2).before(modelItemRule)
// No space between the item type and the opening parenthesis for the group function arguments
c.setNoSpace().between(modelItemTypeRule, modelItemAccess.leftParenthesisKeyword_1_0)
c.setNoSpace().withinKeywordPairs("<", ">")
c.setNoSpace().withinKeywordPairs("(", ")")
@@ -47,10 +47,7 @@ import org.openhab.core.model.item.BindingConfigParseException;
import org.openhab.core.model.item.BindingConfigReader;
import org.openhab.core.model.items.ItemModel;
import org.openhab.core.model.items.ModelBinding;
import org.openhab.core.model.items.ModelGroupFunction;
import org.openhab.core.model.items.ModelGroupItem;
import org.openhab.core.model.items.ModelItem;
import org.openhab.core.model.items.ModelNormalItem;
import org.openhab.core.types.StateDescriptionFragment;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.openhab.core.types.StateDescriptionFragmentProvider;
@@ -234,62 +231,54 @@ public class GenericItemProvider extends AbstractProvider<Item>
}
private @Nullable Item createItemFromModelItem(ModelItem modelItem, String modelName) {
Item item;
if (modelItem instanceof ModelGroupItem modelGroupItem) {
Item baseItem;
try {
baseItem = createItemOfType(modelGroupItem.getType(), modelGroupItem.getName());
} catch (IllegalArgumentException e) {
logger.debug("Error creating base item for group item '{}', item will be ignored: {}",
modelGroupItem.getName(), e.getMessage());
return null;
}
if (baseItem != null) {
// if the user did not specify a function the first value of the enum in xtext (EQUAL) will be used
ModelGroupFunction function = modelGroupItem.getFunction();
item = applyGroupFunction(baseItem, modelGroupItem, function);
} else {
item = new GroupItem(modelGroupItem.getName());
}
} else {
ModelNormalItem normalItem = (ModelNormalItem) modelItem;
try {
item = createItemOfType(normalItem.getType(), normalItem.getName());
} catch (IllegalArgumentException e) {
logger.debug("Error creating item '{}', item will be ignored: {}", normalItem.getName(),
e.getMessage());
return null;
}
String itemType = modelItem.getType();
if (itemType == null || itemType.isBlank()) {
logger.warn("Item '{}' has no type defined, ignoring it.", modelItem.getName());
return null;
}
if (item instanceof ActiveItem activeItem) {
String label = modelItem.getLabel();
String format = extractFormat(label);
if (format != null) {
label = label.substring(0, label.indexOf("[")).trim();
Map<String, String> formatters = Objects
.requireNonNull(stateFormattersMap.computeIfAbsent(modelName, k -> new HashMap<>()));
formatters.put(modelItem.getName(), format);
if (!isIsolatedModel(modelName)) {
stateDescriptionFragments.put(modelItem.getName(),
StateDescriptionFragmentBuilder.create().withPattern(format).build());
}
} else {
Map<String, String> formatters = stateFormattersMap.get(modelName);
if (formatters != null) {
formatters.remove(modelItem.getName());
if (formatters.isEmpty()) {
stateFormattersMap.remove(modelName);
try {
String[] itemTypeSegments = itemType.split(ItemUtil.EXTENSION_SEPARATOR);
String mainItemType = itemTypeSegments[0];
Item item = switch (mainItemType) {
case "Group" -> createGroupItem(modelItem, itemTypeSegments);
default -> createItemOfType(itemType, modelItem.getName());
};
if (item instanceof ActiveItem activeItem) {
String label = modelItem.getLabel();
String format = extractFormat(label);
if (format != null) {
label = label.substring(0, label.indexOf("[")).trim();
Map<String, String> formatters = Objects
.requireNonNull(stateFormattersMap.computeIfAbsent(modelName, k -> new HashMap<>()));
formatters.put(modelItem.getName(), format);
if (!isIsolatedModel(modelName)) {
stateDescriptionFragments.put(modelItem.getName(),
StateDescriptionFragmentBuilder.create().withPattern(format).build());
}
} else {
Map<String, String> formatters = stateFormattersMap.get(modelName);
if (formatters != null) {
formatters.remove(modelItem.getName());
if (formatters.isEmpty()) {
stateFormattersMap.remove(modelName);
}
}
if (!isIsolatedModel(modelName)) {
stateDescriptionFragments.remove(modelItem.getName());
}
}
if (!isIsolatedModel(modelName)) {
stateDescriptionFragments.remove(modelItem.getName());
}
activeItem.setLabel(label);
activeItem.setCategory(modelItem.getIcon());
assignTags(modelItem, activeItem);
return item;
} else {
return null;
}
activeItem.setLabel(label);
activeItem.setCategory(modelItem.getIcon());
assignTags(modelItem, activeItem);
return item;
} else {
} catch (IllegalArgumentException e) {
logger.debug("Error creating item '{}', item will be ignored: {}", modelItem.getName(), e.getMessage());
return null;
}
}
@@ -312,14 +301,14 @@ public class GenericItemProvider extends AbstractProvider<Item>
}
}
private GroupItem applyGroupFunction(Item baseItem, ModelGroupItem modelGroupItem, ModelGroupFunction function) {
private GroupItem applyGroupFunction(Item baseItem, ModelItem modelItem, String function) {
GroupFunctionDTO dto = new GroupFunctionDTO();
dto.name = function.getName();
dto.params = modelGroupItem.getArgs().toArray(new String[0]);
dto.name = function;
dto.params = modelItem.getArgs().toArray(new String[0]);
GroupFunction groupFunction = ItemDTOMapper.mapFunction(baseItem, dto);
return new GroupItem(modelGroupItem.getName(), baseItem, groupFunction);
return new GroupItem(modelItem.getName(), baseItem, groupFunction);
}
private void dispatchBindingsPerItemType(String[] itemTypes) {
@@ -529,6 +518,47 @@ public class GenericItemProvider extends AbstractProvider<Item>
return ret;
}
/**
* Creates a new GroupItem based on the given ModelItem and item type segments.
*
* @param modelItem The ModelItem to create the GroupItem from.
* @param itemTypeSegments The segments of the item type.
* @return A new GroupItem or null if the item type is invalid.
*/
private @Nullable GroupItem createGroupItem(ModelItem modelItem, String[] itemTypeSegments) {
if (itemTypeSegments.length == 1) {
// Just plain "Group" with no base type
return new GroupItem(modelItem.getName());
}
String function = GroupFunction.DEFAULT;
String baseItemType = switch (itemTypeSegments.length) {
case 2 -> itemTypeSegments[1];
case 3 -> {
// 3 segments could either be Group:Type:Function, or Group:Number:Dimension -> Find out which one it is
if (!modelItem.getArgs().isEmpty() || GroupFunction.VALID_FUNCTIONS.contains(itemTypeSegments[2])) {
// It's Group:Type:Function because there are arguments or the third segment is a valid function
function = itemTypeSegments[2];
yield itemTypeSegments[1];
} else {
// Otherwise, it must be Group:Number:Dimension
yield itemTypeSegments[1] + ItemUtil.EXTENSION_SEPARATOR + itemTypeSegments[2];
}
}
case 4 -> {
// 4 segments: "Group:Number:Dimension:Function"
function = itemTypeSegments[3];
yield itemTypeSegments[1] + ItemUtil.EXTENSION_SEPARATOR + itemTypeSegments[2];
}
default -> throw new IllegalArgumentException("Invalid group item type: " + modelItem.getType()
+ ". Expected formats are 'Group', 'Group:Type', 'Group:Type:Function', or 'Group:Number:Dimension:Function' with a maximum of 4 segments.");
};
Item baseItem = createItemOfType(baseItemType, modelItem.getName());
return applyGroupFunction(baseItem, modelItem, function);
}
/**
* Creates a new item of type {@code itemType} by utilizing an appropriate {@link ItemFactory}.
*
@@ -35,6 +35,7 @@ import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.items.GroupFunction;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemUtil;
import org.openhab.core.items.Metadata;
import org.openhab.core.items.fileconverter.AbstractItemFileGenerator;
import org.openhab.core.items.fileconverter.ItemFileGenerator;
@@ -45,8 +46,6 @@ import org.openhab.core.model.item.internal.GenericMetadataProvider;
import org.openhab.core.model.items.ItemModel;
import org.openhab.core.model.items.ItemsFactory;
import org.openhab.core.model.items.ModelBinding;
import org.openhab.core.model.items.ModelGroupFunction;
import org.openhab.core.model.items.ModelGroupItem;
import org.openhab.core.model.items.ModelItem;
import org.openhab.core.model.items.ModelProperty;
import org.openhab.core.types.State;
@@ -115,26 +114,24 @@ public class DslItemFileConverter extends AbstractItemFileGenerator implements I
private ModelItem buildModelItem(Item item, List<Metadata> channelLinks, List<Metadata> metadata,
@Nullable String stateFormatter, boolean hideDefaultParameters) {
ModelItem model;
ModelItem model = ItemsFactory.eINSTANCE.createModelItem();
if (item instanceof GroupItem groupItem) {
ModelGroupItem modelGroup = ItemsFactory.eINSTANCE.createModelGroupItem();
model = modelGroup;
Item baseItem = groupItem.getBaseItem();
List<String> groupType = new ArrayList<>();
groupType.add(groupItem.getType());
if (baseItem != null) {
modelGroup.setType(baseItem.getType());
groupType.add(baseItem.getType());
GroupFunction function = groupItem.getFunction();
if (function != null) {
ModelGroupFunction modelFunction = ModelGroupFunction
.getByName(function.getClass().getSimpleName().toUpperCase());
modelGroup.setFunction(modelFunction);
groupType.add(function.getClass().getSimpleName().toUpperCase());
State[] parameters = function.getParameters();
for (int i = 0; i < parameters.length; i++) {
modelGroup.getArgs().add(parameters[i].toString());
model.getArgs().add(parameters[i].toString());
}
}
}
model.setType(groupType.stream().collect(Collectors.joining(ItemUtil.EXTENSION_SEPARATOR)));
} else {
model = ItemsFactory.eINSTANCE.createModelNormalItem();
model.setType(item.getType());
}
@@ -12,15 +12,18 @@
*/
package org.openhab.core.model.validation
import org.openhab.core.model.items.ModelItem
import org.eclipse.xtext.validation.Check
import org.openhab.core.items.GroupFunction
import org.openhab.core.items.ItemUtil
import org.openhab.core.library.CoreItemFactory
import org.openhab.core.model.items.ItemsPackage
import org.openhab.core.model.items.ModelItem
import org.openhab.core.types.util.UnitUtils
/**
* Custom validation rules.
* Custom validation rules.
*
* see http://www.eclipse.org/Xtext/documentation.html#validation
* see https://eclipse.dev/Xtext/documentation/303_runtime_concepts.html#validation
*/
class ItemsValidator extends AbstractItemsValidator {
@@ -29,23 +32,96 @@ class ItemsValidator extends AbstractItemsValidator {
if (item === null || item.name === null) {
return
}
if (item.name.contains("-")) {
error('Item name must not contain dashes.', ItemsPackage.Literals.MODEL_ITEM__NAME)
}
if (!ItemUtil.isValidItemName(item.name)) {
error('Item name "' + item.name + '" is invalid. It must begin with a letter or underscore followed by alphanumeric characters and underscores, and must not contain any other symbols.', ItemsPackage.Literals.MODEL_ITEM__NAME)
}
}
@Check
def checkDimension(ModelItem item) {
if (item === null || item.type === null) {
@Check
def checkValidItemType(ModelItem item) {
if (item === null || item.type === null) {
return
}
if (item.type.startsWith("Number:")) {
var dimension = item.type.substring(item.type.indexOf(":") + 1)
try {
UnitUtils.parseDimension(dimension)
} catch (IllegalArgumentException e) {
warning("'" + dimension + "' is not a valid dimension.", ItemsPackage.Literals.MODEL_ITEM__TYPE)
}
val segments = item.type.split(":")
val mainType = segments.get(0)
switch mainType {
case "Group": checkGroupType(item, segments)
case "Number": checkNumberType(item, segments)
default: checkBasicItemType(item)
}
}
}
def checkNumberType(ModelItem item, String[] segments) {
switch (segments.size) {
case 1: return // Just "Number", valid
case 2: checkDimension(item, segments.get(1)) // "Number:<dimension>"
default: error("Item: '" + item.name + "' has an invalid Number type, too many segments: " + item.type, ItemsPackage.Literals.MODEL_ITEM__TYPE)
}
}
def checkBasicItemType(ModelItem item) {
if (!CoreItemFactory.VALID_ITEM_TYPES.contains(item.type)) {
error("Item '" + item.name + "' has an invalid type: '" + item.type + "'", ItemsPackage.Literals.MODEL_ITEM__TYPE)
}
}
def checkGroupType(ModelItem item, String[] segments) {
switch (segments.size) {
case 1: return // Just "Group", valid
case 2: checkGroupBaseType(item, segments.get(1))
case 3: checkGroupWithOneParam(item, segments.get(1), segments.get(2))
case 4: checkGroupWithTwoParams(item, segments.get(1), segments.get(2), segments.get(3))
// The xtext grammar will not allow more than 4 segments for Group types, so no default case is needed
}
}
def checkGroupBaseType(ModelItem item, String baseType) {
if (!CoreItemFactory.VALID_ITEM_TYPES.contains(baseType)) {
error("Item '" + item.name + "' has an invalid base item type: '" + baseType + "'", ItemsPackage.Literals.MODEL_ITEM__TYPE)
}
}
def checkGroupWithOneParam(ModelItem item, String baseType, String param) {
checkGroupBaseType(item, baseType)
if (item.args.size == 0 && baseType == "Number") {
if (!isValidGroupFunction(param)) {
// Group:Number:Dimension
checkDimension(item, param)
}
} else {
checkGroupFunction(item, param)
}
}
def checkGroupWithTwoParams(ModelItem item, String baseType, String dimension, String function) {
checkGroupBaseType(item, baseType)
if (baseType == "Number") {
checkDimension(item, dimension)
} else {
error("Item '" + item.name + "' with type '" + item.type + "' cannot have a dimension. Dimensions are only valid for Number base type. The dimension '" + dimension + "' is ignored.", ItemsPackage.Literals.MODEL_ITEM__TYPE)
}
checkGroupFunction(item, function)
}
def checkDimension(ModelItem item, String dimension) {
try {
UnitUtils.parseDimension(dimension)
} catch (IllegalArgumentException e) {
error("Item '" + item.name + "' has an unknown dimension: '" + dimension + "'. The Item will not be created.", ItemsPackage.Literals.MODEL_ITEM__TYPE)
}
}
def checkGroupFunction(ModelItem item, String function) {
if (!isValidGroupFunction(function)) {
warning("Item '" + item.name + "' has an unknown group function: '" + function + "'. Using EQUALITY instead.", ItemsPackage.Literals.MODEL_ITEM__TYPE)
}
}
def isValidGroupFunction(String value) {
return GroupFunction.VALID_FUNCTIONS.contains(value)
}
}
@@ -32,25 +32,25 @@ import org.openhab.core.model.sitemap.sitemap.Chart
//import org.eclipse.xtext.validation.Check
/**
* Custom validation rules.
*
* Custom validation rules.
*
* see http://www.eclipse.org/Xtext/documentation.html#validation
*/
class SitemapValidator extends AbstractSitemapValidator {
val ALLOWED_HINTS = #["text", "number", "date", "time", "datetime"]
val ALLOWED_INTERPOLATION = #["linear", "step"]
@Check
def void checkFramesInFrame(Frame frame) {
for (Widget w : frame.children) {
if (w instanceof Frame) {
error("Frames must not contain other frames",
warning("Frames must not contain other frames",
SitemapPackage.Literals.FRAME.getEStructuralFeature(SitemapPackage.FRAME__CHILDREN));
return;
}
if (w instanceof Button) {
error("Frames should not contain Button, Button is allowed only in Buttongrid",
warning("Frames should not contain Button, Button is allowed only in Buttongrid",
SitemapPackage.Literals.FRAME.getEStructuralFeature(SitemapPackage.FRAME__CHILDREN));
return;
}
@@ -64,7 +64,7 @@ class SitemapValidator extends AbstractSitemapValidator {
for (Widget w : sitemap.children) {
if (w instanceof Button) {
error("Sitemap should not contain Button, Button is allowed only in Buttongrid",
warning("Sitemap should not contain Button, Button is allowed only in Buttongrid",
SitemapPackage.Literals.SITEMAP.getEStructuralFeature(SitemapPackage.SITEMAP__NAME));
return;
}
@@ -74,7 +74,7 @@ class SitemapValidator extends AbstractSitemapValidator {
containsOtherWidgets = true
}
if (containsFrames && containsOtherWidgets) {
error("Sitemap should contain either only frames or none at all",
warning("Sitemap should contain either only frames or none at all",
SitemapPackage.Literals.SITEMAP.getEStructuralFeature(SitemapPackage.SITEMAP__NAME));
return
}
@@ -95,7 +95,7 @@ class SitemapValidator extends AbstractSitemapValidator {
var containsOtherWidgets = false
for (Widget w : widget.children) {
if (w instanceof Button) {
error("Linkable widget should not contain Button, Button is allowed only in Buttongrid",
warning("Linkable widget should not contain Button, Button is allowed only in Buttongrid",
SitemapPackage.Literals.FRAME.getEStructuralFeature(SitemapPackage.LINKABLE_WIDGET__CHILDREN));
return;
}
@@ -105,7 +105,7 @@ class SitemapValidator extends AbstractSitemapValidator {
containsOtherWidgets = true
}
if (containsFrames && containsOtherWidgets) {
error("Linkable widget should contain either only frames or none at all",
warning("Linkable widget should contain either only frames or none at all",
SitemapPackage.Literals.FRAME.getEStructuralFeature(SitemapPackage.LINKABLE_WIDGET__CHILDREN));
return
}
@@ -116,12 +116,12 @@ class SitemapValidator extends AbstractSitemapValidator {
def void checkWidgetsInButtongrid(Buttongrid grid) {
val nb = grid.getButtons.size()
if (nb > 0 && grid.item === null) {
error("To use the \"buttons\" parameter in a Buttongrid, the \"item\" parameter is required",
warning("To use the \"buttons\" parameter in a Buttongrid, the \"item\" parameter is required",
SitemapPackage.Literals.BUTTONGRID.getEStructuralFeature(SitemapPackage.BUTTONGRID__ITEM));
}
for (Widget w : grid.children) {
if (!(w instanceof Button)) {
error("Buttongrid must contain only Button",
warning("Buttongrid must contain only Button",
SitemapPackage.Literals.BUTTONGRID.getEStructuralFeature(SitemapPackage.BUTTONGRID__CHILDREN));
return;
}
@@ -131,17 +131,17 @@ class SitemapValidator extends AbstractSitemapValidator {
@Check
def void checkSetpoints(Setpoint sp) {
if (BigDecimal.ZERO == sp.step) {
error("Setpoint on item '" + sp.item + "' has step size of 0",
warning("Setpoint on item '" + sp.item + "' has step size of 0",
SitemapPackage.Literals.SETPOINT.getEStructuralFeature(SitemapPackage.SETPOINT__STEP));
}
if (sp.step !== null && sp.step < BigDecimal.ZERO) {
error("Setpoint on item '" + sp.item + "' has negative step size",
warning("Setpoint on item '" + sp.item + "' has negative step size",
SitemapPackage.Literals.SETPOINT.getEStructuralFeature(SitemapPackage.SETPOINT__STEP));
}
if (sp.minValue !== null && sp.maxValue !== null && sp.minValue > sp.maxValue) {
error("Setpoint on item '" + sp.item + "' has larger minValue than maxValue",
warning("Setpoint on item '" + sp.item + "' has larger minValue than maxValue",
SitemapPackage.Literals.SETPOINT.getEStructuralFeature(SitemapPackage.SETPOINT__MIN_VALUE));
}
}
@@ -149,7 +149,7 @@ class SitemapValidator extends AbstractSitemapValidator {
@Check
def void checkColortemperaturepicker(Colortemperaturepicker ctp) {
if (ctp.minValue !== null && ctp.maxValue !== null && ctp.minValue > ctp.maxValue) {
error("Colortemperaturepicker on item '" + ctp.item + "' has larger minValue than maxValue",
warning("Colortemperaturepicker on item '" + ctp.item + "' has larger minValue than maxValue",
SitemapPackage.Literals.COLORTEMPERATUREPICKER.getEStructuralFeature(SitemapPackage.COLORTEMPERATUREPICKER__MIN_VALUE));
}
}
@@ -159,7 +159,7 @@ class SitemapValidator extends AbstractSitemapValidator {
if (i.inputHint !== null && !ALLOWED_HINTS.contains(i.inputHint)) {
val node = NodeModelUtils.getNode(i)
val line = node.getStartLine()
error("Input on item '" + i.item + "' has invalid inputHint '" + i.inputHint + "' at line " + line,
warning("Input on item '" + i.item + "' has invalid inputHint '" + i.inputHint + "' at line " + line,
SitemapPackage.Literals.INPUT.getEStructuralFeature(SitemapPackage.INPUT__INPUT_HINT))
}
}
@@ -169,7 +169,7 @@ class SitemapValidator extends AbstractSitemapValidator {
if (i.interpolation !== null && !ALLOWED_INTERPOLATION.contains(i.interpolation)) {
val node = NodeModelUtils.getNode(i)
val line = node.getStartLine()
error("Input on item '" + i.item + "' has invalid interpolation '" + i.interpolation + "' at line " + line,
warning("Input on item '" + i.item + "' has invalid interpolation '" + i.interpolation + "' at line " + line,
SitemapPackage.Literals.INPUT.getEStructuralFeature(SitemapPackage.CHART__INTERPOLATION))
}
}
@@ -22,7 +22,7 @@ import org.eclipse.xtext.nodemodel.util.NodeModelUtils
import org.openhab.core.thing.ThingUID
/**
* Custom validation rules.
* Custom validation rules.
*
* see http://www.eclipse.org/Xtext/documentation.html#validation
*/
@@ -36,24 +36,24 @@ class ThingValidator extends AbstractThingValidator {
// We have to provide thingTypeId and a thingId
if (!thing.eIsSet(ThingPackage.Literals.MODEL_THING__THING_TYPE_ID)) {
if (thing.eIsSet(ThingPackage.Literals.MODEL_PROPERTY_CONTAINER__ID)) {
error("Provide a thing type ID and a thing ID in this format:\n <thingTypeId> <thingId>", ThingPackage.Literals.MODEL_PROPERTY_CONTAINER__ID)
warning("Provide a thing type ID and a thing ID in this format:\n <thingTypeId> <thingId>", ThingPackage.Literals.MODEL_PROPERTY_CONTAINER__ID)
} else {
if (thing.eIsSet(ThingPackage.Literals.MODEL_BRIDGE__BRIDGE)) {
error("Provide a thing type ID and a thing ID in this format:\n <thingTypeId> <thingId>", ThingPackage.Literals.MODEL_BRIDGE__BRIDGE)
warning("Provide a thing type ID and a thing ID in this format:\n <thingTypeId> <thingId>", ThingPackage.Literals.MODEL_BRIDGE__BRIDGE)
}
}
} else {
if (!thing.eIsSet(ThingPackage.Literals.MODEL_THING__THING_ID)) {
error("Provide a thing type ID and a thing ID in this format:\n <thingTypeId> <thingId>", ThingPackage.Literals.MODEL_THING__THING_TYPE_ID)
warning("Provide a thing type ID and a thing ID in this format:\n <thingTypeId> <thingId>", ThingPackage.Literals.MODEL_THING__THING_TYPE_ID)
}
}
} else { // thing in container
} else { // thing in container
if (thing.eIsSet(ThingPackage.Literals.MODEL_THING__THING_TYPE_ID) && thing.eIsSet(ThingPackage.Literals.MODEL_THING__THING_ID)) {
val thingTypeIdFeature = NodeModelUtils.findNodesForFeature(thing, ThingPackage.Literals.MODEL_THING__THING_TYPE_ID).head
val thingIdFeature = NodeModelUtils.findNodesForFeature(thing, ThingPackage.Literals.MODEL_THING__THING_ID).head
val startOffset = thingTypeIdFeature.offset
val endOffset = thingIdFeature.endOffset
getMessageAcceptor().acceptError("Provide a thing UID in this format:\n <bindingId>:<thingTypeId>:<thingId>", thing, startOffset, endOffset - startOffset, null, null)
getMessageAcceptor().acceptWarning("Provide a thing UID in this format:\n <bindingId>:<thingTypeId>:<thingId>", thing, startOffset, endOffset - startOffset, null, null)
} else {
if (thing.id !== null) {
try {
@@ -66,7 +66,7 @@ class ThingValidator extends AbstractThingValidator {
}
}
def private isNested(ModelThing thing) {
thing.eContainingFeature == ThingPackage.Literals.MODEL_BRIDGE__THINGS
}
@@ -14,10 +14,10 @@ package org.openhab.core.model.yaml.internal.items;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.items.GroupFunction;
import org.openhab.core.model.yaml.internal.util.YamlElementUtils;
/**
@@ -28,10 +28,6 @@ import org.openhab.core.model.yaml.internal.util.YamlElementUtils;
*/
public class YamlGroupDTO {
private static final String DEFAULT_FUNCTION = "EQUALITY";
private static final Set<String> VALID_FUNCTIONS = Set.of("AND", "OR", "NAND", "NOR", "XOR", "COUNT", "AVG",
"MEDIAN", "SUM", "MIN", "MAX", "LATEST", "EARLIEST", DEFAULT_FUNCTION);
public String type;
public String dimension;
public String function;
@@ -53,7 +49,7 @@ public class YamlGroupDTO {
} else if (dimension != null) {
warnings.add("\"dimension\" field in group ignored as type is not Number");
}
if (!VALID_FUNCTIONS.contains(getFunction())) {
if (!GroupFunction.VALID_FUNCTIONS.contains(getFunction())) {
errors.add("invalid value \"%s\" for \"function\" field".formatted(function));
ok = false;
}
@@ -65,7 +61,7 @@ public class YamlGroupDTO {
}
public String getFunction() {
return function != null ? function.toUpperCase() : DEFAULT_FUNCTION;
return function != null ? function.toUpperCase() : GroupFunction.DEFAULT;
}
public @NonNull List<@NonNull String> getParameters() {
@@ -15,7 +15,6 @@ package org.openhab.core.model.yaml.internal.util;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@@ -31,11 +30,6 @@ import org.openhab.core.util.StringUtils;
@NonNullByDefault
public class YamlElementUtils {
private static final Set<String> VALID_ITEM_TYPES = Set.of(CoreItemFactory.SWITCH, CoreItemFactory.ROLLERSHUTTER,
CoreItemFactory.CONTACT, CoreItemFactory.STRING, CoreItemFactory.NUMBER, CoreItemFactory.DIMMER,
CoreItemFactory.DATETIME, CoreItemFactory.COLOR, CoreItemFactory.IMAGE, CoreItemFactory.PLAYER,
CoreItemFactory.LOCATION, CoreItemFactory.CALL);
public static boolean equalsConfig(@Nullable Map<String, Object> first, @Nullable Map<String, Object> second) {
if (first != null && second != null) {
return first.size() != second.size() ? false
@@ -57,7 +51,7 @@ public class YamlElementUtils {
public static boolean isValidItemType(@Nullable String type) {
String adjustedType = getAdjustedItemType(type);
return adjustedType == null ? true : VALID_ITEM_TYPES.contains(adjustedType);
return adjustedType == null ? true : CoreItemFactory.VALID_ITEM_TYPES.contains(adjustedType);
}
public static boolean isNumberItemType(@Nullable String type) {
@@ -82,15 +82,15 @@ public class GroupFunctionHelper {
Unit<?> baseItemUnit = baseItem.getUnit();
if (baseItemUnit != null) {
switch (functionName.toUpperCase()) {
case "AVG":
case GroupFunction.AVG:
return new QuantityTypeArithmeticGroupFunction.Avg(baseItemUnit);
case "MEDIAN":
case GroupFunction.MEDIAN:
return new QuantityTypeArithmeticGroupFunction.Median(baseItemUnit);
case "SUM":
case GroupFunction.SUM:
return new QuantityTypeArithmeticGroupFunction.Sum(baseItemUnit);
case "MIN":
case GroupFunction.MIN:
return new QuantityTypeArithmeticGroupFunction.Min(baseItemUnit);
case "MAX":
case GroupFunction.MAX:
return new QuantityTypeArithmeticGroupFunction.Max(baseItemUnit);
default:
}
@@ -102,7 +102,7 @@ public class GroupFunctionHelper {
final String functionName = function.name;
final List<State> args;
switch (functionName.toUpperCase()) {
case "AND":
case GroupFunction.AND:
args = parseStates(baseItem, function.params);
if (args.size() == 2) {
return new ArithmeticGroupFunction.And(args.getFirst(), args.get(1));
@@ -110,7 +110,7 @@ public class GroupFunctionHelper {
logger.error("Group function 'AND' requires two arguments. Using Equality instead.");
}
break;
case "OR":
case GroupFunction.OR:
args = parseStates(baseItem, function.params);
if (args.size() == 2) {
return new ArithmeticGroupFunction.Or(args.getFirst(), args.get(1));
@@ -118,7 +118,7 @@ public class GroupFunctionHelper {
logger.error("Group function 'OR' requires two arguments. Using Equality instead.");
}
break;
case "NAND":
case GroupFunction.NAND:
args = parseStates(baseItem, function.params);
if (args.size() == 2) {
return new ArithmeticGroupFunction.NAnd(args.getFirst(), args.get(1));
@@ -126,7 +126,7 @@ public class GroupFunctionHelper {
logger.error("Group function 'NOT AND' requires two arguments. Using Equality instead.");
}
break;
case "NOR":
case GroupFunction.NOR:
args = parseStates(baseItem, function.params);
if (args.size() == 2) {
return new ArithmeticGroupFunction.NOr(args.getFirst(), args.get(1));
@@ -134,7 +134,7 @@ public class GroupFunctionHelper {
logger.error("Group function 'NOT OR' requires two arguments. Using Equality instead.");
}
break;
case "XOR":
case GroupFunction.XOR:
args = parseStates(baseItem, function.params);
if (args.size() == 2) {
return new ArithmeticGroupFunction.Xor(args.getFirst(), args.get(1));
@@ -142,7 +142,7 @@ public class GroupFunctionHelper {
logger.error("Group function 'XOR' requires two arguments. Using Equality instead.");
}
break;
case "COUNT":
case GroupFunction.COUNT:
if (function.params != null && function.params.length == 1) {
State countParam = new StringType(function.params[0]);
return new ArithmeticGroupFunction.Count(countParam);
@@ -150,21 +150,21 @@ public class GroupFunctionHelper {
logger.error("Group function 'COUNT' requires one argument. Using Equality instead.");
}
break;
case "AVG":
case GroupFunction.AVG:
return new ArithmeticGroupFunction.Avg();
case "MEDIAN":
case GroupFunction.MEDIAN:
return new ArithmeticGroupFunction.Median();
case "SUM":
case GroupFunction.SUM:
return new ArithmeticGroupFunction.Sum();
case "MIN":
case GroupFunction.MIN:
return new ArithmeticGroupFunction.Min();
case "MAX":
case GroupFunction.MAX:
return new ArithmeticGroupFunction.Max();
case "LATEST":
case GroupFunction.LATEST:
return new DateTimeGroupFunction.Latest();
case "EARLIEST":
case GroupFunction.EARLIEST:
return new DateTimeGroupFunction.Earliest();
case "EQUALITY":
case GroupFunction.EQUALITY:
return new GroupFunction.Equality();
default:
logger.error("Unknown group function '{}'. Using Equality instead.", functionName);
@@ -28,6 +28,26 @@ import org.openhab.core.types.UnDefType;
*/
@NonNullByDefault
public interface GroupFunction {
String EQUALITY = "EQUALITY";
String AND = "AND";
String OR = "OR";
String NAND = "NAND";
String NOR = "NOR";
String XOR = "XOR";
String COUNT = "COUNT";
String AVG = "AVG";
String MEDIAN = "MEDIAN";
String SUM = "SUM";
String MIN = "MIN";
String MAX = "MAX";
String LATEST = "LATEST";
String EARLIEST = "EARLIEST";
String DEFAULT = EQUALITY;
Set<String> VALID_FUNCTIONS = Set.of( //
EQUALITY, AND, OR, NAND, NOR, XOR, COUNT, AVG, MEDIAN, SUM, MIN, MAX, LATEST, EARLIEST //
);
/**
* Determines the current state of a group based on a list of items
@@ -12,6 +12,8 @@
*/
package org.openhab.core.library;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.UnitProvider;
@@ -57,6 +59,11 @@ public class CoreItemFactory implements ItemFactory {
public static final String ROLLERSHUTTER = "Rollershutter";
public static final String STRING = "String";
public static final String SWITCH = "Switch";
public static final Set<String> VALID_ITEM_TYPES = Set.of( //
CALL, COLOR, CONTACT, DATETIME, DIMMER, IMAGE, LOCATION, NUMBER, PLAYER, ROLLERSHUTTER, STRING, SWITCH //
);
private final UnitProvider unitProvider;
@Activate
@@ -90,7 +97,6 @@ public class CoreItemFactory implements ItemFactory {
@Override
public String[] getSupportedItemTypes() {
return new String[] { SWITCH, ROLLERSHUTTER, CONTACT, STRING, NUMBER, DIMMER, DATETIME, COLOR, IMAGE, PLAYER,
LOCATION, CALL };
return VALID_ITEM_TYPES.toArray(new String[0]);
}
}
@@ -43,6 +43,7 @@ Fragment-Host: org.openhab.core.model.item
org.eclipse.equinox.common;version='[3.19.0,3.19.1)',\
junit-jupiter-api;version='[5.13.4,5.13.5)',\
junit-jupiter-engine;version='[5.13.4,5.13.5)',\
junit-jupiter-params;version='[5.13.4,5.13.5)',\
junit-platform-commons;version='[1.13.4,1.13.5)',\
junit-platform-engine;version='[1.13.4,1.13.5)',\
junit-platform-launcher;version='[1.13.4,1.13.5)',\
@@ -27,11 +27,15 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.openhab.core.events.Event;
import org.openhab.core.events.EventSubscriber;
import org.openhab.core.items.GenericItem;
@@ -47,10 +51,12 @@ import org.openhab.core.items.events.AbstractItemRegistryEvent;
import org.openhab.core.items.events.ItemAddedEvent;
import org.openhab.core.items.events.ItemRemovedEvent;
import org.openhab.core.items.events.ItemUpdatedEvent;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.items.SwitchItem;
import org.openhab.core.library.types.ArithmeticGroupFunction;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityTypeArithmeticGroupFunction;
import org.openhab.core.model.core.EventType;
import org.openhab.core.model.core.ModelRepository;
import org.openhab.core.model.core.ModelRepositoryChangeListener;
@@ -403,6 +409,34 @@ public class GenericItemProviderTest extends JavaOSGiTest {
});
}
private static Stream<Arguments> testGroupTypeSyntax() {
return Stream.of( //
Arguments.of("Group:Number Grp", "Number", GroupFunction.Equality.class),
Arguments.of("Group:Number:MAX Grp", "Number", ArithmeticGroupFunction.Max.class),
Arguments.of("Group:Number:Power Grp", "Number:Power", GroupFunction.Equality.class),
Arguments.of("Group:Number:Power:MAX Grp", "Number:Power",
QuantityTypeArithmeticGroupFunction.Max.class),
Arguments.of("Group:Switch:OR(ON,OFF) Grp", "Switch", ArithmeticGroupFunction.Or.class));
}
@ParameterizedTest
@MethodSource
public void testGroupTypeSyntax(String model, String expectedBaseType,
Class<? extends GroupFunction> expectedFunction) {
modelRepository.addOrRefreshModel(TESTMODEL_NAME, new ByteArrayInputStream(model.getBytes()));
assertThat(itemRegistry.getAll(), hasSize(1));
if (itemRegistry.getAll().iterator().next() instanceof GroupItem groupItem) {
assertThat(groupItem.getBaseItem(), is(notNullValue()));
assertThat(groupItem.getBaseItem().getType(), is(expectedBaseType));
assertThat(groupItem.getFunction(), instanceOf(expectedFunction));
} else {
throw new AssertionError("Item is not a GroupItem");
}
}
@Test
public void testStableOrder() {
String model = "Group testGroup " + //
@@ -711,4 +745,43 @@ public class GenericItemProviderTest extends JavaOSGiTest {
assertThat(stateDescription, is(notNullValue()));
assertThat(stateDescription.getPattern(), is("%s"));
}
@Test
public void testKeywordsInOtherFields() {
String itemTypeList = CoreItemFactory.VALID_ITEM_TYPES.stream().sorted().collect(joining(", "));
String functionList = GroupFunction.VALID_FUNCTIONS.stream().sorted().collect(joining(", "));
String model = """
DateTime DateTime [%s]
Switch Switch "Switch" <Switch> [%s]
// Multiline item with keywords at the start of line
String
String
<
String
>
[
String
]
Number EQUALITY
""".formatted(itemTypeList, functionList);
modelRepository.addOrRefreshModel(TESTMODEL_NAME, new ByteArrayInputStream(model.getBytes()));
assertThat(itemRegistry.getAll(), hasSize(4));
Item dateTimeItem = itemRegistry.get("DateTime");
assertThat(dateTimeItem.getTags().stream().sorted().collect(joining(", ")), is(equalTo(itemTypeList)));
Item switchItem = itemRegistry.get("Switch");
assertThat(switchItem.getTags().stream().sorted().collect(joining(", ")), is(equalTo(functionList)));
assertThat(switchItem.getLabel(), is("Switch"));
assertThat(switchItem.getCategory(), is("Switch"));
Item multilineItem = itemRegistry.get("String");
assertThat(multilineItem.getTags().stream().sorted().collect(joining(", ")), is(equalTo("String")));
assertThat(multilineItem.getCategory(), is("String"));
assertThat(itemRegistry.get("EQUALITY"), is(notNullValue()));
}
}