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 final org.eclipse.emf.common.util.Diagnostic diagnostic = safeEmf
.call(() -> Diagnostician.INSTANCE.validate(resource.getContents().getFirst())); .call(() -> Diagnostician.INSTANCE.validate(resource.getContents().getFirst()));
for (org.eclipse.emf.common.util.Diagnostic d : diagnostic.getChildren()) { 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) { } catch (NullPointerException e) {
// see https://github.com/eclipse/smarthome/issues/3335 // see https://github.com/eclipse/smarthome/issues/3335
@@ -22,6 +22,7 @@ Import-Package: javax.measure,\
org.openhab.core.items,\ org.openhab.core.items,\
org.openhab.core.items.dto,\ org.openhab.core.items.dto,\
org.openhab.core.items.fileconverter,\ org.openhab.core.items.fileconverter,\
org.openhab.core.library,\
org.openhab.core.library.items,\ org.openhab.core.library.items,\
org.openhab.core.library.types,\ org.openhab.core.library.types,\
org.openhab.core.thing.util,\ org.openhab.core.thing.util,\
@@ -14,31 +14,18 @@ ItemModel:
; ;
ModelItem: ModelItem:
(ModelNormalItem | ModelGroupItem) name=ID type=ModelItemType ('(' args+=(ID|STRING) (',' args+=(ID|STRING))* ')')?
name=ID
(label=STRING)? (label=STRING)?
('<' icon=Icon '>')? ('<' icon=Icon '>')?
('(' groups+=ID (',' groups+=ID)* ')')? ('(' groups+=ID (',' groups+=ID)* ')')?
('[' tags+=(ID|STRING) (',' tags+=(ID|STRING))* ']')? ('[' tags+=(ID|STRING) (',' tags+=(ID|STRING))* ']')?
('{' bindings+=ModelBinding (',' bindings+=ModelBinding)* '}')? ('{' 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
; ;
// Supports item types with up to 4 colon-separated segments, e.g., Group:Number:Dimension:MAX
ModelItemType: ModelItemType:
BaseModelItemType | ('Number' (':' ID)?) ID (':' ID (':' ID (':' ID )?)?)?
;
BaseModelItemType:
'Switch' | 'Rollershutter' | 'String' | 'Dimmer' | 'Contact' | 'DateTime' | 'Color' | 'Player' | 'Location' | 'Call' | 'Image'
; ;
ModelBinding: ModelBinding:
@@ -56,7 +43,7 @@ ValueType returns ecore::EJavaObject:
STRING | NUMBER | BOOLEAN STRING | NUMBER | BOOLEAN
; ;
BOOLEAN returns ecore::EBoolean: BOOLEAN returns ecore::EBoolean:
'true' | 'false' 'true' | 'false'
; ;
@@ -25,8 +25,10 @@ class ItemsFormatter extends AbstractDeclarativeFormatter {
@Inject extension ItemsGrammarAccess @Inject extension ItemsGrammarAccess
override protected void configureFormatting(FormattingConfig c) { override protected void configureFormatting(FormattingConfig c) {
c.setLinewrap(1, 1, 2).before(modelGroupItemRule) c.setLinewrap(1, 1, 2).before(modelItemRule)
c.setLinewrap(1, 1, 2).before(modelNormalItemRule)
// 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("<", ">")
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.item.BindingConfigReader;
import org.openhab.core.model.items.ItemModel; import org.openhab.core.model.items.ItemModel;
import org.openhab.core.model.items.ModelBinding; 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.ModelItem;
import org.openhab.core.model.items.ModelNormalItem;
import org.openhab.core.types.StateDescriptionFragment; import org.openhab.core.types.StateDescriptionFragment;
import org.openhab.core.types.StateDescriptionFragmentBuilder; import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.openhab.core.types.StateDescriptionFragmentProvider; import org.openhab.core.types.StateDescriptionFragmentProvider;
@@ -234,62 +231,54 @@ public class GenericItemProvider extends AbstractProvider<Item>
} }
private @Nullable Item createItemFromModelItem(ModelItem modelItem, String modelName) { private @Nullable Item createItemFromModelItem(ModelItem modelItem, String modelName) {
Item item; String itemType = modelItem.getType();
if (modelItem instanceof ModelGroupItem modelGroupItem) { if (itemType == null || itemType.isBlank()) {
Item baseItem; logger.warn("Item '{}' has no type defined, ignoring it.", modelItem.getName());
try { return null;
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;
}
} }
if (item instanceof ActiveItem activeItem) {
String label = modelItem.getLabel(); try {
String format = extractFormat(label); String[] itemTypeSegments = itemType.split(ItemUtil.EXTENSION_SEPARATOR);
if (format != null) { String mainItemType = itemTypeSegments[0];
label = label.substring(0, label.indexOf("[")).trim();
Map<String, String> formatters = Objects Item item = switch (mainItemType) {
.requireNonNull(stateFormattersMap.computeIfAbsent(modelName, k -> new HashMap<>())); case "Group" -> createGroupItem(modelItem, itemTypeSegments);
formatters.put(modelItem.getName(), format); default -> createItemOfType(itemType, modelItem.getName());
if (!isIsolatedModel(modelName)) { };
stateDescriptionFragments.put(modelItem.getName(),
StateDescriptionFragmentBuilder.create().withPattern(format).build()); if (item instanceof ActiveItem activeItem) {
} String label = modelItem.getLabel();
} else { String format = extractFormat(label);
Map<String, String> formatters = stateFormattersMap.get(modelName); if (format != null) {
if (formatters != null) { label = label.substring(0, label.indexOf("[")).trim();
formatters.remove(modelItem.getName()); Map<String, String> formatters = Objects
if (formatters.isEmpty()) { .requireNonNull(stateFormattersMap.computeIfAbsent(modelName, k -> new HashMap<>()));
stateFormattersMap.remove(modelName); 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)) { activeItem.setLabel(label);
stateDescriptionFragments.remove(modelItem.getName()); activeItem.setCategory(modelItem.getIcon());
} assignTags(modelItem, activeItem);
return item;
} else {
return null;
} }
activeItem.setLabel(label); } catch (IllegalArgumentException e) {
activeItem.setCategory(modelItem.getIcon()); logger.debug("Error creating item '{}', item will be ignored: {}", modelItem.getName(), e.getMessage());
assignTags(modelItem, activeItem);
return item;
} else {
return null; 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(); GroupFunctionDTO dto = new GroupFunctionDTO();
dto.name = function.getName(); dto.name = function;
dto.params = modelGroupItem.getArgs().toArray(new String[0]); dto.params = modelItem.getArgs().toArray(new String[0]);
GroupFunction groupFunction = ItemDTOMapper.mapFunction(baseItem, dto); 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) { private void dispatchBindingsPerItemType(String[] itemTypes) {
@@ -529,6 +518,47 @@ public class GenericItemProvider extends AbstractProvider<Item>
return ret; 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}. * 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.GroupFunction;
import org.openhab.core.items.GroupItem; import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item; import org.openhab.core.items.Item;
import org.openhab.core.items.ItemUtil;
import org.openhab.core.items.Metadata; import org.openhab.core.items.Metadata;
import org.openhab.core.items.fileconverter.AbstractItemFileGenerator; import org.openhab.core.items.fileconverter.AbstractItemFileGenerator;
import org.openhab.core.items.fileconverter.ItemFileGenerator; 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.ItemModel;
import org.openhab.core.model.items.ItemsFactory; import org.openhab.core.model.items.ItemsFactory;
import org.openhab.core.model.items.ModelBinding; 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.ModelItem;
import org.openhab.core.model.items.ModelProperty; import org.openhab.core.model.items.ModelProperty;
import org.openhab.core.types.State; 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, private ModelItem buildModelItem(Item item, List<Metadata> channelLinks, List<Metadata> metadata,
@Nullable String stateFormatter, boolean hideDefaultParameters) { @Nullable String stateFormatter, boolean hideDefaultParameters) {
ModelItem model; ModelItem model = ItemsFactory.eINSTANCE.createModelItem();
if (item instanceof GroupItem groupItem) { if (item instanceof GroupItem groupItem) {
ModelGroupItem modelGroup = ItemsFactory.eINSTANCE.createModelGroupItem();
model = modelGroup;
Item baseItem = groupItem.getBaseItem(); Item baseItem = groupItem.getBaseItem();
List<String> groupType = new ArrayList<>();
groupType.add(groupItem.getType());
if (baseItem != null) { if (baseItem != null) {
modelGroup.setType(baseItem.getType()); groupType.add(baseItem.getType());
GroupFunction function = groupItem.getFunction(); GroupFunction function = groupItem.getFunction();
if (function != null) { if (function != null) {
ModelGroupFunction modelFunction = ModelGroupFunction groupType.add(function.getClass().getSimpleName().toUpperCase());
.getByName(function.getClass().getSimpleName().toUpperCase());
modelGroup.setFunction(modelFunction);
State[] parameters = function.getParameters(); State[] parameters = function.getParameters();
for (int i = 0; i < parameters.length; i++) { 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 { } else {
model = ItemsFactory.eINSTANCE.createModelNormalItem();
model.setType(item.getType()); model.setType(item.getType());
} }
@@ -12,15 +12,18 @@
*/ */
package org.openhab.core.model.validation package org.openhab.core.model.validation
import org.openhab.core.model.items.ModelItem
import org.eclipse.xtext.validation.Check 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.ItemsPackage
import org.openhab.core.model.items.ModelItem
import org.openhab.core.types.util.UnitUtils 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 { class ItemsValidator extends AbstractItemsValidator {
@@ -29,23 +32,96 @@ class ItemsValidator extends AbstractItemsValidator {
if (item === null || item.name === null) { if (item === null || item.name === null) {
return return
} }
if (item.name.contains("-")) { if (!ItemUtil.isValidItemName(item.name)) {
error('Item name must not contain dashes.', ItemsPackage.Literals.MODEL_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 @Check
def checkDimension(ModelItem item) { def checkValidItemType(ModelItem item) {
if (item === null || item.type === null) { if (item === null || item.type === null) {
return return
} }
if (item.type.startsWith("Number:")) {
var dimension = item.type.substring(item.type.indexOf(":") + 1) val segments = item.type.split(":")
try { val mainType = segments.get(0)
UnitUtils.parseDimension(dimension) switch mainType {
} catch (IllegalArgumentException e) { case "Group": checkGroupType(item, segments)
warning("'" + dimension + "' is not a valid dimension.", ItemsPackage.Literals.MODEL_ITEM__TYPE) 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 //import org.eclipse.xtext.validation.Check
/** /**
* Custom validation rules. * Custom validation rules.
* *
* see http://www.eclipse.org/Xtext/documentation.html#validation * see http://www.eclipse.org/Xtext/documentation.html#validation
*/ */
class SitemapValidator extends AbstractSitemapValidator { class SitemapValidator extends AbstractSitemapValidator {
val ALLOWED_HINTS = #["text", "number", "date", "time", "datetime"] val ALLOWED_HINTS = #["text", "number", "date", "time", "datetime"]
val ALLOWED_INTERPOLATION = #["linear", "step"] val ALLOWED_INTERPOLATION = #["linear", "step"]
@Check @Check
def void checkFramesInFrame(Frame frame) { def void checkFramesInFrame(Frame frame) {
for (Widget w : frame.children) { for (Widget w : frame.children) {
if (w instanceof Frame) { 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)); SitemapPackage.Literals.FRAME.getEStructuralFeature(SitemapPackage.FRAME__CHILDREN));
return; return;
} }
if (w instanceof Button) { 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)); SitemapPackage.Literals.FRAME.getEStructuralFeature(SitemapPackage.FRAME__CHILDREN));
return; return;
} }
@@ -64,7 +64,7 @@ class SitemapValidator extends AbstractSitemapValidator {
for (Widget w : sitemap.children) { for (Widget w : sitemap.children) {
if (w instanceof Button) { 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)); SitemapPackage.Literals.SITEMAP.getEStructuralFeature(SitemapPackage.SITEMAP__NAME));
return; return;
} }
@@ -74,7 +74,7 @@ class SitemapValidator extends AbstractSitemapValidator {
containsOtherWidgets = true containsOtherWidgets = true
} }
if (containsFrames && containsOtherWidgets) { 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)); SitemapPackage.Literals.SITEMAP.getEStructuralFeature(SitemapPackage.SITEMAP__NAME));
return return
} }
@@ -95,7 +95,7 @@ class SitemapValidator extends AbstractSitemapValidator {
var containsOtherWidgets = false var containsOtherWidgets = false
for (Widget w : widget.children) { for (Widget w : widget.children) {
if (w instanceof Button) { 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)); SitemapPackage.Literals.FRAME.getEStructuralFeature(SitemapPackage.LINKABLE_WIDGET__CHILDREN));
return; return;
} }
@@ -105,7 +105,7 @@ class SitemapValidator extends AbstractSitemapValidator {
containsOtherWidgets = true containsOtherWidgets = true
} }
if (containsFrames && containsOtherWidgets) { 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)); SitemapPackage.Literals.FRAME.getEStructuralFeature(SitemapPackage.LINKABLE_WIDGET__CHILDREN));
return return
} }
@@ -116,12 +116,12 @@ class SitemapValidator extends AbstractSitemapValidator {
def void checkWidgetsInButtongrid(Buttongrid grid) { def void checkWidgetsInButtongrid(Buttongrid grid) {
val nb = grid.getButtons.size() val nb = grid.getButtons.size()
if (nb > 0 && grid.item === null) { 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)); SitemapPackage.Literals.BUTTONGRID.getEStructuralFeature(SitemapPackage.BUTTONGRID__ITEM));
} }
for (Widget w : grid.children) { for (Widget w : grid.children) {
if (!(w instanceof Button)) { if (!(w instanceof Button)) {
error("Buttongrid must contain only Button", warning("Buttongrid must contain only Button",
SitemapPackage.Literals.BUTTONGRID.getEStructuralFeature(SitemapPackage.BUTTONGRID__CHILDREN)); SitemapPackage.Literals.BUTTONGRID.getEStructuralFeature(SitemapPackage.BUTTONGRID__CHILDREN));
return; return;
} }
@@ -131,17 +131,17 @@ class SitemapValidator extends AbstractSitemapValidator {
@Check @Check
def void checkSetpoints(Setpoint sp) { def void checkSetpoints(Setpoint sp) {
if (BigDecimal.ZERO == sp.step) { 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)); SitemapPackage.Literals.SETPOINT.getEStructuralFeature(SitemapPackage.SETPOINT__STEP));
} }
if (sp.step !== null && sp.step < BigDecimal.ZERO) { 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)); SitemapPackage.Literals.SETPOINT.getEStructuralFeature(SitemapPackage.SETPOINT__STEP));
} }
if (sp.minValue !== null && sp.maxValue !== null && sp.minValue > sp.maxValue) { 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)); SitemapPackage.Literals.SETPOINT.getEStructuralFeature(SitemapPackage.SETPOINT__MIN_VALUE));
} }
} }
@@ -149,7 +149,7 @@ class SitemapValidator extends AbstractSitemapValidator {
@Check @Check
def void checkColortemperaturepicker(Colortemperaturepicker ctp) { def void checkColortemperaturepicker(Colortemperaturepicker ctp) {
if (ctp.minValue !== null && ctp.maxValue !== null && ctp.minValue > ctp.maxValue) { 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)); 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)) { if (i.inputHint !== null && !ALLOWED_HINTS.contains(i.inputHint)) {
val node = NodeModelUtils.getNode(i) val node = NodeModelUtils.getNode(i)
val line = node.getStartLine() 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)) 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)) { if (i.interpolation !== null && !ALLOWED_INTERPOLATION.contains(i.interpolation)) {
val node = NodeModelUtils.getNode(i) val node = NodeModelUtils.getNode(i)
val line = node.getStartLine() 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)) SitemapPackage.Literals.INPUT.getEStructuralFeature(SitemapPackage.CHART__INTERPOLATION))
} }
} }
@@ -22,7 +22,7 @@ import org.eclipse.xtext.nodemodel.util.NodeModelUtils
import org.openhab.core.thing.ThingUID import org.openhab.core.thing.ThingUID
/** /**
* Custom validation rules. * Custom validation rules.
* *
* see http://www.eclipse.org/Xtext/documentation.html#validation * 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 // We have to provide thingTypeId and a thingId
if (!thing.eIsSet(ThingPackage.Literals.MODEL_THING__THING_TYPE_ID)) { if (!thing.eIsSet(ThingPackage.Literals.MODEL_THING__THING_TYPE_ID)) {
if (thing.eIsSet(ThingPackage.Literals.MODEL_PROPERTY_CONTAINER__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 { } else {
if (thing.eIsSet(ThingPackage.Literals.MODEL_BRIDGE__BRIDGE)) { 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 { } else {
if (!thing.eIsSet(ThingPackage.Literals.MODEL_THING__THING_ID)) { 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)) { 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 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 thingIdFeature = NodeModelUtils.findNodesForFeature(thing, ThingPackage.Literals.MODEL_THING__THING_ID).head
val startOffset = thingTypeIdFeature.offset val startOffset = thingTypeIdFeature.offset
val endOffset = thingIdFeature.endOffset 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 { } else {
if (thing.id !== null) { if (thing.id !== null) {
try { try {
@@ -66,7 +66,7 @@ class ThingValidator extends AbstractThingValidator {
} }
} }
def private isNested(ModelThing thing) { def private isNested(ModelThing thing) {
thing.eContainingFeature == ThingPackage.Literals.MODEL_BRIDGE__THINGS 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.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.items.GroupFunction;
import org.openhab.core.model.yaml.internal.util.YamlElementUtils; 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 { 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 type;
public String dimension; public String dimension;
public String function; public String function;
@@ -53,7 +49,7 @@ public class YamlGroupDTO {
} else if (dimension != null) { } else if (dimension != null) {
warnings.add("\"dimension\" field in group ignored as type is not Number"); 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)); errors.add("invalid value \"%s\" for \"function\" field".formatted(function));
ok = false; ok = false;
} }
@@ -65,7 +61,7 @@ public class YamlGroupDTO {
} }
public String getFunction() { public String getFunction() {
return function != null ? function.toUpperCase() : DEFAULT_FUNCTION; return function != null ? function.toUpperCase() : GroupFunction.DEFAULT;
} }
public @NonNull List<@NonNull String> getParameters() { 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.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@@ -31,11 +30,6 @@ import org.openhab.core.util.StringUtils;
@NonNullByDefault @NonNullByDefault
public class YamlElementUtils { 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) { public static boolean equalsConfig(@Nullable Map<String, Object> first, @Nullable Map<String, Object> second) {
if (first != null && second != null) { if (first != null && second != null) {
return first.size() != second.size() ? false return first.size() != second.size() ? false
@@ -57,7 +51,7 @@ public class YamlElementUtils {
public static boolean isValidItemType(@Nullable String type) { public static boolean isValidItemType(@Nullable String type) {
String adjustedType = getAdjustedItemType(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) { public static boolean isNumberItemType(@Nullable String type) {
@@ -82,15 +82,15 @@ public class GroupFunctionHelper {
Unit<?> baseItemUnit = baseItem.getUnit(); Unit<?> baseItemUnit = baseItem.getUnit();
if (baseItemUnit != null) { if (baseItemUnit != null) {
switch (functionName.toUpperCase()) { switch (functionName.toUpperCase()) {
case "AVG": case GroupFunction.AVG:
return new QuantityTypeArithmeticGroupFunction.Avg(baseItemUnit); return new QuantityTypeArithmeticGroupFunction.Avg(baseItemUnit);
case "MEDIAN": case GroupFunction.MEDIAN:
return new QuantityTypeArithmeticGroupFunction.Median(baseItemUnit); return new QuantityTypeArithmeticGroupFunction.Median(baseItemUnit);
case "SUM": case GroupFunction.SUM:
return new QuantityTypeArithmeticGroupFunction.Sum(baseItemUnit); return new QuantityTypeArithmeticGroupFunction.Sum(baseItemUnit);
case "MIN": case GroupFunction.MIN:
return new QuantityTypeArithmeticGroupFunction.Min(baseItemUnit); return new QuantityTypeArithmeticGroupFunction.Min(baseItemUnit);
case "MAX": case GroupFunction.MAX:
return new QuantityTypeArithmeticGroupFunction.Max(baseItemUnit); return new QuantityTypeArithmeticGroupFunction.Max(baseItemUnit);
default: default:
} }
@@ -102,7 +102,7 @@ public class GroupFunctionHelper {
final String functionName = function.name; final String functionName = function.name;
final List<State> args; final List<State> args;
switch (functionName.toUpperCase()) { switch (functionName.toUpperCase()) {
case "AND": case GroupFunction.AND:
args = parseStates(baseItem, function.params); args = parseStates(baseItem, function.params);
if (args.size() == 2) { if (args.size() == 2) {
return new ArithmeticGroupFunction.And(args.getFirst(), args.get(1)); 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."); logger.error("Group function 'AND' requires two arguments. Using Equality instead.");
} }
break; break;
case "OR": case GroupFunction.OR:
args = parseStates(baseItem, function.params); args = parseStates(baseItem, function.params);
if (args.size() == 2) { if (args.size() == 2) {
return new ArithmeticGroupFunction.Or(args.getFirst(), args.get(1)); 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."); logger.error("Group function 'OR' requires two arguments. Using Equality instead.");
} }
break; break;
case "NAND": case GroupFunction.NAND:
args = parseStates(baseItem, function.params); args = parseStates(baseItem, function.params);
if (args.size() == 2) { if (args.size() == 2) {
return new ArithmeticGroupFunction.NAnd(args.getFirst(), args.get(1)); 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."); logger.error("Group function 'NOT AND' requires two arguments. Using Equality instead.");
} }
break; break;
case "NOR": case GroupFunction.NOR:
args = parseStates(baseItem, function.params); args = parseStates(baseItem, function.params);
if (args.size() == 2) { if (args.size() == 2) {
return new ArithmeticGroupFunction.NOr(args.getFirst(), args.get(1)); 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."); logger.error("Group function 'NOT OR' requires two arguments. Using Equality instead.");
} }
break; break;
case "XOR": case GroupFunction.XOR:
args = parseStates(baseItem, function.params); args = parseStates(baseItem, function.params);
if (args.size() == 2) { if (args.size() == 2) {
return new ArithmeticGroupFunction.Xor(args.getFirst(), args.get(1)); 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."); logger.error("Group function 'XOR' requires two arguments. Using Equality instead.");
} }
break; break;
case "COUNT": case GroupFunction.COUNT:
if (function.params != null && function.params.length == 1) { if (function.params != null && function.params.length == 1) {
State countParam = new StringType(function.params[0]); State countParam = new StringType(function.params[0]);
return new ArithmeticGroupFunction.Count(countParam); return new ArithmeticGroupFunction.Count(countParam);
@@ -150,21 +150,21 @@ public class GroupFunctionHelper {
logger.error("Group function 'COUNT' requires one argument. Using Equality instead."); logger.error("Group function 'COUNT' requires one argument. Using Equality instead.");
} }
break; break;
case "AVG": case GroupFunction.AVG:
return new ArithmeticGroupFunction.Avg(); return new ArithmeticGroupFunction.Avg();
case "MEDIAN": case GroupFunction.MEDIAN:
return new ArithmeticGroupFunction.Median(); return new ArithmeticGroupFunction.Median();
case "SUM": case GroupFunction.SUM:
return new ArithmeticGroupFunction.Sum(); return new ArithmeticGroupFunction.Sum();
case "MIN": case GroupFunction.MIN:
return new ArithmeticGroupFunction.Min(); return new ArithmeticGroupFunction.Min();
case "MAX": case GroupFunction.MAX:
return new ArithmeticGroupFunction.Max(); return new ArithmeticGroupFunction.Max();
case "LATEST": case GroupFunction.LATEST:
return new DateTimeGroupFunction.Latest(); return new DateTimeGroupFunction.Latest();
case "EARLIEST": case GroupFunction.EARLIEST:
return new DateTimeGroupFunction.Earliest(); return new DateTimeGroupFunction.Earliest();
case "EQUALITY": case GroupFunction.EQUALITY:
return new GroupFunction.Equality(); return new GroupFunction.Equality();
default: default:
logger.error("Unknown group function '{}'. Using Equality instead.", functionName); logger.error("Unknown group function '{}'. Using Equality instead.", functionName);
@@ -28,6 +28,26 @@ import org.openhab.core.types.UnDefType;
*/ */
@NonNullByDefault @NonNullByDefault
public interface GroupFunction { 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 * Determines the current state of a group based on a list of items
@@ -12,6 +12,8 @@
*/ */
package org.openhab.core.library; package org.openhab.core.library;
import java.util.Set;
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.i18n.UnitProvider; 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 ROLLERSHUTTER = "Rollershutter";
public static final String STRING = "String"; public static final String STRING = "String";
public static final String SWITCH = "Switch"; 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; private final UnitProvider unitProvider;
@Activate @Activate
@@ -90,7 +97,6 @@ public class CoreItemFactory implements ItemFactory {
@Override @Override
public String[] getSupportedItemTypes() { public String[] getSupportedItemTypes() {
return new String[] { SWITCH, ROLLERSHUTTER, CONTACT, STRING, NUMBER, DIMMER, DATETIME, COLOR, IMAGE, PLAYER, return VALID_ITEM_TYPES.toArray(new String[0]);
LOCATION, CALL };
} }
} }
@@ -43,6 +43,7 @@ Fragment-Host: org.openhab.core.model.item
org.eclipse.equinox.common;version='[3.19.0,3.19.1)',\ org.eclipse.equinox.common;version='[3.19.0,3.19.1)',\
junit-jupiter-api;version='[5.13.4,5.13.5)',\ junit-jupiter-api;version='[5.13.4,5.13.5)',\
junit-jupiter-engine;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-commons;version='[1.13.4,1.13.5)',\
junit-platform-engine;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)',\ 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.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; 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.Event;
import org.openhab.core.events.EventSubscriber; import org.openhab.core.events.EventSubscriber;
import org.openhab.core.items.GenericItem; 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.ItemAddedEvent;
import org.openhab.core.items.events.ItemRemovedEvent; import org.openhab.core.items.events.ItemRemovedEvent;
import org.openhab.core.items.events.ItemUpdatedEvent; 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.NumberItem;
import org.openhab.core.library.items.SwitchItem; import org.openhab.core.library.items.SwitchItem;
import org.openhab.core.library.types.ArithmeticGroupFunction; import org.openhab.core.library.types.ArithmeticGroupFunction;
import org.openhab.core.library.types.OnOffType; 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.EventType;
import org.openhab.core.model.core.ModelRepository; import org.openhab.core.model.core.ModelRepository;
import org.openhab.core.model.core.ModelRepositoryChangeListener; 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 @Test
public void testStableOrder() { public void testStableOrder() {
String model = "Group testGroup " + // String model = "Group testGroup " + //
@@ -711,4 +745,43 @@ public class GenericItemProviderTest extends JavaOSGiTest {
assertThat(stateDescription, is(notNullValue())); assertThat(stateDescription, is(notNullValue()));
assertThat(stateDescription.getPattern(), is("%s")); 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()));
}
} }