Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion bundles/org.openhab.core.io.rest.core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,26 @@
<artifactId>org.openhab.core.semantics</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.model.sitemap</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bom</groupId>
<artifactId>org.openhab.core.bom.compile-model</artifactId>
<type>pom</type>
</dependency>
Comment on lines +68 to +82

@lolodomo lolodomo Aug 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the move of the 2 classes SitemapFileGenerator and AbstractSitemapFileGenerator in org.openhab.core.ui, just adding a dependency to org.openhab.core.ui should be sufficient.

<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.test</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = """
Comment on lines +184 to +192

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide the same example in both formats.

version: 2

@lolodomo lolodomo Aug 24, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

version should be 1.

I see it was kept to 2 for other examples in that file, that is to be fixed in a separate PR so that it can be easily backported to 5.0.x. I will create it.

sitemaps:
MySitemap:
label: Label
widgets:
MyWidget:
type: Switch
label: Label
item: MyItem
""";

private final Logger logger = LoggerFactory.getLogger(FileFormatResource.class);

private final ItemRegistry itemRegistry;
Expand All @@ -184,8 +210,10 @@ public class FileFormatResource implements RESTResource {
private final Inbox inbox;
private final ThingTypeRegistry thingTypeRegistry;
private final ConfigDescriptionRegistry configDescRegistry;
private final List<SitemapProvider> sitemapProviders = new ArrayList<>();
private final Map<String, ItemFileGenerator> itemFileGenerators = new ConcurrentHashMap<>();
private final Map<String, ThingFileGenerator> thingFileGenerators = new ConcurrentHashMap<>();
private final Map<String, SitemapFileGenerator> sitemapFileGenerators = new ConcurrentHashMap<>();

@Activate
public FileFormatResource(//
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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<String> 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<String> sitemapNames;
Map<String, SitemapProvider> 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<Sitemap> sitemaps = sitemapNames.stream().sorted().map(name -> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid sorting the names so that that order provided as input is kept.

In case no input is provided, the sitemaps are already sorted by name at line 378.

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"
*/
Expand Down Expand Up @@ -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<Thing> getThingsOrDiscoveryResult(List<String> thingUIDs) {
return thingUIDs.stream().distinct().map(uid -> {
ThingUID thingUID = new ThingUID(uid);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
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;
Expand Down Expand Up @@ -614,7 +615,7 @@ 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().getElements().isEmpty();
bean.labelcolor = convertItemValueColor(itemUIRegistry.getLabelColor(widget), itemState);
bean.valuecolor = convertItemValueColor(itemUIRegistry.getValueColor(widget), itemState);
bean.iconcolor = convertItemValueColor(itemUIRegistry.getIconColor(widget), itemState);
Expand Down Expand Up @@ -643,7 +644,7 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null
}
}
if (widget instanceof Switch switchWidget) {
for (Mapping mapping : switchWidget.getMappings()) {
for (Mapping mapping : switchWidget.getMappings().getElements()) {

Copilot AI Aug 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential NullPointerException: getMappings() could return null, but there's no null check before calling getElements(). This should be guarded with a null check like other similar patterns in the codebase.

Copilot uses AI. Check for mistakes.
MappingDTO mappingBean = new MappingDTO();
mappingBean.command = mapping.getCmd();
mappingBean.releaseCommand = mapping.getReleaseCmd();
Expand All @@ -653,7 +654,7 @@ private PageDTO createPageBean(String sitemapName, @Nullable String title, @Null
}
}
if (widget instanceof Selection selectionWidget) {
for (Mapping mapping : selectionWidget.getMappings()) {
for (Mapping mapping : selectionWidget.getMappings().getElements()) {

Copilot AI Aug 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential NullPointerException: getMappings() could return null, but there's no null check before calling getElements(). This should be guarded with a null check like other similar patterns in the codebase.

Copilot uses AI. Check for mistakes.
MappingDTO mappingBean = new MappingDTO();
mappingBean.command = mapping.getCmd();
mappingBean.label = mapping.getLabel();
Expand Down Expand Up @@ -714,19 +715,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().getElements().isEmpty()) {
bean.icon = null;
bean.staticIcon = null;
}
Expand Down Expand Up @@ -867,12 +872,12 @@ private Set<GenericItem> getAllItems(List<Widget> widgets) {
items.addAll(getAllItems(grid.getChildren()));
}
// Consider items involved in any icon condition
items.addAll(getItemsInIconCond(widget.getIconRules()));
items.addAll(getItemsInIconCond(widget.getIconRules().getElements()));

Copilot AI Aug 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential NullPointerException: getIconRules() could return null, but there's no null check before calling getElements(). This should be guarded with null checks like the pattern used in WidgetsChangeListener.

Suggested change
items.addAll(getItemsInIconCond(widget.getIconRules().getElements()));
if (widget.getIconRules() != null) {
items.addAll(getItemsInIconCond(widget.getIconRules().getElements()));
} else {
items.addAll(getItemsInIconCond(List.of()));
}

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed

// Consider items involved in any visibility, labelcolor, valuecolor and iconcolor condition
items.addAll(getItemsInVisibilityCond(widget.getVisibility()));
items.addAll(getItemsInColorCond(widget.getLabelColor()));
items.addAll(getItemsInColorCond(widget.getValueColor()));
items.addAll(getItemsInColorCond(widget.getIconColor()));
items.addAll(getItemsInVisibilityCond(widget.getVisibility().getElements()));

Copilot AI Aug 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential NullPointerException: getVisibility() could return null, but there's no null check before calling getElements(). This should be guarded with null checks like the pattern used in WidgetsChangeListener.

Suggested change
items.addAll(getItemsInVisibilityCond(widget.getVisibility().getElements()));
if (widget.getVisibility() != null) {
items.addAll(getItemsInVisibilityCond(widget.getVisibility().getElements()));
}

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed

items.addAll(getItemsInColorCond(widget.getLabelColor().getElements()));

Copilot AI Aug 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential NullPointerException: getLabelColor() could return null, but there's no null check before calling getElements(). This should be guarded with null checks like the pattern used in WidgetsChangeListener.

Suggested change
items.addAll(getItemsInColorCond(widget.getLabelColor().getElements()));
if (widget.getLabelColor() != null) {
items.addAll(getItemsInColorCond(widget.getLabelColor().getElements()));
}

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed

items.addAll(getItemsInColorCond(widget.getValueColor().getElements()));

Copilot AI Aug 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential NullPointerException: getValueColor() could return null, but there's no null check before calling getElements(). This should be guarded with null checks like the pattern used in WidgetsChangeListener.

Suggested change
items.addAll(getItemsInColorCond(widget.getValueColor().getElements()));
items.addAll(getItemsInColorCond(widget.getValueColor() != null ? widget.getValueColor().getElements() : org.eclipse.emf.common.util.ECollections.emptyEList()));

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed

items.addAll(getItemsInColorCond(widget.getIconColor().getElements()));

Copilot AI Aug 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential NullPointerException: getIconColor() could return null, but there's no null check before calling getElements(). This should be guarded with null checks like the pattern used in WidgetsChangeListener.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed

}
return items;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -135,24 +138,39 @@ private Set<Item> getAllItems(EList<Widget> 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());
}
}
}
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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<Condition> conditions, String name) {
Expand Down
Loading
Loading