diff --git a/bundles/org.openhab.core.io.rest.core/pom.xml b/bundles/org.openhab.core.io.rest.core/pom.xml index c2c00dd8f89..b9dc8976942 100644 --- a/bundles/org.openhab.core.io.rest.core/pom.xml +++ b/bundles/org.openhab.core.io.rest.core/pom.xml @@ -65,6 +65,21 @@ org.openhab.core.semantics ${project.version} + + org.openhab.core.bundles + org.openhab.core.model.core + ${project.version} + + + org.openhab.core.bundles + org.openhab.core.model.sitemap + ${project.version} + + + org.openhab.core.bom + org.openhab.core.bom.compile-model + pom + org.openhab.core.bundles org.openhab.core.test @@ -72,5 +87,4 @@ test - diff --git a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/fileformat/FileFormatResource.java b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/fileformat/FileFormatResource.java index ce91d748fda..6d72e6076ea 100644 --- a/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/fileformat/FileFormatResource.java +++ b/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/internal/fileformat/FileFormatResource.java @@ -18,9 +18,11 @@ import java.net.URI; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @@ -57,6 +59,9 @@ import org.openhab.core.items.MetadataKey; import org.openhab.core.items.MetadataRegistry; import org.openhab.core.items.fileconverter.ItemFileGenerator; +import org.openhab.core.model.sitemap.SitemapProvider; +import org.openhab.core.model.sitemap.fileconverter.SitemapFileGenerator; +import org.openhab.core.model.sitemap.sitemap.Sitemap; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingRegistry; @@ -92,12 +97,13 @@ /** * This class acts as a REST resource and provides different methods to generate file format - * for existing items and things. + * for existing items, things and sitemaps. * * This resource is registered with the Jersey servlet. * * @author Laurent Garnier - Initial contribution * @author Laurent Garnier - Add YAML output for things + * @author Mark Herwege - Add sitemap DSL */ @Component @JaxrsResource @@ -175,6 +181,26 @@ public class FileFormatResource implements RESTResource { param: my param value """; + private static final String DSL_SITEMAPS_EXAMPLE = """ + sitemap MySitemap label="My Sitemap" { + Frame { + Input item=MyItem label="My Input" + } + } + """; + + private static final String YAML_SITEMAPS_EXAMPLE = """ + version: 2 + sitemaps: + MySitemap: + label: Label + widgets: + MyWidget: + type: Switch + label: Label + item: MyItem + """; + private final Logger logger = LoggerFactory.getLogger(FileFormatResource.class); private final ItemRegistry itemRegistry; @@ -184,8 +210,10 @@ public class FileFormatResource implements RESTResource { private final Inbox inbox; private final ThingTypeRegistry thingTypeRegistry; private final ConfigDescriptionRegistry configDescRegistry; + private final List sitemapProviders = new ArrayList<>(); private final Map itemFileGenerators = new ConcurrentHashMap<>(); private final Map thingFileGenerators = new ConcurrentHashMap<>(); + private final Map sitemapFileGenerators = new ConcurrentHashMap<>(); @Activate public FileFormatResource(// @@ -227,6 +255,24 @@ protected void removeThingFileGenerator(ThingFileGenerator thingFileGenerator) { thingFileGenerators.remove(thingFileGenerator.getFileFormatGenerator()); } + @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE) + protected void addSitemapFileGenerator(SitemapFileGenerator sitemapFileGenerator) { + sitemapFileGenerators.put(sitemapFileGenerator.getFileFormatGenerator(), sitemapFileGenerator); + } + + protected void removeSitemapFileGenerator(SitemapFileGenerator sitemapFileGenerator) { + sitemapFileGenerators.remove(sitemapFileGenerator.getFileFormatGenerator()); + } + + @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE) + protected void addSitemapProvider(SitemapProvider sitemapProvider) { + sitemapProviders.add(sitemapProvider); + } + + protected void removeSitemapProvider(SitemapProvider sitemapProvider) { + sitemapProviders.remove(sitemapProvider); + } + @POST @RolesAllowed({ Role.ADMIN }) @Path("/items") @@ -305,6 +351,55 @@ public Response createFileFormatForThings(final @Context HttpHeaders httpHeaders return Response.ok(new String(outputStream.toByteArray())).build(); } + @POST + @RolesAllowed({ Role.ADMIN }) + @Path("/sitemaps") + @Consumes(MediaType.APPLICATION_JSON) + @Produces({ "text/vnd.openhab.dsl.sitemap", "application/yaml" }) + @Operation(operationId = "createFileFormatForSitemaps", summary = "Create file format for a list of sitemaps in registry.", security = { + @SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = { + @ApiResponse(responseCode = "200", description = "OK", content = { + @Content(mediaType = "text/vnd.openhab.dsl.sitemap", schema = @Schema(example = DSL_SITEMAPS_EXAMPLE)), + @Content(mediaType = "application/yaml", schema = @Schema(example = YAML_SITEMAPS_EXAMPLE)) }), + @ApiResponse(responseCode = "404", description = "One or more sitemaps not found in registry."), + @ApiResponse(responseCode = "415", description = "Unsupported media type.") }) + public Response createFileFormatForSitemaps(final @Context HttpHeaders httpHeaders, + @Parameter(description = "Array of Sitemap UIDs. If empty or omitted, return all Sitemaps from the Registry.") @Nullable List sitemapUIDs) { + String acceptHeader = httpHeaders.getHeaderString(HttpHeaders.ACCEPT); + logger.debug("createFileFormatForSitemaps: mediaType = {}, sitemapUIDs = {}", acceptHeader, sitemapUIDs); + SitemapFileGenerator generator = getSitemapFileGenerator(acceptHeader); + if (generator == null) { + return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE) + .entity("Unsupported media type '" + acceptHeader + "'!").build(); + } + Collection sitemapNames; + Map allSitemapNames = sitemapProviders.stream() + .flatMap(provider -> provider.getSitemapNames().stream().map(name -> Map.entry(name, provider))) + .sorted(Comparator.comparing(Map.Entry::getKey)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (existing, replacement) -> existing)); + if (sitemapUIDs == null || sitemapUIDs.isEmpty()) { + sitemapNames = allSitemapNames.keySet(); + } else if (allSitemapNames.keySet().containsAll(sitemapUIDs)) { + sitemapNames = sitemapUIDs; + } else { + String sitemapUID = sitemapUIDs.stream().filter(name -> !allSitemapNames.keySet().contains(name)) + .findFirst().get(); + return Response.status(Response.Status.NOT_FOUND) + .entity("Sitemap with UID '" + sitemapUID + "' does not exist!").build(); + } + List sitemaps = sitemapNames.stream().sorted().map(name -> { + SitemapProvider provider = allSitemapNames.get(name); + if (provider == null) { + return null; + } + return provider.getSitemap(name); + }).filter(Objects::nonNull).toList(); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + generator.generateFileFormat(outputStream, sitemaps); + return Response.ok(new String(outputStream.toByteArray())).build(); + } + /* * Get all the metadata for a list of items including channel links mapped to metadata in the namespace "channel" */ @@ -465,6 +560,14 @@ private Thing simulateThing(DiscoveryResult result, ThingType thingType) { }; } + private @Nullable SitemapFileGenerator getSitemapFileGenerator(String mediaType) { + return switch (mediaType) { + case "text/vnd.openhab.dsl.sitemap" -> sitemapFileGenerators.get("DSL"); + case "application/yaml" -> sitemapFileGenerators.get("YAML"); + default -> null; + }; + } + private List getThingsOrDiscoveryResult(List thingUIDs) { return thingUIDs.stream().distinct().map(uid -> { ThingUID thingUID = new ThingUID(uid); diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java index d9ad389c8d2..74c7e169fa0 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/SitemapResource.java @@ -82,13 +82,16 @@ import org.openhab.core.model.sitemap.SitemapProvider; import org.openhab.core.model.sitemap.sitemap.Button; import org.openhab.core.model.sitemap.sitemap.ButtonDefinition; +import org.openhab.core.model.sitemap.sitemap.ButtonDefinitionList; import org.openhab.core.model.sitemap.sitemap.Buttongrid; import org.openhab.core.model.sitemap.sitemap.Chart; import org.openhab.core.model.sitemap.sitemap.ColorArray; +import org.openhab.core.model.sitemap.sitemap.ColorArrayList; import org.openhab.core.model.sitemap.sitemap.Colortemperaturepicker; import org.openhab.core.model.sitemap.sitemap.Condition; import org.openhab.core.model.sitemap.sitemap.Frame; import org.openhab.core.model.sitemap.sitemap.IconRule; +import org.openhab.core.model.sitemap.sitemap.IconRuleList; import org.openhab.core.model.sitemap.sitemap.Image; import org.openhab.core.model.sitemap.sitemap.Input; import org.openhab.core.model.sitemap.sitemap.LinkableWidget; @@ -101,6 +104,7 @@ import org.openhab.core.model.sitemap.sitemap.Switch; import org.openhab.core.model.sitemap.sitemap.Video; import org.openhab.core.model.sitemap.sitemap.VisibilityRule; +import org.openhab.core.model.sitemap.sitemap.VisibilityRuleList; import org.openhab.core.model.sitemap.sitemap.Webview; import org.openhab.core.model.sitemap.sitemap.Widget; import org.openhab.core.types.State; @@ -614,7 +618,8 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null } bean.widgetId = widgetId; bean.icon = itemUIRegistry.getCategory(widget); - bean.staticIcon = widget.getStaticIcon() != null || !widget.getIconRules().isEmpty(); + bean.staticIcon = widget.getStaticIcon() != null + || (widget.getIconRules() != null && !widget.getIconRules().getElements().isEmpty()); bean.labelcolor = convertItemValueColor(itemUIRegistry.getLabelColor(widget), itemState); bean.valuecolor = convertItemValueColor(itemUIRegistry.getValueColor(widget), itemState); bean.iconcolor = convertItemValueColor(itemUIRegistry.getIconColor(widget), itemState); @@ -642,8 +647,8 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null isLeaf(children), uri, locale, false, evenIfHidden); } } - if (widget instanceof Switch switchWidget) { - for (Mapping mapping : switchWidget.getMappings()) { + if (widget instanceof Switch switchWidget && switchWidget.getMappings() != null) { + for (Mapping mapping : switchWidget.getMappings().getElements()) { MappingDTO mappingBean = new MappingDTO(); mappingBean.command = mapping.getCmd(); mappingBean.releaseCommand = mapping.getReleaseCmd(); @@ -652,8 +657,8 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null bean.mappings.add(mappingBean); } } - if (widget instanceof Selection selectionWidget) { - for (Mapping mapping : selectionWidget.getMappings()) { + if (widget instanceof Selection selectionWidget && selectionWidget.getMappings() != null) { + for (Mapping mapping : selectionWidget.getMappings().getElements()) { MappingDTO mappingBean = new MappingDTO(); mappingBean.command = mapping.getCmd(); mappingBean.label = mapping.getLabel(); @@ -714,19 +719,23 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null bean.maxValue = colortemperaturepickerWidget.getMaxValue(); } if (widget instanceof Buttongrid buttonGridWidget) { - for (ButtonDefinition button : buttonGridWidget.getButtons()) { - MappingDTO mappingBean = new MappingDTO(); - mappingBean.row = button.getRow(); - mappingBean.column = button.getColumn(); - mappingBean.command = button.getCmd(); - mappingBean.label = button.getLabel(); - mappingBean.icon = button.getIcon(); - bean.mappings.add(mappingBean); + ButtonDefinitionList buttonDefinitionList = buttonGridWidget.getButtons(); + if (buttonDefinitionList != null) { + for (ButtonDefinition button : buttonDefinitionList.getElements()) { + MappingDTO mappingBean = new MappingDTO(); + mappingBean.row = button.getRow(); + mappingBean.column = button.getColumn(); + mappingBean.command = button.getCmd(); + mappingBean.label = button.getLabel(); + mappingBean.icon = button.getIcon(); + bean.mappings.add(mappingBean); + } } } if (widget instanceof Button buttonWidget) { // Get the icon from the widget only - if (widget.getIcon() == null && widget.getStaticIcon() == null && widget.getIconRules().isEmpty()) { + if (widget.getIcon() == null && widget.getStaticIcon() == null + && (widget.getIconRules() == null || widget.getIconRules().getElements().isEmpty())) { bean.icon = null; bean.staticIcon = null; } @@ -877,26 +886,32 @@ private Set getAllItems(List widgets) { return items; } - private Set getItemsInVisibilityCond(EList ruleList) { + private Set getItemsInVisibilityCond(@Nullable VisibilityRuleList ruleList) { Set items = new HashSet<>(); - for (VisibilityRule rule : ruleList) { - getItemsInConditions(rule.getConditions(), items); + if (ruleList != null) { + for (VisibilityRule rule : ruleList.getElements()) { + getItemsInConditions(rule.getConditions(), items); + } } return items; } - private Set getItemsInColorCond(EList colorList) { + private Set getItemsInColorCond(@Nullable ColorArrayList colorList) { Set items = new HashSet<>(); - for (ColorArray rule : colorList) { - getItemsInConditions(rule.getConditions(), items); + if (colorList != null) { + for (ColorArray rule : colorList.getElements()) { + getItemsInConditions(rule.getConditions(), items); + } } return items; } - private Set getItemsInIconCond(EList ruleList) { + private Set getItemsInIconCond(@Nullable IconRuleList ruleList) { Set items = new HashSet<>(); - for (IconRule rule : ruleList) { - getItemsInConditions(rule.getConditions(), items); + if (ruleList != null) { + for (IconRule rule : ruleList.getElements()) { + getItemsInConditions(rule.getConditions(), items); + } } return items; } diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetsChangeListener.java b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetsChangeListener.java index 6bbb3173ffd..327cd6a2b68 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetsChangeListener.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/main/java/org/openhab/core/io/rest/sitemap/internal/WidgetsChangeListener.java @@ -40,10 +40,13 @@ import org.openhab.core.model.sitemap.sitemap.Buttongrid; import org.openhab.core.model.sitemap.sitemap.Chart; import org.openhab.core.model.sitemap.sitemap.ColorArray; +import org.openhab.core.model.sitemap.sitemap.ColorArrayList; import org.openhab.core.model.sitemap.sitemap.Condition; import org.openhab.core.model.sitemap.sitemap.Frame; import org.openhab.core.model.sitemap.sitemap.IconRule; +import org.openhab.core.model.sitemap.sitemap.IconRuleList; import org.openhab.core.model.sitemap.sitemap.VisibilityRule; +import org.openhab.core.model.sitemap.sitemap.VisibilityRuleList; import org.openhab.core.model.sitemap.sitemap.Widget; import org.openhab.core.types.State; import org.openhab.core.ui.items.ItemUIRegistry; @@ -135,24 +138,39 @@ private Set getAllItems(EList widgets) { items.addAll(getAllItems(grid.getChildren())); } // now scan icon rules - for (IconRule rule : widget.getIconRules()) { - addItemsFromConditions(items, rule.getConditions()); + IconRuleList iconRuleList = widget.getIconRules(); + if (iconRuleList != null) { + for (IconRule rule : iconRuleList.getElements()) { + addItemsFromConditions(items, rule.getConditions()); + } } // now scan visibility rules - for (VisibilityRule rule : widget.getVisibility()) { - addItemsFromConditions(items, rule.getConditions()); + VisibilityRuleList visibilityRuleList = widget.getVisibility(); + if (visibilityRuleList != null) { + for (VisibilityRule rule : visibilityRuleList.getElements()) { + addItemsFromConditions(items, rule.getConditions()); + } } // now scan label color rules - for (ColorArray rule : widget.getLabelColor()) { - addItemsFromConditions(items, rule.getConditions()); + ColorArrayList labelColorArrayList = widget.getLabelColor(); + if (labelColorArrayList != null) { + for (ColorArray rule : labelColorArrayList.getElements()) { + addItemsFromConditions(items, rule.getConditions()); + } } // now scan value color rules - for (ColorArray rule : widget.getValueColor()) { - addItemsFromConditions(items, rule.getConditions()); + ColorArrayList valueColorArrayList = widget.getValueColor(); + if (valueColorArrayList != null) { + for (ColorArray rule : valueColorArrayList.getElements()) { + addItemsFromConditions(items, rule.getConditions()); + } } // now scan icon color rules - for (ColorArray rule : widget.getIconColor()) { - addItemsFromConditions(items, rule.getConditions()); + ColorArrayList iconColorArrayList = widget.getIconColor(); + if (iconColorArrayList != null) { + for (ColorArray rule : iconColorArrayList.getElements()) { + addItemsFromConditions(items, rule.getConditions()); + } } } } @@ -231,7 +249,8 @@ private SitemapWidgetEvent constructSitemapEventForWidget(Item item, State state event.reloadIcon = widget.getStaticIcon() == null; if (widget instanceof Button buttonWidget) { // Get the icon from the widget only - if (widget.getIcon() == null && widget.getStaticIcon() == null && widget.getIconRules().isEmpty()) { + if (widget.getIcon() == null && widget.getStaticIcon() == null + && (widget.getIconRules() == null || widget.getIconRules().getElements().isEmpty())) { event.icon = null; event.reloadIcon = false; } @@ -281,11 +300,16 @@ private Item getItemForWidget(Widget w) { } private boolean definesVisibilityOrColorOrIcon(Widget w, String name) { - return w.getVisibility().stream().anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name)) - || w.getLabelColor().stream().anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name)) - || w.getValueColor().stream().anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name)) - || w.getIconColor().stream().anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name)) - || w.getIconRules().stream().anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name)); + return (w.getVisibility() != null && w.getVisibility().getElements().stream() + .anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name))) + || (w.getLabelColor() != null && w.getLabelColor().getElements().stream() + .anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name))) + || (w.getValueColor() != null && w.getValueColor().getElements().stream() + .anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name))) + || (w.getIconColor() != null && w.getIconColor().getElements().stream() + .anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name))) + || (w.getIconRules() != null && w.getIconRules().getElements().stream() + .anyMatch(r -> conditionsDependsOnItem(r.getConditions(), name))); } private boolean conditionsDependsOnItem(@Nullable EList conditions, String name) { diff --git a/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java b/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java index 9c7413f1287..1aead0e7f0b 100644 --- a/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java +++ b/bundles/org.openhab.core.io.rest.sitemap/src/test/java/org/openhab/core/io/rest/sitemap/internal/SitemapResourceTest.java @@ -16,8 +16,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.hamcrest.collection.IsEmptyCollection.empty; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import java.util.Collection; import java.util.Collections; @@ -53,11 +52,14 @@ import org.openhab.core.library.types.PercentType; import org.openhab.core.model.sitemap.SitemapProvider; import org.openhab.core.model.sitemap.sitemap.ColorArray; +import org.openhab.core.model.sitemap.sitemap.ColorArrayList; import org.openhab.core.model.sitemap.sitemap.Condition; import org.openhab.core.model.sitemap.sitemap.Group; import org.openhab.core.model.sitemap.sitemap.IconRule; +import org.openhab.core.model.sitemap.sitemap.IconRuleList; import org.openhab.core.model.sitemap.sitemap.Sitemap; import org.openhab.core.model.sitemap.sitemap.VisibilityRule; +import org.openhab.core.model.sitemap.sitemap.VisibilityRuleList; import org.openhab.core.model.sitemap.sitemap.Widget; import org.openhab.core.test.java.JavaTest; import org.openhab.core.types.Command; @@ -621,11 +623,21 @@ private static void mockWidgetMethods(EList iconRules1, EList items, model.getItems().add(buildModelItem(item, getChannelLinks(metadata, item.getName()), getMetadata(metadata, item.getName()), hideDefaultParameters)); } - modelRepository.generateSyntaxFromModel(out, "items", model); + modelRepository.generateSyntaxFromModel(out, "items", model, serializer); } private ModelItem buildModelItem(Item item, List channelLinks, List metadata, diff --git a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext index a89e623c9df..d01175cc7da 100644 --- a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext +++ b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/Sitemap.xtext @@ -24,207 +24,190 @@ LinkableWidget: '}')?; Frame: - {Frame} 'Frame' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); - + {Frame} 'Frame' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Text: - {Text} 'Text' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); + {Text} 'Text' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Group: - 'Group' (('item=' item=GroupItemRef) & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); + 'Group' (('item=' item=GroupItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Image: - 'Image' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('url=' url=STRING)? & ('refresh=' refresh=INT)? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); + 'Image' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('url=' url=STRING) | ('refresh=' refresh=INT) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Video: - 'Video' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('url=' url=STRING) & ('encoding=' encoding=STRING)? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); + 'Video' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('url=' url=STRING) | ('encoding=' encoding=STRING) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Chart: - 'Chart' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('service=' service=STRING)? & ('refresh=' refresh=INT)? & ('period=' period=Period) & - ('legend=' legend=BOOLEAN_OBJECT)? & ('forceasitem=' forceAsItem=BOOLEAN_OBJECT)? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')? & - ('yAxisDecimalPattern=' yAxisDecimalPattern=(STRING))? & - ('interpolation=' interpolation=(STRING))?); + 'Chart' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('service=' service=STRING) | ('refresh=' refresh=INT) | ('period=' period=Period) | + ('legend=' legend=BOOLEAN_OBJECT) | ('forceasitem=' forceAsItem=BOOLEAN_OBJECT) | + ('yAxisDecimalPattern=' yAxisDecimalPattern=(STRING)) | + ('interpolation=' interpolation=(STRING)) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Webview: - 'Webview' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('height=' height=INT)? & ('url=' url=STRING) & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); + 'Webview' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('height=' height=INT) | ('url=' url=STRING) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Switch: - 'Switch' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('mappings=[' mappings+=Mapping (',' mappings+=Mapping)* ']')? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); + 'Switch' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('mappings=' mappings=MappingList) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Mapview: - 'Mapview' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('height=' height=INT)? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); + 'Mapview' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('height=' height=INT) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Slider: - 'Slider' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - (switchEnabled?='switchSupport')? & (releaseOnly?='releaseOnly')? & - ('minValue=' minValue=Number)? & ('maxValue=' maxValue=Number)? & ('step=' step=Number)? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); + 'Slider' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + (switchEnabled?='switchSupport') | (releaseOnly?='releaseOnly') | + ('minValue=' minValue=Number) | ('maxValue=' maxValue=Number) | ('step=' step=Number) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Selection: - 'Selection' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('mappings=[' mappings+=Mapping (',' mappings+=Mapping)* ']')? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); + 'Selection' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('mappings=' mappings=MappingList) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Setpoint: - 'Setpoint' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('minValue=' minValue=Number)? & ('maxValue=' maxValue=Number)? & ('step=' step=Number)? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); + 'Setpoint' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('minValue=' minValue=Number) | ('maxValue=' maxValue=Number) | ('step=' step=Number) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Colorpicker: - 'Colorpicker' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); + 'Colorpicker' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Colortemperaturepicker: - 'Colortemperaturepicker' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('minValue=' minValue=Number)? & ('maxValue=' maxValue=Number)? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); + 'Colortemperaturepicker' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('minValue=' minValue=Number) | ('maxValue=' maxValue=Number) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Input: - 'Input' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('inputHint=' inputHint=STRING)? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); + 'Input' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('inputHint=' inputHint=STRING) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Buttongrid: - {Buttongrid} 'Buttongrid' (('item=' item=ItemRef)? & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('buttons=[' buttons+=ButtonDefinition (',' buttons+=ButtonDefinition)* ']')? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); + {Buttongrid} 'Buttongrid' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('buttons=' buttons=ButtonDefinitionList) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Button: - 'Button' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('row=' row=INT) & ('column=' column=INT) & (stateless?='stateless')? & - ('click=' cmd=Command) & ('release=' releaseCmd=Command)? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); + 'Button' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('row=' row=INT) | ('column=' column=INT) | (stateless?='stateless') | + ('click=' cmd=Command) | ('release=' releaseCmd=Command) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; Default: - 'Default' (('item=' item=ItemRef) & ('label=' label=(ID | STRING))? & - (('icon=' icon=Icon) | - ('icon=[' (IconRules+=IconRule (',' IconRules+=IconRule)*) ']') | - ('staticIcon=' staticIcon=Icon))? & - ('height=' height=INT)? & - ('labelcolor=[' (LabelColor+=ColorArray (',' LabelColor+=ColorArray)*) ']')? & - ('valuecolor=[' (ValueColor+=ColorArray (',' ValueColor+=ColorArray)*) ']')? & - ('iconcolor=[' (IconColor+=ColorArray (',' IconColor+=ColorArray)*) ']')? & - ('visibility=[' (Visibility+=VisibilityRule (',' Visibility+=VisibilityRule)*) ']')?); + 'Default' (('item=' item=ItemRef) | ('label=' label=(ID | STRING)) | + ('icon=' icon=Icon) | ('icon=' IconRules=IconRuleList) | ('staticIcon=' staticIcon=Icon) | + ('height=' height=INT) | + ('labelcolor=' labelColor=ColorArrayList) | + ('valuecolor=' valueColor=ColorArrayList) | + ('iconcolor=' iconColor=ColorArrayList) | + ('visibility=' visibility=VisibilityRuleList))*; ButtonDefinition: row=INT ':' column=INT ':' cmd=Command '=' label=(ID | STRING) ('=' icon=Icon)?; +MappingList returns MappingList: + '[' elements+=Mapping (',' elements+=Mapping)* ']' +; + +ColorArrayList returns ColorArrayList: + '[' elements+=ColorArray (',' elements+=ColorArray)* ']' +; + +IconRuleList returns IconRuleList: + '[' elements+=IconRule (',' elements+=IconRule)* ']' +; + +VisibilityRuleList returns VisibilityRuleList: + '[' elements+=VisibilityRule (',' elements+=VisibilityRule)* ']' +; + +ButtonDefinitionList returns ButtonDefinitionList: + '[' elements+=ButtonDefinition (',' elements+=ButtonDefinition)* ']' +; + Mapping: cmd=Command (':' releaseCmd=Command)? '=' label=(ID | STRING) ('=' icon=Icon)?; diff --git a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/SitemapRuntimeModule.xtend b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/SitemapRuntimeModule.xtend index ad4be5c67a2..4244f227512 100644 --- a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/SitemapRuntimeModule.xtend +++ b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/SitemapRuntimeModule.xtend @@ -21,6 +21,10 @@ import org.eclipse.xtext.conversion.IValueConverterService import org.eclipse.xtext.linking.lazy.LazyURIEncoder import com.google.inject.Binder import com.google.inject.name.Names +import org.openhab.core.model.sitemap.formatting.SitemapFormatter +import org.eclipse.xtext.formatting.IFormatter +import org.openhab.core.model.sitemap.formatting.SitemapIndentationInformation +import org.eclipse.xtext.formatting.IIndentationInformation /** * Use this class to register components to be used at runtime / without the Equinox extension registry. @@ -30,6 +34,15 @@ class SitemapRuntimeModule extends org.openhab.core.model.sitemap.AbstractSitema return SitemapConverters } + override Class bindIFormatter() { + return SitemapFormatter + } + + override void configure(Binder binder) { + super.configure(binder) + binder.bind(IIndentationInformation).to(SitemapIndentationInformation) + } + override void configureUseIndexFragmentsForLazyLinking(Binder binder) { binder.bind(Boolean.TYPE).annotatedWith(Names.named(LazyURIEncoder.USE_INDEXED_FRAGMENTS_BINDING)).toInstance( Boolean.FALSE) diff --git a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/SitemapStandaloneSetup.xtend b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/SitemapStandaloneSetup.xtend index 1a82db8bc4b..c5cac00799c 100644 --- a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/SitemapStandaloneSetup.xtend +++ b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/SitemapStandaloneSetup.xtend @@ -15,20 +15,25 @@ package org.openhab.core.model.sitemap import org.eclipse.emf.ecore.EPackage import org.eclipse.emf.ecore.resource.Resource import org.eclipse.xtext.resource.IResourceServiceProvider +import com.google.inject.Injector /** * Initialization support for running Xtext languages * without equinox extension registry */ class SitemapStandaloneSetup extends SitemapStandaloneSetupGenerated { - def static void doSetup() { - new SitemapStandaloneSetup().createInjectorAndDoEMFRegistration() + static Injector injector; + + def static Injector doSetup() { + if (injector === null) { + injector = new SitemapStandaloneSetup().createInjectorAndDoEMFRegistration(); + } + return injector; } - + def static void unregister() { EPackage.Registry.INSTANCE.remove("https://openhab.org/model/Sitemap"); Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().remove("sitemap"); IResourceServiceProvider.Registry.INSTANCE.getExtensionToFactoryMap().remove("sitemap"); } - } diff --git a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/fileconverter/AbstractSitemapFileGenerator.java b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/fileconverter/AbstractSitemapFileGenerator.java new file mode 100644 index 00000000000..187bd48b00a --- /dev/null +++ b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/fileconverter/AbstractSitemapFileGenerator.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.model.sitemap.fileconverter; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.model.sitemap.sitemap.Sitemap; +import org.osgi.service.component.annotations.Activate; + +/** + * {@link AbstractSitemapFileGenerator} is the base class for any {@link Sitemap} file generator. + * + * @author Mark Herwege - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractSitemapFileGenerator implements SitemapFileGenerator { + + @Activate + public AbstractSitemapFileGenerator() { + } +} diff --git a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/fileconverter/SitemapFileGenerator.java b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/fileconverter/SitemapFileGenerator.java new file mode 100644 index 00000000000..cb7be4430a0 --- /dev/null +++ b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/fileconverter/SitemapFileGenerator.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.model.sitemap.fileconverter; + +import java.io.OutputStream; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.model.sitemap.sitemap.Sitemap; + +/** + * {@link SitemapFileGenerator} is the interface to implement by any file generator for {@link Sitemap} object. + * + * @author Mark Herwege - Initial contribution + */ +@NonNullByDefault +public interface SitemapFileGenerator { + + /** + * Returns the format of the file. + * + * @return the file format + */ + String getFileFormatGenerator(); + + /** + * Generate the file format for a list of sitemaps. + * + * @param out the output stream to write the generated syntax to + * @param sitemaps the sitemaps + */ + void generateFileFormat(OutputStream out, List sitemaps); +} diff --git a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/formatting/SitemapFormatter.xtend b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/formatting/SitemapFormatter.xtend index 6dccbc9cfd9..9eb247cce23 100644 --- a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/formatting/SitemapFormatter.xtend +++ b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/formatting/SitemapFormatter.xtend @@ -17,26 +17,66 @@ package org.openhab.core.model.sitemap.formatting import org.eclipse.xtext.formatting.impl.AbstractDeclarativeFormatter import org.eclipse.xtext.formatting.impl.FormattingConfig -// import com.google.inject.Inject; -// import org.openhab.core.model.services.SitemapGrammarAccess +import com.google.inject.Inject +import org.openhab.core.model.sitemap.services.SitemapGrammarAccess /** * This class contains custom formatting description. - * - * see : http://www.eclipse.org/Xtext/documentation.html#formatting - * on how and when to use it - * - * Also see {@link org.eclipse.xtext.xtext.XtextFormatter} as an example */ class SitemapFormatter extends AbstractDeclarativeFormatter { -// @Inject extension SitemapGrammarAccess - - override protected void configureFormatting(FormattingConfig c) { -// It's usually a good idea to activate the following three statements. -// They will add and preserve newlines around comments -// c.setLinewrap(0, 1, 2).before(SL_COMMENTRule) -// c.setLinewrap(0, 1, 2).before(ML_COMMENTRule) -// c.setLinewrap(0, 1, 1).after(ML_COMMENTRule) + @Inject extension SitemapGrammarAccess + + override protected void configureFormatting(FormattingConfig c) { + c.wrappedLineIndentation = 4 + c.autoLinewrap = 200 + + c.setLinewrap(1, 1, 2).before(widgetRule) + + c.setIndentationIncrement.after("{") + c.setLinewrap().before("}") + c.setIndentationDecrement.before("}") + c.setLinewrap().after("}") + + c.setNoSpace().withinKeywordPairs("<", ">") + c.setNoSpace().withinKeywordPairs("(", ")") + c.setNoSpace().withinKeywordPairs("[", "]") + + c.setNoSpace().after("item=", "label=", "icon=", "staticIcon=") + c.setNoSpace().after("url=", "refresh=", "encoding=", "service=", "period=", "legend=", "forceasitem=", "yAxisDecimalPattern=", "interpolation=", "height=") + c.setNoSpace().after("minValue=", "maxValue=", "step=", "inputHint=", "row=", "column=", "click=", "release=") + c.setNoSpace().after("labelcolor=", "valuecolor=", "iconcolor=", "visibility=", "mappings=", "buttons=") + + c.setNoSpace().before(",") + c.setNoSpace().around(":", "=") + + c.setLinewrap(0, 1, 2).before(SL_COMMENTRule) + c.setLinewrap(0, 1, 2).before(ML_COMMENTRule) + c.setLinewrap(0, 1, 1).after(ML_COMMENTRule) } + + def withinKeywordPairs(FormattingConfig.NoSpaceLocator locator, String leftKW, String rightKW) { + for (pair : findKeywordPairs(leftKW, rightKW)) { + locator.after(pair.first) + locator.before(pair.second) + } + } + + def around(FormattingConfig.ElementLocator locator, String ... listKW) { + for (keyword : findKeywords(listKW)) { + locator.around(keyword) + } + } + + def after(FormattingConfig.ElementLocator locator, String ... listKW) { + for (keyword : findKeywords(listKW)) { + locator.after(keyword) + } + } + + def before(FormattingConfig.ElementLocator locator, String ... listKW) { + for (keyword : findKeywords(listKW)) { + locator.before(keyword) + } + } } diff --git a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/formatting/SitemapIndentationInformation.java b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/formatting/SitemapIndentationInformation.java new file mode 100644 index 00000000000..c969d4af325 --- /dev/null +++ b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/formatting/SitemapIndentationInformation.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.model.sitemap.formatting; + +import org.eclipse.xtext.formatting.IIndentationInformation; + +public class SitemapIndentationInformation implements IIndentationInformation { + @Override + public String getIndentString() { + return " "; // 2 spaces + } +} diff --git a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/internal/fileconverter/DslSitemapFileConverter.java b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/internal/fileconverter/DslSitemapFileConverter.java new file mode 100644 index 00000000000..336b56891b7 --- /dev/null +++ b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/internal/fileconverter/DslSitemapFileConverter.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.model.sitemap.internal.fileconverter; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.xtext.serializer.ISerializer; +import org.openhab.core.model.core.ModelRepository; +import org.openhab.core.model.sitemap.SitemapStandaloneSetup; +import org.openhab.core.model.sitemap.fileconverter.AbstractSitemapFileGenerator; +import org.openhab.core.model.sitemap.fileconverter.SitemapFileGenerator; +import org.openhab.core.model.sitemap.sitemap.Sitemap; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link DslSitemapFileConverter} is the DSL file converter for {@link Sitemap} object + * with the capabilities of parsing and generating file. + * + * @author Mark Herwege - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = SitemapFileGenerator.class) +public class DslSitemapFileConverter extends AbstractSitemapFileGenerator { + + private final Logger logger = LoggerFactory.getLogger(DslSitemapFileConverter.class); + + private final ModelRepository modelRepository; + + private final ISerializer serializer; + + @Activate + public DslSitemapFileConverter(final @Reference ModelRepository modelRepository) { + this.modelRepository = modelRepository; + this.serializer = SitemapStandaloneSetup.doSetup().getInstance(ISerializer.class); + } + + @Override + public String getFileFormatGenerator() { + return "DSL"; + } + + @Override + public synchronized void generateFileFormat(OutputStream out, List sitemaps) { + if (sitemaps.isEmpty()) { + return; + } + for (Sitemap sitemap : sitemaps) { + modelRepository.generateSyntaxFromModel(out, "sitemap", sitemap, serializer); + try { + out.write(System.lineSeparator().getBytes()); + } catch (IOException e) { + logger.warn("Exception when saving the sitemap {}", sitemap.getName()); + } + } + } +} diff --git a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/validation/SitemapValidator.xtend b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/validation/SitemapValidator.xtend index 3749f783bc6..1f6a5a6cde5 100644 --- a/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/validation/SitemapValidator.xtend +++ b/bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/validation/SitemapValidator.xtend @@ -29,6 +29,11 @@ import java.math.BigDecimal import org.openhab.core.model.sitemap.sitemap.Input import org.eclipse.xtext.nodemodel.util.NodeModelUtils import org.openhab.core.model.sitemap.sitemap.Chart +import org.openhab.core.model.sitemap.sitemap.Webview +import org.openhab.core.model.sitemap.sitemap.Text +import org.openhab.core.model.sitemap.sitemap.Image +import org.openhab.core.model.sitemap.sitemap.Video +import org.openhab.core.model.sitemap.sitemap.Slider //import org.eclipse.xtext.validation.Check /** @@ -40,17 +45,45 @@ class SitemapValidator extends AbstractSitemapValidator { val ALLOWED_HINTS = #["text", "number", "date", "time", "datetime"] val ALLOWED_INTERPOLATION = #["linear", "step"] + + @Check + def void checkWidgetHasItem(Widget w) { + if (!(w instanceof Frame || w instanceof Text || w instanceof Image || w instanceof Video || w instanceof Webview || w instanceof Buttongrid) && w.item === null) { + val node = NodeModelUtils.getNode(w) + val line = node.getStartLine() + error("'" + w.getClass().getSimpleName() + "' widget doesn't have item defined at line " + line, + SitemapPackage.Literals.INPUT.getEStructuralFeature(SitemapPackage.WIDGET)) + } + } + + @Check + def void checkWidgetIcon(Widget w) { + if ((w.icon !== null || w.iconRules !== null) && w.staticIcon !== null) { + val node = NodeModelUtils.getNode(w) + val line = node.getStartLine() + error("Widget '" + w.getClass().getSimpleName() + "' has icon '" + w.icon + "' and staticIcon '" + w.staticIcon + "' defined at the same time " + line, + SitemapPackage.Literals.INPUT.getEStructuralFeature(SitemapPackage.WIDGET)) + } + if (w.icon !== null && w.iconRules !== null) { + val node = NodeModelUtils.getNode(w) + val line = node.getStartLine() + error("Widget '" + w.getClass().getSimpleName() + "' has icon '" + w.icon + "' and icon rules '" + w.iconRules + "' defined at the same time " + line, + SitemapPackage.Literals.INPUT.getEStructuralFeature(SitemapPackage.WIDGET)) + } + } @Check def void checkFramesInFrame(Frame frame) { for (Widget w : frame.children) { + val node = NodeModelUtils.getNode(w) + val line = node.getStartLine() if (w instanceof Frame) { - error("Frames must not contain other frames", + error("Frames must not contain other frames at line " + line, SitemapPackage.Literals.FRAME.getEStructuralFeature(SitemapPackage.FRAME__CHILDREN)); return; } if (w instanceof Button) { - error("Frames should not contain Button, Button is allowed only in Buttongrid", + error("Frames should not contain Button, Button is allowed only in Buttongrid at line " + line, SitemapPackage.Literals.FRAME.getEStructuralFeature(SitemapPackage.FRAME__CHILDREN)); return; } @@ -63,8 +96,10 @@ class SitemapValidator extends AbstractSitemapValidator { var containsOtherWidgets = false for (Widget w : sitemap.children) { + val node = NodeModelUtils.getNode(w) + val line = node.getStartLine() if (w instanceof Button) { - error("Sitemap should not contain Button, Button is allowed only in Buttongrid", + error("Sitemap should not contain Button, Button is allowed only in Buttongrid at line " + line, SitemapPackage.Literals.SITEMAP.getEStructuralFeature(SitemapPackage.SITEMAP__NAME)); return; } @@ -74,7 +109,7 @@ class SitemapValidator extends AbstractSitemapValidator { containsOtherWidgets = true } if (containsFrames && containsOtherWidgets) { - error("Sitemap should contain either only frames or none at all", + error("Sitemap should contain either only frames or none at all at line " + line, SitemapPackage.Literals.SITEMAP.getEStructuralFeature(SitemapPackage.SITEMAP__NAME)); return } @@ -94,8 +129,10 @@ class SitemapValidator extends AbstractSitemapValidator { var containsFrames = false var containsOtherWidgets = false for (Widget w : widget.children) { + val node = NodeModelUtils.getNode(w) + val line = node.getStartLine() if (w instanceof Button) { - error("Linkable widget should not contain Button, Button is allowed only in Buttongrid", + error("Linkable widget should not contain Button, Button is allowed only in Buttongrid at line " + line, SitemapPackage.Literals.FRAME.getEStructuralFeature(SitemapPackage.LINKABLE_WIDGET__CHILDREN)); return; } @@ -105,7 +142,7 @@ class SitemapValidator extends AbstractSitemapValidator { containsOtherWidgets = true } if (containsFrames && containsOtherWidgets) { - error("Linkable widget should contain either only frames or none at all", + error("Linkable widget should contain either only frames or none at all at line " + line, SitemapPackage.Literals.FRAME.getEStructuralFeature(SitemapPackage.LINKABLE_WIDGET__CHILDREN)); return } @@ -114,14 +151,18 @@ class SitemapValidator extends AbstractSitemapValidator { @Check def void checkWidgetsInButtongrid(Buttongrid grid) { - val nb = grid.getButtons.size() + val nb = grid.getButtons !== null ? grid.getButtons.getElements.size() : 0 if (nb > 0 && grid.item === null) { - error("To use the \"buttons\" parameter in a Buttongrid, the \"item\" parameter is required", + val node = NodeModelUtils.getNode(grid) + val line = node.getStartLine() + error("To use the \"buttons\" parameter in a Buttongrid, the \"item\" parameter is required at line " + line, SitemapPackage.Literals.BUTTONGRID.getEStructuralFeature(SitemapPackage.BUTTONGRID__ITEM)); } for (Widget w : grid.children) { if (!(w instanceof Button)) { - error("Buttongrid must contain only Button", + val node = NodeModelUtils.getNode(w) + val line = node.getStartLine() + error("Buttongrid must contain only Button at line " + line, SitemapPackage.Literals.BUTTONGRID.getEStructuralFeature(SitemapPackage.BUTTONGRID__CHILDREN)); return; } @@ -129,48 +170,82 @@ class SitemapValidator extends AbstractSitemapValidator { } @Check - def void checkSetpoints(Setpoint sp) { + def void checkSetpointParameters(Setpoint sp) { + val node = NodeModelUtils.getNode(sp) + val line = node.getStartLine() if (BigDecimal.ZERO == sp.step) { - error("Setpoint on item '" + sp.item + "' has step size of 0", + error("Setpoint widget has step size of '0' at line " + line, 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", + error("Setpoint has negative step size of '" + sp.step + "' at line " + line, 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", + error("Setpoint on item has larger minValue '" + sp.minValue + "' than maxValue '" + sp.maxValue + "' at line " + line, SitemapPackage.Literals.SETPOINT.getEStructuralFeature(SitemapPackage.SETPOINT__MIN_VALUE)); } } @Check - def void checkColortemperaturepicker(Colortemperaturepicker ctp) { + def void checkSliderParameters(Slider s) { + val node = NodeModelUtils.getNode(s) + val line = node.getStartLine() + if (BigDecimal.ZERO == s.step) { + error("Slider widget has step size of '0' at line " + line, + SitemapPackage.Literals.SLIDER.getEStructuralFeature(SitemapPackage.SLIDER__STEP)); + } + if (s.step !== null && s.step < BigDecimal.ZERO) { + error("Slider has negative step size of '" + s.step + "' at line " + line, + SitemapPackage.Literals.SLIDER.getEStructuralFeature(SitemapPackage.SLIDER__STEP)); + } + if (s.minValue !== null && s.maxValue !== null && s.minValue > s.maxValue) { + error("Slider on item has larger minValue '" + s.minValue + "' than maxValue '" + s.maxValue + "' at line " + line, + SitemapPackage.Literals.SLIDER.getEStructuralFeature(SitemapPackage.SLIDER__MIN_VALUE)); + } + } + + @Check + def void checkColortemperaturepickerParameters(Colortemperaturepicker ctp) { if (ctp.minValue !== null && ctp.maxValue !== null && ctp.minValue > ctp.maxValue) { - error("Colortemperaturepicker on item '" + ctp.item + "' has larger minValue than maxValue", + val node = NodeModelUtils.getNode(ctp) + val line = node.getStartLine() + error("Colortemperaturepicker widget has larger minValue '" + ctp.minValue + "' than maxValue '" + ctp.maxValue + "' at line " + line, SitemapPackage.Literals.COLORTEMPERATUREPICKER.getEStructuralFeature(SitemapPackage.COLORTEMPERATUREPICKER__MIN_VALUE)); } } @Check - def void checkInputHintParameter(Input i) { + def void checkInputParameters(Input i) { 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, + error("Input widget has invalid inputHint '" + i.inputHint + "' at line " + line, SitemapPackage.Literals.INPUT.getEStructuralFeature(SitemapPackage.INPUT__INPUT_HINT)) } } @Check - def void checkInterpolationParameter(Chart i) { - 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, + def void checkChartParameters(Chart c) { + val node = NodeModelUtils.getNode(c) + val line = node.getStartLine() + if (c.interpolation !== null && !ALLOWED_INTERPOLATION.contains(c.interpolation)) { + error("Chart widget has invalid interpolation '" + c.interpolation + "' at line " + line, SitemapPackage.Literals.INPUT.getEStructuralFeature(SitemapPackage.CHART__INTERPOLATION)) } + if (c.period === null) { + error("Chart widget doesn't have period defined at line " + line, + SitemapPackage.Literals.INPUT.getEStructuralFeature(SitemapPackage.CHART__PERIOD)) + } + } + + @Check + def void checkVideoParameters(Video v) { + if (v.url === null) { + val node = NodeModelUtils.getNode(v) + val line = node.getStartLine() + error("Video widget doesn't have url defined at line " + line, + SitemapPackage.Literals.INPUT.getEStructuralFeature(SitemapPackage.VIDEO__URL)) + } } } diff --git a/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/ThingStandaloneSetup.xtend b/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/ThingStandaloneSetup.xtend index 3514a130a6e..376eb9482cb 100644 --- a/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/ThingStandaloneSetup.xtend +++ b/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/ThingStandaloneSetup.xtend @@ -15,20 +15,25 @@ package org.openhab.core.model.thing import org.eclipse.emf.ecore.EPackage import org.eclipse.emf.ecore.resource.Resource import org.eclipse.xtext.resource.IResourceServiceProvider +import com.google.inject.Injector /** * Initialization support for running Xtext languages * without equinox extension registry */ class ThingStandaloneSetup extends ThingStandaloneSetupGenerated { - def static void doSetup() { - new ThingStandaloneSetup().createInjectorAndDoEMFRegistration() + static Injector injector; + + def static Injector doSetup() { + if (injector === null) { + injector = new ThingStandaloneSetup().createInjectorAndDoEMFRegistration(); + } + return injector; } - + def static void unregister() { EPackage.Registry.INSTANCE.remove("https://openhab.org/model/Thing"); Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().remove("things"); IResourceServiceProvider.Registry.INSTANCE.getExtensionToFactoryMap().remove("things"); } - } diff --git a/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/fileconverter/DslThingFileConverter.java b/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/fileconverter/DslThingFileConverter.java index 728b8cc801a..14868cde1ac 100644 --- a/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/fileconverter/DslThingFileConverter.java +++ b/bundles/org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/fileconverter/DslThingFileConverter.java @@ -22,8 +22,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.xtext.serializer.ISerializer; import org.openhab.core.config.core.ConfigDescriptionRegistry; import org.openhab.core.model.core.ModelRepository; +import org.openhab.core.model.thing.ThingStandaloneSetup; import org.openhab.core.model.thing.thing.ModelBridge; import org.openhab.core.model.thing.thing.ModelChannel; import org.openhab.core.model.thing.thing.ModelProperty; @@ -60,6 +62,8 @@ public class DslThingFileConverter extends AbstractThingFileGenerator { private final ModelRepository modelRepository; + private final ISerializer serializer; + @Activate public DslThingFileConverter(final @Reference ModelRepository modelRepository, final @Reference ThingTypeRegistry thingTypeRegistry, @@ -67,6 +71,7 @@ public DslThingFileConverter(final @Reference ModelRepository modelRepository, final @Reference ConfigDescriptionRegistry configDescRegistry) { super(thingTypeRegistry, channelTypeRegistry, configDescRegistry); this.modelRepository = modelRepository; + this.serializer = ThingStandaloneSetup.doSetup().getInstance(ISerializer.class); } @Override @@ -91,7 +96,7 @@ public synchronized void generateFileFormat(OutputStream out, List things // Double quotes are unexpectedly generated in thing UID when the segment contains a -. // Fix that by removing these double quotes. Requires to first build the generated syntax as a String ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - modelRepository.generateSyntaxFromModel(outputStream, "things", model); + modelRepository.generateSyntaxFromModel(outputStream, "things", model, serializer); String syntax = new String(outputStream.toByteArray()).replaceAll(":\"([a-zA-Z0-9_][a-zA-Z0-9_-]*)\"", ":$1"); try { out.write(syntax.getBytes()); diff --git a/bundles/org.openhab.core.model.yaml/pom.xml b/bundles/org.openhab.core.model.yaml/pom.xml index e4f130069ae..f3778f68c52 100644 --- a/bundles/org.openhab.core.model.yaml/pom.xml +++ b/bundles/org.openhab.core.model.yaml/pom.xml @@ -35,5 +35,15 @@ org.openhab.core.thing ${project.version} + + org.openhab.core.bundles + org.openhab.core.model.sitemap + ${project.version} + + + org.openhab.core.bom + org.openhab.core.bom.compile-model + pom + diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/sitemaps/YamlMappingDTO.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/sitemaps/YamlMappingDTO.java new file mode 100644 index 00000000000..ec386f868c6 --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/sitemaps/YamlMappingDTO.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.model.yaml.internal.sitemaps; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.model.yaml.YamlElement; +import org.openhab.core.model.yaml.YamlElementName; + +/** + * The {@link YamlMappingDTO} is a data transfer object used to serialize a sitemap in a YAML configuration file. + * + * @author Mark Herwege - Initial contribution + */ +@YamlElementName("mapping") +public class YamlMappingDTO implements YamlElement, Cloneable { + + public String uid; + public String cmd; + public String releaseCmd; + public String label; + public String icon; + + public YamlMappingDTO() { + } + + @Override + public @NonNull String getId() { + return uid == null ? "" : uid; + } + + @Override + public void setId(@NonNull String id) { + uid = id; + } + + @Override + public YamlElement cloneWithoutId() { + YamlMappingDTO copy; + try { + copy = (YamlMappingDTO) super.clone(); + copy.uid = null; + return copy; + } catch (CloneNotSupportedException e) { + // Will never happen + return new YamlMappingDTO(); + } + } + + @Override + public boolean isValid(@Nullable List<@NonNull String> errors, @Nullable List<@NonNull String> warnings) { + return true; + } + + @Override + public int hashCode() { + return Objects.hash(uid, cmd, label); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } else if (obj == null || getClass() != obj.getClass()) { + return false; + } + YamlMappingDTO other = (YamlMappingDTO) obj; + return Objects.equals(uid, other.uid) && Objects.equals(label, other.label) && Objects.equals(cmd, other.cmd); + } +} diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/sitemaps/YamlRuleDTO.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/sitemaps/YamlRuleDTO.java new file mode 100644 index 00000000000..043a8d017c6 --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/sitemaps/YamlRuleDTO.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.model.yaml.internal.sitemaps; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.model.yaml.YamlElement; +import org.openhab.core.model.yaml.YamlElementName; + +/** + * The {@link YamlRuleDTO} is a data transfer object used to serialize a sitemap widget rule in a YAML configuration + * file. + * + * @author Mark Herwege - Initial contribution + */ +@YamlElementName("rule") +public class YamlRuleDTO implements YamlElement, Cloneable { + + public String uid; + public List conditions; + public String argument; + + public YamlRuleDTO() { + } + + @Override + public @NonNull String getId() { + return uid == null ? "" : uid; + } + + @Override + public void setId(@NonNull String id) { + uid = id; + } + + @Override + public YamlElement cloneWithoutId() { + YamlRuleDTO copy; + try { + copy = (YamlRuleDTO) super.clone(); + copy.uid = null; + return copy; + } catch (CloneNotSupportedException e) { + // Will never happen + return new YamlRuleDTO(); + } + } + + @Override + public boolean isValid(@Nullable List<@NonNull String> errors, @Nullable List<@NonNull String> warnings) { + return true; + } + + @Override + public int hashCode() { + return Objects.hash(uid, conditions); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } else if (obj == null || getClass() != obj.getClass()) { + return false; + } + YamlRuleDTO other = (YamlRuleDTO) obj; + return Objects.equals(uid, other.uid) && Objects.equals(conditions, other.conditions); + } + + public class Condition { + public String item; + public String condition; + public String value; + } +} diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/sitemaps/YamlSitemapDTO.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/sitemaps/YamlSitemapDTO.java new file mode 100644 index 00000000000..a627b29feab --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/sitemaps/YamlSitemapDTO.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.model.yaml.internal.sitemaps; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.model.yaml.YamlElement; +import org.openhab.core.model.yaml.YamlElementName; + +/** + * The {@link YamlSitemapDTO} is a data transfer object used to serialize a sitemap in a YAML configuration file. + * + * @author Mark Herwege - Initial contribution + */ +@YamlElementName("sitemap") +public class YamlSitemapDTO implements YamlElement, Cloneable { + + public String uid; + public String label; + public String icon; + public List> widgets; + + public YamlSitemapDTO() { + } + + @Override + public @NonNull String getId() { + return uid == null ? "" : uid; + } + + @Override + public void setId(@NonNull String id) { + uid = id; + } + + @Override + public YamlElement cloneWithoutId() { + YamlSitemapDTO copy; + try { + copy = (YamlSitemapDTO) super.clone(); + copy.uid = null; + return copy; + } catch (CloneNotSupportedException e) { + // Will never happen + return new YamlSitemapDTO(); + } + } + + @Override + public boolean isValid(@Nullable List<@NonNull String> errors, @Nullable List<@NonNull String> warnings) { + return true; + } + + @Override + public int hashCode() { + return Objects.hash(uid, label); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } else if (obj == null || getClass() != obj.getClass()) { + return false; + } + YamlSitemapDTO other = (YamlSitemapDTO) obj; + return Objects.equals(uid, other.uid); + } +} diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/sitemaps/YamlWidgetDTO.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/sitemaps/YamlWidgetDTO.java new file mode 100644 index 00000000000..bdab5da763a --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/sitemaps/YamlWidgetDTO.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.model.yaml.internal.sitemaps; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.model.yaml.YamlElement; +import org.openhab.core.model.yaml.YamlElementName; + +/** + * The {@link YamlWidgetDTO} is a data transfer object used to serialize a sitemap widget in a YAML configuration file. + * + * @author Mark Herwege - Initial contribution + */ +@YamlElementName("widget") +public class YamlWidgetDTO implements YamlElement, Cloneable { + + public String item; + public String label; + public String icon; + public List iconRules; + public Boolean staticIcon; + + public String url; + public Integer refresh; + public String encoding; + public String service; + public String period; + public Boolean legend; + public Boolean forceAsItem; + public String yAxisDecimalPattern; + public String interpolation; + public Integer height; + public Boolean switchEnabled; + public Boolean releaseOnly; + public BigDecimal minValue; + public BigDecimal maxValue; + public BigDecimal step; + public String inputHint; + public Integer row; + public Integer column; + public Boolean stateless; + public String cmd; + public String releaseCmd; + + public List mappings; + + public List labelColor; + public List valueColor; + public List iconColor; + public List visibility; + + public List> widgets; + + public YamlWidgetDTO() { + } + + @Override + public @NonNull String getId() { + return ""; + } + + @Override + public void setId(@NonNull String id) { + } + + @Override + public YamlElement cloneWithoutId() { + YamlWidgetDTO copy; + try { + copy = (YamlWidgetDTO) super.clone(); + return copy; + } catch (CloneNotSupportedException e) { + // Will never happen + return new YamlWidgetDTO(); + } + } + + @Override + public boolean isValid(@Nullable List<@NonNull String> errors, @Nullable List<@NonNull String> warnings) { + return true; + } + + @Override + public int hashCode() { + return Objects.hash(item, label, widgets); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } else if (obj == null || getClass() != obj.getClass()) { + return false; + } + YamlWidgetDTO other = (YamlWidgetDTO) obj; + return Objects.equals(item, other.item) && Objects.equals(label, other.label) + && Objects.equals(widgets, other.widgets); + } +} diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/sitemaps/fileconverter/YamlSitemapFileConverter.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/sitemaps/fileconverter/YamlSitemapFileConverter.java new file mode 100644 index 00000000000..91b08ba559b --- /dev/null +++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/sitemaps/fileconverter/YamlSitemapFileConverter.java @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.model.yaml.internal.sitemaps.fileconverter; + +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.emf.common.util.EList; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.model.sitemap.fileconverter.AbstractSitemapFileGenerator; +import org.openhab.core.model.sitemap.fileconverter.SitemapFileGenerator; +import org.openhab.core.model.sitemap.sitemap.ButtonDefinitionList; +import org.openhab.core.model.sitemap.sitemap.Buttongrid; +import org.openhab.core.model.sitemap.sitemap.ColorArray; +import org.openhab.core.model.sitemap.sitemap.ColorArrayList; +import org.openhab.core.model.sitemap.sitemap.Condition; +import org.openhab.core.model.sitemap.sitemap.IconRule; +import org.openhab.core.model.sitemap.sitemap.IconRuleList; +import org.openhab.core.model.sitemap.sitemap.LinkableWidget; +import org.openhab.core.model.sitemap.sitemap.Mapping; +import org.openhab.core.model.sitemap.sitemap.MappingList; +import org.openhab.core.model.sitemap.sitemap.Sitemap; +import org.openhab.core.model.sitemap.sitemap.SitemapFactory; +import org.openhab.core.model.sitemap.sitemap.SitemapPackage; +import org.openhab.core.model.sitemap.sitemap.VisibilityRule; +import org.openhab.core.model.sitemap.sitemap.VisibilityRuleList; +import org.openhab.core.model.sitemap.sitemap.Widget; +import org.openhab.core.model.sitemap.sitemap.impl.ButtonImpl; +import org.openhab.core.model.sitemap.sitemap.impl.ButtongridImpl; +import org.openhab.core.model.sitemap.sitemap.impl.ChartImpl; +import org.openhab.core.model.sitemap.sitemap.impl.ColortemperaturepickerImpl; +import org.openhab.core.model.sitemap.sitemap.impl.DefaultImpl; +import org.openhab.core.model.sitemap.sitemap.impl.ImageImpl; +import org.openhab.core.model.sitemap.sitemap.impl.InputImpl; +import org.openhab.core.model.sitemap.sitemap.impl.MapviewImpl; +import org.openhab.core.model.sitemap.sitemap.impl.SelectionImpl; +import org.openhab.core.model.sitemap.sitemap.impl.SetpointImpl; +import org.openhab.core.model.sitemap.sitemap.impl.SliderImpl; +import org.openhab.core.model.sitemap.sitemap.impl.SwitchImpl; +import org.openhab.core.model.sitemap.sitemap.impl.VideoImpl; +import org.openhab.core.model.sitemap.sitemap.impl.WebviewImpl; +import org.openhab.core.model.yaml.YamlElement; +import org.openhab.core.model.yaml.YamlModelRepository; +import org.openhab.core.model.yaml.internal.sitemaps.YamlMappingDTO; +import org.openhab.core.model.yaml.internal.sitemaps.YamlRuleDTO; +import org.openhab.core.model.yaml.internal.sitemaps.YamlSitemapDTO; +import org.openhab.core.model.yaml.internal.sitemaps.YamlWidgetDTO; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * {@link YamlSitemapFileConverter} is the YAML file converter for {@link Sitemap} objects. + * + * @author Mark Herwege - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = SitemapFileGenerator.class) +public class YamlSitemapFileConverter extends AbstractSitemapFileGenerator { + + private final YamlModelRepository modelRepository; + + @Activate + public YamlSitemapFileConverter(final @Reference YamlModelRepository modelRepository) { + super(); + this.modelRepository = modelRepository; + } + + @Override + public String getFileFormatGenerator() { + return "YAML"; + } + + @Override + public synchronized void generateFileFormat(OutputStream out, List sitemaps) { + List elements = new ArrayList<>(); + sitemaps.forEach(sitemap -> { + elements.add(buildSitemapDTO(sitemap)); + }); + modelRepository.generateSyntaxFromElements(out, elements); + } + + private YamlSitemapDTO buildSitemapDTO(Sitemap sitemap) { + YamlSitemapDTO dto = new YamlSitemapDTO(); + dto.uid = sitemap.getName(); + dto.label = sitemap.getLabel(); + dto.icon = sitemap.getIcon(); + EList children = sitemap.getChildren(); + if (children != null && !children.isEmpty()) { + dto.widgets = children.stream().map(w -> buildWidgetDTO(w)).toList(); + } + return dto; + } + + private Map.Entry buildWidgetDTO(Widget widget) { + YamlWidgetDTO dto = new YamlWidgetDTO(); + if (!(widget instanceof ButtongridImpl)) { + // We will convert Buttongrid with buttons definition to explicit Button widget, so don't set item + dto.item = widget.getItem(); + } + dto.label = widget.getLabel(); + + dto.icon = widget.getStaticIcon() != null ? widget.getStaticIcon() + : (widget.getIconRules() == null ? widget.getIcon() : null); + IconRuleList iconRuleList = widget.getIconRules(); + if (iconRuleList != null && iconRuleList.getElements() != null && !iconRuleList.getElements().isEmpty()) { + dto.iconRules = iconRuleList.getElements().stream().map(e -> buildRuleDTO(e)).toList(); + } + dto.staticIcon = widget.getStaticIcon() != null ? true : null; + + switch (widget) { + case ImageImpl imageWidget: { + dto.url = imageWidget.getUrl(); + dto.refresh = imageWidget.getRefresh(); + break; + } + case VideoImpl videoWidget: { + dto.url = videoWidget.getUrl(); + dto.encoding = videoWidget.getEncoding(); + break; + } + case ChartImpl chartWidget: { + dto.service = chartWidget.getService(); + dto.refresh = chartWidget.getRefresh(); + dto.period = chartWidget.getPeriod(); + dto.legend = chartWidget.getLegend(); + dto.forceAsItem = chartWidget.getForceAsItem() ? true : null; + dto.yAxisDecimalPattern = chartWidget.getYAxisDecimalPattern(); + dto.interpolation = chartWidget.getInterpolation(); + break; + } + case WebviewImpl webviewWidget: { + dto.height = webviewWidget.getHeight(); + dto.url = webviewWidget.getUrl(); + break; + } + case SwitchImpl switchWidget: { + MappingList mappingList = switchWidget.getMappings(); + if (mappingList != null && mappingList.getElements() != null && mappingList.getElements().size() > 0) { + dto.mappings = mappingList.getElements().stream().map(e -> buildMappingDTO(e)) + .filter(Objects::nonNull).toList(); + } + break; + } + case MapviewImpl mapviewWidget: { + dto.height = mapviewWidget.getHeight(); + break; + } + case SliderImpl sliderWidget: { + dto.switchEnabled = sliderWidget.isSwitchEnabled() ? true : null; + dto.releaseOnly = sliderWidget.isReleaseOnly() ? true : null; + dto.minValue = sliderWidget.getMinValue(); + dto.maxValue = sliderWidget.getMaxValue(); + dto.step = sliderWidget.getStep(); + break; + } + case SelectionImpl selectionWidget: { + MappingList mappingList = selectionWidget.getMappings(); + if (mappingList != null && mappingList.getElements() != null && mappingList.getElements().size() > 0) { + dto.mappings = mappingList.getElements().stream().map(e -> buildMappingDTO(e)) + .filter(Objects::nonNull).toList(); + } + break; + } + case SetpointImpl setpointWidget: { + dto.minValue = setpointWidget.getMinValue(); + dto.maxValue = setpointWidget.getMaxValue(); + dto.step = setpointWidget.getStep(); + break; + } + case ColortemperaturepickerImpl colortemperaturepickerWidget: { + dto.minValue = colortemperaturepickerWidget.getMinValue(); + dto.maxValue = colortemperaturepickerWidget.getMaxValue(); + break; + } + case InputImpl inputWidget: { + dto.inputHint = inputWidget.getInputHint(); + break; + } + case ButtongridImpl buttongridWidget: { + List> buttons = convertToButtonWidgets(buttongridWidget); + if (buttons != null) { + dto.widgets = buttons; + } + break; + } + case ButtonImpl buttonWidget: { + dto.row = buttonWidget.getRow(); + dto.column = buttonWidget.getColumn(); + dto.stateless = buttonWidget.isStateless() ? true : null; + dto.cmd = buttonWidget.getCmd(); + dto.releaseCmd = buttonWidget.getReleaseCmd(); + break; + } + case DefaultImpl defaultWidget: { + dto.height = defaultWidget.getHeight(); + break; + } + default: + break; + } + + ColorArrayList labelColorList = widget.getLabelColor(); + if (labelColorList != null && labelColorList.getElements() != null && labelColorList.getElements().size() > 0) { + dto.labelColor = labelColorList.getElements().stream().map(e -> buildRuleDTO(e)).toList(); + } + ColorArrayList valueColorList = widget.getValueColor(); + if (valueColorList != null && valueColorList.getElements() != null && valueColorList.getElements().size() > 0) { + dto.valueColor = valueColorList.getElements().stream().map(e -> buildRuleDTO(e)).toList(); + } + ColorArrayList iconColorList = widget.getIconColor(); + if (iconColorList != null && iconColorList.getElements() != null && iconColorList.getElements().size() > 0) { + dto.iconColor = iconColorList.getElements().stream().map(e -> buildRuleDTO(e)).toList(); + } + VisibilityRuleList visiblityRuleList = widget.getVisibility(); + if (visiblityRuleList != null && visiblityRuleList.getElements() != null + && visiblityRuleList.getElements().size() > 0) { + dto.visibility = visiblityRuleList.getElements().stream().map(e -> buildRuleDTO(e)).toList(); + } + + if (widget instanceof LinkableWidget linkableWidget) { + EList children = linkableWidget.getChildren(); + if (children != null && !children.isEmpty()) { + dto.widgets = children.stream().map(w -> buildWidgetDTO(w)).toList(); + } + } + + String widgetType = widget.getClass().getInterfaces()[0].getSimpleName(); + return Map.entry(widgetType, dto); + } + + private @Nullable YamlRuleDTO buildRuleDTO(T rule) { + EList conditions = null; + String argument = null; + switch (rule) { + case IconRule iconRule: { + conditions = iconRule.getConditions(); + argument = iconRule.getArg(); + break; + } + case ColorArray colorArray: { + conditions = colorArray.getConditions(); + argument = colorArray.getArg(); + break; + } + case VisibilityRule visiblityRule: { + conditions = visiblityRule.getConditions(); + break; + } + default: + break; + } + if (conditions == null) { + return null; + } + YamlRuleDTO dto = new YamlRuleDTO(); + dto.conditions = conditions.stream().map(c -> { + YamlRuleDTO.Condition condition = dto.new Condition(); + condition.item = c.getItem(); + condition.condition = c.getCondition(); + condition.value = (c.getSign() != null ? c.getSign() : "") + c.getState(); + return condition; + }).toList(); + dto.argument = argument; + return dto; + } + + private YamlMappingDTO buildMappingDTO(Mapping mapping) { + YamlMappingDTO dto = new YamlMappingDTO(); + dto.cmd = mapping.getCmd(); + dto.releaseCmd = mapping.getReleaseCmd(); + dto.label = mapping.getLabel(); + dto.icon = mapping.getIcon(); + return dto; + } + + private @Nullable List> convertToButtonWidgets(Buttongrid widget) { + String item = widget.getItem(); + ButtonDefinitionList buttons = widget.getButtons(); + if (buttons == null || buttons.getElements() == null) { + return null; + } + return buttons.getElements().stream().map(b -> { + ButtonImpl buttonWidget = (ButtonImpl) SitemapFactory.eINSTANCE.createButton(); + buttonWidget.eSet(SitemapPackage.BUTTON__ITEM, item); + buttonWidget.eSet(SitemapPackage.BUTTON__ROW, b.getRow()); + buttonWidget.eSet(SitemapPackage.BUTTON__COLUMN, b.getColumn()); + buttonWidget.eSet(SitemapPackage.BUTTON__CMD, b.getCmd()); + buttonWidget.eSet(SitemapPackage.BUTTON__LABEL, b.getLabel()); + buttonWidget.eSet(SitemapPackage.BUTTON__ICON, b.getIcon()); + return buildWidgetDTO(buttonWidget); + }).toList(); + } +} diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/components/UIComponentSitemapProvider.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/components/UIComponentSitemapProvider.java index 901c4152cb3..9118a35e8b5 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/components/UIComponentSitemapProvider.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/components/UIComponentSitemapProvider.java @@ -24,7 +24,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.eclipse.emf.common.util.EList; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.common.registry.RegistryChangeListener; @@ -32,15 +31,15 @@ import org.openhab.core.model.core.EventType; import org.openhab.core.model.core.ModelRepositoryChangeListener; import org.openhab.core.model.sitemap.SitemapProvider; -import org.openhab.core.model.sitemap.sitemap.ButtonDefinition; -import org.openhab.core.model.sitemap.sitemap.ColorArray; -import org.openhab.core.model.sitemap.sitemap.IconRule; +import org.openhab.core.model.sitemap.sitemap.ButtonDefinitionList; +import org.openhab.core.model.sitemap.sitemap.ColorArrayList; +import org.openhab.core.model.sitemap.sitemap.IconRuleList; import org.openhab.core.model.sitemap.sitemap.LinkableWidget; -import org.openhab.core.model.sitemap.sitemap.Mapping; +import org.openhab.core.model.sitemap.sitemap.MappingList; import org.openhab.core.model.sitemap.sitemap.Sitemap; import org.openhab.core.model.sitemap.sitemap.SitemapFactory; import org.openhab.core.model.sitemap.sitemap.SitemapPackage; -import org.openhab.core.model.sitemap.sitemap.VisibilityRule; +import org.openhab.core.model.sitemap.sitemap.VisibilityRuleList; import org.openhab.core.model.sitemap.sitemap.Widget; import org.openhab.core.model.sitemap.sitemap.impl.ButtonDefinitionImpl; import org.openhab.core.model.sitemap.sitemap.impl.ButtonImpl; @@ -385,8 +384,8 @@ private void setWidgetIconPropertyFromComponentConfig(Widget widget, @Nullable U } } - private void addWidgetMappings(EList mappings, UIComponent component) { - if (component.getConfig() != null && component.getConfig().containsKey("mappings")) { + private void addWidgetMappings(@Nullable MappingList mappings, UIComponent component) { + if (mappings != null && component.getConfig() != null && component.getConfig().containsKey("mappings")) { Object sourceMappings = component.getConfig().get("mappings"); if (sourceMappings instanceof Collection sourceMappingsCollection) { for (Object sourceMapping : sourceMappingsCollection) { @@ -408,15 +407,15 @@ private void addWidgetMappings(EList mappings, UIComponent component) { mapping.setReleaseCmd(releaseCmd); mapping.setLabel(label); mapping.setIcon(icon); - mappings.add(mapping); + mappings.getElements().add(mapping); } } } } } - private void addWidgetButtons(EList buttons, UIComponent component) { - if (component.getConfig() != null && component.getConfig().containsKey("buttons")) { + private void addWidgetButtons(@Nullable ButtonDefinitionList buttons, UIComponent component) { + if (buttons != null && component.getConfig() != null && component.getConfig().containsKey("buttons")) { Object sourceButtons = component.getConfig().get("buttons"); if (sourceButtons instanceof Collection sourceButtonsCollection) { for (Object sourceButton : sourceButtonsCollection) { @@ -435,15 +434,15 @@ private void addWidgetButtons(EList buttons, UIComponent compo button.setCmd(cmd); button.setLabel(label); button.setIcon(icon); - buttons.add(button); + buttons.getElements().add(button); } } } } } - private void addWidgetVisibility(EList visibility, UIComponent component) { - if (component.getConfig() != null && component.getConfig().containsKey("visibility")) { + private void addWidgetVisibility(@Nullable VisibilityRuleList visibility, UIComponent component) { + if (visibility != null && component.getConfig() != null && component.getConfig().containsKey("visibility")) { Object sourceVisibilities = component.getConfig().get("visibility"); if (sourceVisibilities instanceof Collection sourceVisibilitiesCollection) { for (Object sourceVisibility : sourceVisibilitiesCollection) { @@ -453,27 +452,27 @@ private void addWidgetVisibility(EList visibility, UIComponent c .createVisibilityRule(); List conditions = getConditions(conditionsString, component, "visibility"); visibilityRule.eSet(SitemapPackage.VISIBILITY_RULE__CONDITIONS, conditions); - visibility.add(visibilityRule); + visibility.getElements().add(visibilityRule); } } } } } - private void addLabelColor(EList labelColor, UIComponent component) { + private void addLabelColor(@Nullable ColorArrayList labelColor, UIComponent component) { addColor(labelColor, component, "labelcolor"); } - private void addValueColor(EList valueColor, UIComponent component) { + private void addValueColor(@Nullable ColorArrayList valueColor, UIComponent component) { addColor(valueColor, component, "valuecolor"); } - private void addIconColor(EList iconColor, UIComponent component) { + private void addIconColor(@Nullable ColorArrayList iconColor, UIComponent component) { addColor(iconColor, component, "iconcolor"); } - private void addColor(EList color, UIComponent component, String key) { - if (component.getConfig() != null && component.getConfig().containsKey(key)) { + private void addColor(@Nullable ColorArrayList color, UIComponent component, String key) { + if (color != null && component.getConfig() != null && component.getConfig().containsKey(key)) { Object sourceColors = component.getConfig().get(key); if (sourceColors instanceof Collection sourceColorsCollection) { for (Object sourceColor : sourceColorsCollection) { @@ -484,15 +483,15 @@ private void addColor(EList color, UIComponent component, String key colorArray.setArg(argument); List conditions = getConditions(conditionsString, component, key); colorArray.eSet(SitemapPackage.COLOR_ARRAY__CONDITIONS, conditions); - color.add(colorArray); + color.getElements().add(colorArray); } } } } } - private void addIconRules(EList icon, UIComponent component) { - if (component.getConfig() != null && component.getConfig().containsKey("iconrules")) { + private void addIconRules(@Nullable IconRuleList icon, UIComponent component) { + if (icon != null && component.getConfig() != null && component.getConfig().containsKey("iconrules")) { Object sourceIcons = component.getConfig().get("iconrules"); if (sourceIcons instanceof Collection sourceIconsCollection) { for (Object sourceIcon : sourceIconsCollection) { @@ -503,7 +502,7 @@ private void addIconRules(EList icon, UIComponent component) { iconRule.setArg(argument); List conditions = getConditions(conditionsString, component, "iconrules"); iconRule.eSet(SitemapPackage.ICON_RULE__CONDITIONS, conditions); - icon.add(iconRule); + icon.getElements().add(iconRule); } } } diff --git a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java index f78dc067be8..b06ee6adc8a 100644 --- a/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java +++ b/bundles/org.openhab.core.ui/src/main/java/org/openhab/core/ui/internal/items/ItemUIRegistryImpl.java @@ -65,16 +65,20 @@ import org.openhab.core.library.types.PlayPauseType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.model.sitemap.sitemap.ColorArray; +import org.openhab.core.model.sitemap.sitemap.ColorArrayList; import org.openhab.core.model.sitemap.sitemap.Default; import org.openhab.core.model.sitemap.sitemap.Group; import org.openhab.core.model.sitemap.sitemap.IconRule; +import org.openhab.core.model.sitemap.sitemap.IconRuleList; import org.openhab.core.model.sitemap.sitemap.LinkableWidget; import org.openhab.core.model.sitemap.sitemap.Mapping; +import org.openhab.core.model.sitemap.sitemap.MappingList; import org.openhab.core.model.sitemap.sitemap.Sitemap; import org.openhab.core.model.sitemap.sitemap.SitemapFactory; import org.openhab.core.model.sitemap.sitemap.Slider; import org.openhab.core.model.sitemap.sitemap.Switch; import org.openhab.core.model.sitemap.sitemap.VisibilityRule; +import org.openhab.core.model.sitemap.sitemap.VisibilityRuleList; import org.openhab.core.model.sitemap.sitemap.Widget; import org.openhab.core.transform.TransformationException; import org.openhab.core.transform.TransformationHelper; @@ -323,7 +327,8 @@ && hasItemTag(itemName, "ColorTemperature")) { private Switch createPlayerButtons() { final Switch playerItemSwitch = SitemapFactory.eINSTANCE.createSwitch(); - final List mappings = playerItemSwitch.getMappings(); + final MappingList mappingList = SitemapFactory.eINSTANCE.createMappingList(); + final List mappings = mappingList.getElements(); Mapping commandMapping; mappings.add(commandMapping = SitemapFactory.eINSTANCE.createMapping()); commandMapping.setCmd(NextPreviousType.PREVIOUS.name()); @@ -337,6 +342,7 @@ private Switch createPlayerButtons() { mappings.add(commandMapping = SitemapFactory.eINSTANCE.createMapping()); commandMapping.setCmd(NextPreviousType.NEXT.name()); commandMapping.setLabel(">>"); + playerItemSwitch.setMappings(mappingList); return playerItemSwitch; } @@ -754,7 +760,8 @@ private String transform(String label, boolean matchTransform, @Nullable String } } else if (w instanceof Switch sw) { StateDescription stateDescr = i.getStateDescription(); - if (sw.getMappings().isEmpty() && (stateDescr == null || stateDescr.getOptions().isEmpty())) { + if ((sw.getMappings() == null || sw.getMappings().getElements().isEmpty()) + && (stateDescr == null || stateDescr.getOptions().isEmpty())) { returnState = itemState.as(OnOffType.class); } } @@ -853,11 +860,11 @@ private void copyProperties(Widget source, Widget target) { target.setIcon(source.getIcon()); target.setStaticIcon(source.getStaticIcon()); target.setLabel(source.getLabel()); - target.getVisibility().addAll(EcoreUtil.copyAll(source.getVisibility())); - target.getLabelColor().addAll(EcoreUtil.copyAll(source.getLabelColor())); - target.getValueColor().addAll(EcoreUtil.copyAll(source.getValueColor())); - target.getIconColor().addAll(EcoreUtil.copyAll(source.getIconColor())); - target.getIconRules().addAll(EcoreUtil.copyAll(source.getIconRules())); + target.setVisibility(EcoreUtil.copy(source.getVisibility())); + target.setLabelColor(EcoreUtil.copy(source.getLabelColor())); + target.setValueColor(EcoreUtil.copy(source.getValueColor())); + target.setIconColor(EcoreUtil.copy(source.getIconColor())); + target.setIconRules(EcoreUtil.copy(source.getIconRules())); } /** @@ -1207,9 +1214,14 @@ private boolean matchStateToValue(State state, String value, @Nullable String ma return matched; } - private @Nullable String processColorDefinition(Widget w, @Nullable List colorList, String colorType) { + private @Nullable String processColorDefinition(Widget w, @Nullable ColorArrayList colorArrayList, + String colorType) { + if (colorArrayList == null) { + return null; + } + List colorList = colorArrayList.getElements(); // Sanity check - if (colorList == null || colorList.isEmpty()) { + if (colorList.isEmpty()) { return null; } @@ -1258,9 +1270,13 @@ private boolean matchStateToValue(State state, String value, @Nullable String ma @Override public boolean getVisiblity(Widget w) { + VisibilityRuleList visibilityRuleList = w.getVisibility(); // Default to visible if parameters not set - List ruleList = w.getVisibility(); - if (ruleList == null || ruleList.isEmpty()) { + if (visibilityRuleList == null) { + return true; + } + List ruleList = visibilityRuleList.getElements(); + if (ruleList.isEmpty()) { return true; } @@ -1279,9 +1295,13 @@ public boolean getVisiblity(Widget w) { @Override public @Nullable String getConditionalIcon(Widget w) { - List ruleList = w.getIconRules(); + IconRuleList iconRuleList = w.getIconRules(); + if (iconRuleList == null) { + return null; + } + List ruleList = iconRuleList.getElements(); // Sanity check - if (ruleList == null || ruleList.isEmpty()) { + if (ruleList.isEmpty()) { return null; } diff --git a/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/items/ItemUIRegistryImplTest.java b/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/items/ItemUIRegistryImplTest.java index baef93ca74b..88df324c7f9 100644 --- a/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/items/ItemUIRegistryImplTest.java +++ b/bundles/org.openhab.core.ui/src/test/java/org/openhab/core/ui/internal/items/ItemUIRegistryImplTest.java @@ -62,12 +62,15 @@ import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; import org.openhab.core.model.sitemap.sitemap.ColorArray; +import org.openhab.core.model.sitemap.sitemap.ColorArrayList; import org.openhab.core.model.sitemap.sitemap.Colorpicker; import org.openhab.core.model.sitemap.sitemap.Condition; import org.openhab.core.model.sitemap.sitemap.Group; import org.openhab.core.model.sitemap.sitemap.IconRule; +import org.openhab.core.model.sitemap.sitemap.IconRuleList; import org.openhab.core.model.sitemap.sitemap.Image; import org.openhab.core.model.sitemap.sitemap.Mapping; +import org.openhab.core.model.sitemap.sitemap.MappingList; import org.openhab.core.model.sitemap.sitemap.Mapview; import org.openhab.core.model.sitemap.sitemap.Selection; import org.openhab.core.model.sitemap.sitemap.Sitemap; @@ -76,6 +79,7 @@ import org.openhab.core.model.sitemap.sitemap.Switch; import org.openhab.core.model.sitemap.sitemap.Text; import org.openhab.core.model.sitemap.sitemap.VisibilityRule; +import org.openhab.core.model.sitemap.sitemap.VisibilityRuleList; import org.openhab.core.model.sitemap.sitemap.Widget; import org.openhab.core.types.CommandDescriptionBuilder; import org.openhab.core.types.CommandOption; @@ -591,7 +595,9 @@ public void testStateConversionForSwitchWidgetThroughGetState() throws ItemNotFo Switch switchWidget = mock(Switch.class); when(switchWidget.getItem()).thenReturn("myItem"); - when(switchWidget.getMappings()).thenReturn(new BasicEList<>()); + MappingList mappingList = mock(MappingList.class); + when(switchWidget.getMappings()).thenReturn(mappingList); + when(switchWidget.getMappings().getElements()).thenReturn(new BasicEList<>()); State stateForSwitch = uiRegistry.getState(switchWidget); @@ -614,7 +620,9 @@ public void testStateConversionForSwitchWidgetWithMappingThroughGetState() throw Mapping mapping = mock(Mapping.class); BasicEList mappings = new BasicEList<>(); mappings.add(mapping); - when(switchWidget.getMappings()).thenReturn(mappings); + MappingList mappingList = mock(MappingList.class); + when(switchWidget.getMappings()).thenReturn(mappingList); + when(switchWidget.getMappings().getElements()).thenReturn(mappings); State stateForSwitch = uiRegistry.getState(switchWidget); @@ -868,7 +876,9 @@ public void getLabelColorLabelWithDecimalValue() { when(rule.getArg()).thenReturn("yellow"); BasicEList rules = new BasicEList<>(); rules.add(rule); - when(widgetMock.getLabelColor()).thenReturn(rules); + ColorArrayList colorArrayList = mock(ColorArrayList.class); + when(widgetMock.getLabelColor()).thenReturn(colorArrayList); + when(widgetMock.getLabelColor().getElements()).thenReturn(rules); when(itemMock.getState()).thenReturn(new DecimalType(10f / 3f)); @@ -897,7 +907,9 @@ public void getLabelColorLabelWithUnitValue() { when(rule.getArg()).thenReturn("yellow"); BasicEList rules = new BasicEList<>(); rules.add(rule); - when(widgetMock.getLabelColor()).thenReturn(rules); + ColorArrayList colorArrayList = mock(ColorArrayList.class); + when(widgetMock.getLabelColor()).thenReturn(colorArrayList); + when(widgetMock.getLabelColor().getElements()).thenReturn(rules); when(itemMock.getState()).thenReturn(new QuantityType<>("20 °C")); @@ -940,7 +952,7 @@ public void getDefaultWidgets() { defaultWidget = uiRegistry.getDefaultWidget(PlayerItem.class, ITEM_NAME); assertThat(defaultWidget, is(instanceOf(Switch.class))); - assertThat(((Switch) defaultWidget).getMappings(), hasSize(4)); + assertThat(((Switch) defaultWidget).getMappings().getElements(), hasSize(4)); defaultWidget = uiRegistry.getDefaultWidget(RollershutterItem.class, ITEM_NAME); assertThat(defaultWidget, is(instanceOf(Switch.class))); @@ -1098,7 +1110,9 @@ public void getLabelColorDefaultColor() { when(rule3.getConditions()).thenReturn(conditions5); when(rule3.getArg()).thenReturn("blue"); rules.add(rule3); - when(widgetMock.getLabelColor()).thenReturn(rules); + ColorArrayList colorArrayList = mock(ColorArrayList.class); + when(widgetMock.getLabelColor()).thenReturn(colorArrayList); + when(widgetMock.getLabelColor().getElements()).thenReturn(rules); when(itemMock.getState()).thenReturn(new DecimalType(20.9)); @@ -1165,7 +1179,9 @@ public void getValueColor() { when(rule3.getConditions()).thenReturn(conditions5); when(rule3.getArg()).thenReturn("blue"); rules.add(rule3); - when(widgetMock.getValueColor()).thenReturn(rules); + ColorArrayList colorArrayList = mock(ColorArrayList.class); + when(widgetMock.getValueColor()).thenReturn(colorArrayList); + when(widgetMock.getValueColor().getElements()).thenReturn(rules); when(itemMock.getState()).thenReturn(new DecimalType(20.9)); @@ -1232,7 +1248,9 @@ public void getIconColor() { when(rule3.getConditions()).thenReturn(conditions5); when(rule3.getArg()).thenReturn("blue"); rules.add(rule3); - when(widgetMock.getIconColor()).thenReturn(rules); + ColorArrayList colorArrayList = mock(ColorArrayList.class); + when(widgetMock.getIconColor()).thenReturn(colorArrayList); + when(widgetMock.getIconColor().getElements()).thenReturn(rules); when(itemMock.getState()).thenReturn(new DecimalType(20.9)); @@ -1280,7 +1298,9 @@ public void getVisibility() { when(rule.getConditions()).thenReturn(conditions); BasicEList rules = new BasicEList<>(); rules.add(rule); - when(widgetMock.getVisibility()).thenReturn(rules); + VisibilityRuleList visibilityRuleList = mock(VisibilityRuleList.class); + when(widgetMock.getVisibility()).thenReturn(visibilityRuleList); + when(widgetMock.getVisibility().getElements()).thenReturn(rules); when(itemMock.getState()).thenReturn(new DecimalType(20.9)); @@ -1340,7 +1360,9 @@ public void getCategoryWhenIconSetWithRules() { when(rule2.getConditions()).thenReturn(conditions2); when(rule2.getArg()).thenReturn("humidity"); rules.add(rule2); - when(widgetMock.getIconRules()).thenReturn(rules); + IconRuleList iconRuleList = mock(IconRuleList.class); + when(widgetMock.getIconRules()).thenReturn(iconRuleList); + when(widgetMock.getIconRules().getElements()).thenReturn(rules); when(itemMock.getState()).thenReturn(new DecimalType(20.9));