[REST] New REST APIs to generate DSL syntax for items and things (#4569)

* [REST] New REST APIs to generate DSL syntax for items and things

Related to #4509

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
pull/4629/head
lolodomo 2025-03-02 11:11:01 +01:00 committed by GitHub
parent e16b52d601
commit 67303fa5f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 1445 additions and 24 deletions

View File

@ -0,0 +1,421 @@
/*
* 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.io.rest.core.internal.fileformat;
import static org.openhab.core.config.discovery.inbox.InboxPredicates.forThingUID;
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.auth.Role;
import org.openhab.core.config.core.ConfigDescription;
import org.openhab.core.config.core.ConfigDescriptionParameter;
import org.openhab.core.config.core.ConfigDescriptionRegistry;
import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.inbox.Inbox;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.items.Metadata;
import org.openhab.core.items.MetadataKey;
import org.openhab.core.items.MetadataRegistry;
import org.openhab.core.items.fileconverter.ItemFileGenerator;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingFactory;
import org.openhab.core.thing.fileconverter.ThingFileGenerator;
import org.openhab.core.thing.link.ItemChannelLink;
import org.openhab.core.thing.link.ItemChannelLinkRegistry;
import org.openhab.core.thing.type.ThingType;
import org.openhab.core.thing.type.ThingTypeRegistry;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsName;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
/**
* This class acts as a REST resource and provides different methods to generate file format
* for existing items and things.
*
* This resource is registered with the Jersey servlet.
*
* @author Laurent Garnier - Initial contribution
*/
@Component
@JaxrsResource
@JaxrsName(FileFormatResource.PATH_FILE_FORMAT)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(FileFormatResource.PATH_FILE_FORMAT)
@Tag(name = FileFormatResource.PATH_FILE_FORMAT)
@NonNullByDefault
public class FileFormatResource implements RESTResource {
/** The URI path to this resource */
public static final String PATH_FILE_FORMAT = "file-format";
private final Logger logger = LoggerFactory.getLogger(FileFormatResource.class);
private final ItemRegistry itemRegistry;
private final MetadataRegistry metadataRegistry;
private final ItemChannelLinkRegistry itemChannelLinkRegistry;
private final ThingRegistry thingRegistry;
private final Inbox inbox;
private final ThingTypeRegistry thingTypeRegistry;
private final ConfigDescriptionRegistry configDescRegistry;
private final Map<String, ItemFileGenerator> itemFileGenerators = new ConcurrentHashMap<>();
private final Map<String, ThingFileGenerator> thingFileGenerators = new ConcurrentHashMap<>();
@Activate
public FileFormatResource(//
final @Reference ItemRegistry itemRegistry, //
final @Reference MetadataRegistry metadataRegistry,
final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry,
final @Reference ThingRegistry thingRegistry, //
final @Reference Inbox inbox, //
final @Reference ThingTypeRegistry thingTypeRegistry, //
final @Reference ConfigDescriptionRegistry configDescRegistry) {
this.itemRegistry = itemRegistry;
this.metadataRegistry = metadataRegistry;
this.itemChannelLinkRegistry = itemChannelLinkRegistry;
this.thingRegistry = thingRegistry;
this.inbox = inbox;
this.thingTypeRegistry = thingTypeRegistry;
this.configDescRegistry = configDescRegistry;
}
@Deactivate
void deactivate() {
}
@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE)
protected void addItemFileGenerator(ItemFileGenerator itemFileGenerator) {
itemFileGenerators.put(itemFileGenerator.getFileFormatGenerator(), itemFileGenerator);
}
protected void removeItemFileGenerator(ItemFileGenerator itemFileGenerator) {
itemFileGenerators.remove(itemFileGenerator.getFileFormatGenerator());
}
@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE)
protected void addThingFileGenerator(ThingFileGenerator thingFileGenerator) {
thingFileGenerators.put(thingFileGenerator.getFileFormatGenerator(), thingFileGenerator);
}
protected void removeThingFileGenerator(ThingFileGenerator thingFileGenerator) {
thingFileGenerators.remove(thingFileGenerator.getFileFormatGenerator());
}
@GET
@RolesAllowed({ Role.ADMIN })
@Path("/items")
@Produces("text/vnd.openhab.dsl.item")
@Operation(operationId = "createFileFormatForAllItems", summary = "Create file format for all existing items in registry.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "text/vnd.openhab.dsl.item", schema = @Schema(example = "Group Group1 \"Label\"\nGroup:Switch:OR(ON,OFF) Group2 \"Label\"\nSwitch MyItem \"Label\" <icon> (Group1, Group2) [Tag1, Tag2] { channel=\"binding:type:id:channelid\", namespace=\"my value\" [param=\"my param value\"] }"))),
@ApiResponse(responseCode = "415", description = "Unsupported media type.") })
public Response createFileFormatForAllItems(final @Context HttpHeaders httpHeaders,
@DefaultValue("true") @QueryParam("hideDefaultParameters") @Parameter(description = "hide the configuration parameters having the default value") boolean hideDefaultParameters) {
String acceptHeader = httpHeaders.getHeaderString(HttpHeaders.ACCEPT);
String format = "text/vnd.openhab.dsl.item".equals(acceptHeader) ? "DSL" : null;
ItemFileGenerator generator = format == null ? null : itemFileGenerators.get(format);
if (generator == null) {
return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE)
.entity("Unsupported media type '" + acceptHeader + "'!").build();
}
Collection<Item> items = itemRegistry.getAll();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
generator.generateFileFormat(outputStream, sortItems(items), getMetadata(items), hideDefaultParameters);
return Response.ok(new String(outputStream.toByteArray())).build();
}
@GET
@RolesAllowed({ Role.ADMIN })
@Path("/items/{itemname: [a-zA-Z_0-9]+}")
@Produces("text/vnd.openhab.dsl.item")
@Operation(operationId = "createFileFormatForItem", summary = "Create file format for an existing item in registry.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "text/vnd.openhab.dsl.item", schema = @Schema(example = "Number MyItem \"Label\" <icon> (Group1, Group2) [Tag1, Tag2] { channel=\"binding:type:id:channelid\", namespace=\"my value\" [param=\"my param value\"] }"))),
@ApiResponse(responseCode = "404", description = "Item not found in registry."),
@ApiResponse(responseCode = "415", description = "Unsupported media type.") })
public Response createFileFormatForItem(final @Context HttpHeaders httpHeaders,
@DefaultValue("true") @QueryParam("hideDefaultParameters") @Parameter(description = "hide the configuration parameters having the default value") boolean hideDefaultParameters,
@PathParam("itemname") @Parameter(description = "item name") String itemname) {
String acceptHeader = httpHeaders.getHeaderString(HttpHeaders.ACCEPT);
String format = "text/vnd.openhab.dsl.item".equals(acceptHeader) ? "DSL" : null;
ItemFileGenerator generator = format == null ? null : itemFileGenerators.get(format);
if (generator == null) {
return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE)
.entity("Unsupported media type '" + acceptHeader + "'!").build();
}
Item item = itemRegistry.get(itemname);
if (item == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Item with name '" + itemname + "' not found in the items registry!").build();
}
List<Item> items = List.of(item);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
generator.generateFileFormat(outputStream, items, getMetadata(items), hideDefaultParameters);
return Response.ok(new String(outputStream.toByteArray())).build();
}
@GET
@RolesAllowed({ Role.ADMIN })
@Path("/things")
@Produces("text/vnd.openhab.dsl.thing")
@Operation(operationId = "createFileFormatForAllThings", summary = "Create file format for all existing things in registry.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "text/vnd.openhab.dsl.thing", schema = @Schema(example = "Bridge binding:typeBridge:idBridge \"Label\" @ \"Location\" [stringParam=\"my value\"] {\n Thing type id \"Label\" @ \"Location\" [booleanParam=true, decimalParam=2.5]\n}"))),
@ApiResponse(responseCode = "415", description = "Unsupported media type.") })
public Response createFileFormatForAllThings(final @Context HttpHeaders httpHeaders,
@DefaultValue("true") @QueryParam("hideDefaultParameters") @Parameter(description = "hide the configuration parameters having the default value") boolean hideDefaultParameters) {
String acceptHeader = httpHeaders.getHeaderString(HttpHeaders.ACCEPT);
String format = "text/vnd.openhab.dsl.thing".equals(acceptHeader) ? "DSL" : null;
ThingFileGenerator generator = format == null ? null : thingFileGenerators.get(format);
if (generator == null) {
return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE)
.entity("Unsupported media type '" + acceptHeader + "'!").build();
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
generator.generateFileFormat(outputStream, sortThings(thingRegistry.getAll()), hideDefaultParameters);
return Response.ok(new String(outputStream.toByteArray())).build();
}
@GET
@RolesAllowed({ Role.ADMIN })
@Path("/things/{thingUID}")
@Produces("text/vnd.openhab.dsl.thing")
@Operation(operationId = "createFileFormatForThing", summary = "Create file format for an existing thing in things or discovery registry.", security = {
@SecurityRequirement(name = "oauth2", scopes = { "admin" }) }, responses = {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "text/vnd.openhab.dsl.thing", schema = @Schema(example = "Thing binding:type:idBridge:id \"Label\" @ \"Location\" (binding:typeBridge:idBridge) [stringParam=\"my value\", booleanParam=true, decimalParam=2.5]"))),
@ApiResponse(responseCode = "404", description = "Thing not found in things or discovery registry or thing type not found."),
@ApiResponse(responseCode = "415", description = "Unsupported media type.") })
public Response createFileFormatForThing(final @Context HttpHeaders httpHeaders,
@DefaultValue("true") @QueryParam("hideDefaultParameters") @Parameter(description = "hide the configuration parameters having the default value") boolean hideDefaultParameters,
@PathParam("thingUID") @Parameter(description = "thingUID") String thingUID) {
String acceptHeader = httpHeaders.getHeaderString(HttpHeaders.ACCEPT);
String format = "text/vnd.openhab.dsl.thing".equals(acceptHeader) ? "DSL" : null;
ThingFileGenerator generator = format == null ? null : thingFileGenerators.get(format);
if (generator == null) {
return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE)
.entity("Unsupported media type '" + acceptHeader + "'!").build();
}
ThingUID aThingUID = new ThingUID(thingUID);
Thing thing = thingRegistry.get(aThingUID);
if (thing == null) {
List<DiscoveryResult> results = inbox.getAll().stream().filter(forThingUID(new ThingUID(thingUID)))
.toList();
if (results.isEmpty()) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Thing with UID '" + thingUID + "' not found in the things or discovery registry!")
.build();
}
DiscoveryResult result = results.get(0);
ThingType thingType = thingTypeRegistry.getThingType(result.getThingTypeUID());
if (thingType == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Thing type with UID '" + result.getThingTypeUID() + "' does not exist!").build();
}
thing = simulateThing(result, thingType);
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
generator.generateFileFormat(outputStream, List.of(thing), hideDefaultParameters);
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"
*/
private Collection<Metadata> getMetadata(Collection<Item> items) {
Collection<Metadata> metadata = new ArrayList<>();
for (Item item : items) {
String itemName = item.getName();
metadataRegistry.getAll().stream().filter(md -> md.getUID().getItemName().equals(itemName)).forEach(md -> {
metadata.add(md);
});
itemChannelLinkRegistry.getLinks(itemName).forEach(link -> {
MetadataKey key = new MetadataKey("channel", itemName);
Metadata md = new Metadata(key, link.getLinkedUID().getAsString(),
link.getConfiguration().getProperties());
metadata.add(md);
});
}
return metadata;
}
/*
* Sort the items in such a way:
* - group items are before non group items
* - group items are sorted to have as much as possible ancestors before their children
* - items not linked to a channel are before items linked to a channel
* - items linked to a channel are grouped by thing UID
* - items linked to the same thing UID are sorted by item name
*/
private List<Item> sortItems(Collection<Item> items) {
List<Item> groups = items.stream().filter(item -> item instanceof GroupItem).sorted((item1, item2) -> {
return item1.getName().compareTo(item2.getName());
}).collect(Collectors.toList());
List<Item> topGroups = groups.stream().filter(group -> group.getGroupNames().isEmpty())
.sorted((group1, group2) -> {
return group1.getName().compareTo(group2.getName());
}).collect(Collectors.toList());
List<Item> groupTree = new ArrayList<>();
for (Item group : topGroups) {
fillGroupTree(groupTree, group);
}
if (groupTree.size() != groups.size()) {
logger.warn("Something want wrong when sorting groups; failback to a sort by name.");
groupTree = groups;
}
List<Item> nonGroups = items.stream().filter(item -> !(item instanceof GroupItem)).sorted((item1, item2) -> {
Set<ItemChannelLink> channelLinks1 = itemChannelLinkRegistry.getLinks(item1.getName());
String thingUID1 = channelLinks1.isEmpty() ? null
: channelLinks1.iterator().next().getLinkedUID().getThingUID().getAsString();
Set<ItemChannelLink> channelLinks2 = itemChannelLinkRegistry.getLinks(item2.getName());
String thingUID2 = channelLinks2.isEmpty() ? null
: channelLinks2.iterator().next().getLinkedUID().getThingUID().getAsString();
if (thingUID1 == null && thingUID2 != null) {
return -1;
} else if (thingUID1 != null && thingUID2 == null) {
return 1;
} else if (thingUID1 != null && thingUID2 != null && !thingUID1.equals(thingUID2)) {
return thingUID1.compareTo(thingUID2);
}
return item1.getName().compareTo(item2.getName());
}).collect(Collectors.toList());
return Stream.of(groupTree, nonGroups).flatMap(List::stream).collect(Collectors.toList());
}
private void fillGroupTree(List<Item> groups, Item item) {
if (item instanceof GroupItem group && !groups.contains(group)) {
groups.add(group);
List<Item> members = group.getMembers().stream().sorted((member1, member2) -> {
return member1.getName().compareTo(member2.getName());
}).collect(Collectors.toList());
for (Item member : members) {
fillGroupTree(groups, member);
}
}
}
/*
* Sort the things in such a way:
* - things are grouped by binding, sorted by natural order of binding name
* - all things of a binding are sorted to follow the tree, that is bridge thing is before its sub-things
* - all things of a binding at a certain tree depth are sorted by thing UID
*/
private List<Thing> sortThings(Collection<Thing> things) {
List<Thing> thingTree = new ArrayList<>();
Set<String> bindings = things.stream().map(thing -> thing.getUID().getBindingId()).collect(Collectors.toSet());
for (String binding : bindings.stream().sorted().collect(Collectors.toList())) {
List<Thing> topThings = things.stream()
.filter(thing -> thing.getUID().getBindingId().equals(binding) && thing.getBridgeUID() == null)
.sorted((thing1, thing2) -> {
return thing1.getUID().getAsString().compareTo(thing2.getUID().getAsString());
}).collect(Collectors.toList());
for (Thing thing : topThings) {
fillThingTree(thingTree, thing);
}
}
return thingTree;
}
private void fillThingTree(List<Thing> things, Thing thing) {
if (!things.contains(thing)) {
things.add(thing);
if (thing instanceof Bridge bridge) {
List<Thing> subThings = bridge.getThings().stream().sorted((thing1, thing2) -> {
return thing1.getUID().getAsString().compareTo(thing2.getUID().getAsString());
}).collect(Collectors.toList());
for (Thing subThing : subThings) {
fillThingTree(things, subThing);
}
}
}
}
/*
* Create a thing from a discovery result without inserting it in the thing registry
*/
private Thing simulateThing(DiscoveryResult result, ThingType thingType) {
Map<String, Object> configParams = new HashMap<>();
List<ConfigDescriptionParameter> configDescriptionParameters = List.of();
URI descURI = thingType.getConfigDescriptionURI();
if (descURI != null) {
ConfigDescription desc = configDescRegistry.getConfigDescription(descURI);
if (desc != null) {
configDescriptionParameters = desc.getParameters();
}
}
for (ConfigDescriptionParameter param : configDescriptionParameters) {
Object value = result.getProperties().get(param.getName());
Object normalizedValue = value != null ? ConfigUtil.normalizeType(value, param) : null;
if (normalizedValue != null) {
configParams.put(param.getName(), normalizedValue);
}
}
Configuration config = new Configuration(configParams);
return ThingFactory.createThing(thingType, result.getThingUID(), config, result.getBridgeUID(),
configDescRegistry);
}
}

View File

@ -13,6 +13,7 @@
package org.openhab.core.model.core;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Set;
import org.eclipse.emf.ecore.EObject;
@ -26,6 +27,7 @@ import org.eclipse.jdt.annotation.Nullable;
* come from.
*
* @author Kai Kreuzer - Initial contribution
* @author Laurent Garnier - Added method generateSyntaxFromModel
*/
@NonNullByDefault
public interface ModelRepository {
@ -92,4 +94,14 @@ public interface ModelRepository {
* @param listener the listener to remove
*/
void removeModelRepositoryChangeListener(ModelRepositoryChangeListener listener);
/**
* Generate the syntax from a provided model content.
*
* @param out the output stream to write the generated syntax to
* @param modelType the model type
* @param modelContent the content of the model
* @return the corresponding syntax
*/
void generateSyntaxFromModel(OutputStream out, String modelType, EObject modelContent);
}

View File

@ -15,6 +15,7 @@ package org.openhab.core.model.core.internal;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.ArrayList;
@ -50,6 +51,7 @@ import org.slf4j.LoggerFactory;
* @author Kai Kreuzer - Initial contribution
* @author Oliver Libutzki - Added reloadAllModelsOfType method
* @author Simon Kaufmann - added validation of models before loading them
* @author Laurent Garnier - Added method generateSyntaxFromModel
*/
@Component(immediate = true)
@NonNullByDefault
@ -64,6 +66,8 @@ public class ModelRepositoryImpl implements ModelRepository {
private final SafeEMF safeEmf;
private int counter;
@Activate
public ModelRepositoryImpl(final @Reference SafeEMF safeEmf) {
this.safeEmf = safeEmf;
@ -171,7 +175,8 @@ public class ModelRepositoryImpl implements ModelRepository {
return resourceListCopy.stream()
.filter(input -> input.getURI().lastSegment().contains(".") && input.isLoaded()
&& modelType.equalsIgnoreCase(input.getURI().fileExtension()))
&& modelType.equalsIgnoreCase(input.getURI().fileExtension())
&& !input.getURI().lastSegment().startsWith("tmp_"))
.map(from -> from.getURI().path()).toList();
}
}
@ -227,6 +232,23 @@ public class ModelRepositoryImpl implements ModelRepository {
listeners.remove(listener);
}
@Override
public void generateSyntaxFromModel(OutputStream out, String modelType, EObject modelContent) {
String result = "";
synchronized (resourceSet) {
String name = "tmp_generated_syntax_%d.%s".formatted(++counter, modelType);
Resource resource = resourceSet.createResource(URI.createURI(name));
try {
resource.getContents().add(modelContent);
resource.save(out, Map.of(XtextResource.OPTION_ENCODING, StandardCharsets.UTF_8.name()));
} catch (IOException e) {
logger.warn("Exception when saving the model {}", resource.getURI().lastSegment());
} finally {
resourceSet.getResources().remove(resource);
}
}
}
private @Nullable Resource getResource(String name) {
return resourceSet.getResource(URI.createURI(name), false);
}

View File

@ -21,6 +21,7 @@ Import-Package: javax.measure,\
org.openhab.core.i18n,\
org.openhab.core.items,\
org.openhab.core.items.dto,\
org.openhab.core.items.fileconverter,\
org.openhab.core.library.items,\
org.openhab.core.library.types,\
org.openhab.core.thing.util,\

View File

@ -18,19 +18,24 @@ org.openhab.core.model
import com.google.inject.Binder
import com.google.inject.name.Names
import org.openhab.core.model.formatting.ItemsFormatter
import org.openhab.core.model.internal.valueconverter.ItemValueConverters
import org.eclipse.xtext.conversion.IValueConverterService
import org.eclipse.xtext.formatting.IFormatter
import org.eclipse.xtext.linking.lazy.LazyURIEncoder
/**
* Use this class to register components to be used at runtime / without the Equinox extension registry.
*/
class ItemsRuntimeModule extends AbstractItemsRuntimeModule {
override Class<? extends IValueConverterService> bindIValueConverterService() {
return ItemValueConverters
}
override Class<? extends IFormatter> bindIFormatter() {
return ItemsFormatter
}
override void configureUseIndexFragmentsForLazyLinking(Binder binder) {
binder.bind(Boolean.TYPE).annotatedWith(Names.named(LazyURIEncoder.USE_INDEXED_FRAGMENTS_BINDING)).toInstance(
Boolean.FALSE)

View File

@ -26,17 +26,17 @@ class ItemsFormatter extends AbstractDeclarativeFormatter {
override protected void configureFormatting(FormattingConfig c) {
c.setLinewrap(1, 1, 2).before(modelGroupItemRule)
c.setLinewrap(1, 1, 2).before(modelItemTypeRule)
c.setLinewrap(1, 1, 2).before(modelNormalItemRule)
c.setNoSpace().withinKeywordPairs("<", ">")
c.setNoSpace().withinKeywordPairs("(", ")")
c.setNoSpace().withinKeywordPairs("[", "]")
c.setIndentationIncrement.after(modelItemTypeRule)
c.setIndentationDecrement.before(modelItemTypeRule)
c.setIndentationIncrement.after(modelGroupItemRule)
c.setIndentationDecrement.before(modelGroupItemRule)
c.setNoSpace().around(":", "=")
c.setNoSpace().before(",")
c.autoLinewrap = 400
c.autoLinewrap = 160
c.setLinewrap(0, 1, 2).before(SL_COMMENTRule)
c.setLinewrap(0, 1, 2).before(ML_COMMENTRule)
c.setLinewrap(0, 1, 1).after(ML_COMMENTRule)
@ -48,4 +48,16 @@ class ItemsFormatter extends AbstractDeclarativeFormatter {
locator.before(pair.second)
}
}
def around(FormattingConfig.ElementLocator locator, String ... listKW) {
for (keyword : findKeywords(listKW)) {
locator.around(keyword)
}
}
def before(FormattingConfig.ElementLocator locator, String ... listKW) {
for (keyword : findKeywords(listKW)) {
locator.before(keyword)
}
}
}

View File

@ -0,0 +1,250 @@
/*
* 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.item.internal.fileconverter;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.core.ConfigDescription;
import org.openhab.core.config.core.ConfigDescriptionParameter;
import org.openhab.core.config.core.ConfigDescriptionRegistry;
import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.items.GroupFunction;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.Metadata;
import org.openhab.core.items.fileconverter.AbstractItemFileGenerator;
import org.openhab.core.items.fileconverter.ItemFileGenerator;
import org.openhab.core.model.core.ModelRepository;
import org.openhab.core.model.items.ItemModel;
import org.openhab.core.model.items.ItemsFactory;
import org.openhab.core.model.items.ModelBinding;
import org.openhab.core.model.items.ModelGroupFunction;
import org.openhab.core.model.items.ModelGroupItem;
import org.openhab.core.model.items.ModelItem;
import org.openhab.core.model.items.ModelProperty;
import org.openhab.core.types.State;
import org.openhab.core.types.StateDescription;
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 DslItemFileConverter} is the DSL file converter for {@link Item} object
* with the capabilities of parsing and generating file.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
@Component(immediate = true, service = ItemFileGenerator.class)
public class DslItemFileConverter extends AbstractItemFileGenerator {
private final Logger logger = LoggerFactory.getLogger(DslItemFileConverter.class);
private final ModelRepository modelRepository;
private final ConfigDescriptionRegistry configDescriptionRegistry;
@Activate
public DslItemFileConverter(final @Reference ModelRepository modelRepository,
final @Reference ConfigDescriptionRegistry configDescriptionRegistry) {
this.modelRepository = modelRepository;
this.configDescriptionRegistry = configDescriptionRegistry;
}
@Override
public String getFileFormatGenerator() {
return "DSL";
}
@Override
public synchronized void generateFileFormat(OutputStream out, List<Item> items, Collection<Metadata> metadata,
boolean hideDefaultParameters) {
ItemModel model = ItemsFactory.eINSTANCE.createItemModel();
for (Item item : items) {
model.getItems().add(buildModelItem(item, getChannelLinks(metadata, item.getName()),
getMetadata(metadata, item.getName()), hideDefaultParameters));
}
modelRepository.generateSyntaxFromModel(out, "items", model);
}
private ModelItem buildModelItem(Item item, List<Metadata> channelLinks, List<Metadata> metadata,
boolean hideDefaultParameters) {
ModelItem model;
if (item instanceof GroupItem groupItem) {
ModelGroupItem modelGroup = ItemsFactory.eINSTANCE.createModelGroupItem();
model = modelGroup;
Item baseItem = groupItem.getBaseItem();
if (baseItem != null) {
modelGroup.setType(baseItem.getType());
GroupFunction function = groupItem.getFunction();
if (function != null) {
ModelGroupFunction modelFunction = ModelGroupFunction
.getByName(function.getClass().getSimpleName().toUpperCase());
modelGroup.setFunction(modelFunction);
State[] parameters = function.getParameters();
for (int i = 0; i < parameters.length; i++) {
modelGroup.getArgs().add(parameters[i].toString());
}
}
}
} else {
model = ItemsFactory.eINSTANCE.createModelNormalItem();
model.setType(item.getType());
}
model.setName(item.getName());
String label = item.getLabel();
boolean patternInjected = false;
String defaultPattern = getDefaultStatePattern(item);
if (label != null && !label.isEmpty()) {
StateDescription stateDescr = item.getStateDescription();
String statePattern = stateDescr == null ? null : stateDescr.getPattern();
String patterToInject = statePattern != null && !statePattern.equals(defaultPattern) ? statePattern : null;
if (patterToInject != null) {
// Inject the pattern in the label
patternInjected = true;
model.setLabel("%s [%s]".formatted(label, patterToInject));
} else {
model.setLabel(label);
}
}
String category = item.getCategory();
if (category != null && !category.isEmpty()) {
model.setIcon(category);
}
for (String group : item.getGroupNames()) {
model.getGroups().add(group);
}
for (String tag : item.getTags().stream().sorted().collect(Collectors.toList())) {
model.getTags().add(tag);
}
for (Metadata md : channelLinks) {
String namespace = md.getUID().getNamespace();
ModelBinding binding = ItemsFactory.eINSTANCE.createModelBinding();
binding.setType(namespace);
binding.setConfiguration(md.getValue());
for (ConfigParameter param : getConfigurationParameters(md, hideDefaultParameters)) {
ModelProperty property = buildModelProperty(param.name(), param.value());
if (property != null) {
binding.getProperties().add(property);
}
}
model.getBindings().add(binding);
}
for (Metadata md : metadata) {
String namespace = md.getUID().getNamespace();
ModelBinding binding = ItemsFactory.eINSTANCE.createModelBinding();
binding.setType(namespace);
binding.setConfiguration(md.getValue());
String statePattern = null;
for (ConfigParameter param : getConfigurationParameters(md)) {
ModelProperty property = buildModelProperty(param.name(), param.value());
if (property != null) {
binding.getProperties().add(property);
}
if ("stateDescription".equals(namespace) && "pattern".equals(param.name())) {
statePattern = param.value().toString();
}
}
// Ignore state description in case it contains only a state pattern and state pattern was injected
// in the item label or is the default pattern
if (!(statePattern != null && binding.getProperties().size() == 1
&& (patternInjected || statePattern.equals(defaultPattern)))) {
model.getBindings().add(binding);
}
}
return model;
}
private @Nullable ModelProperty buildModelProperty(String key, Object value) {
ModelProperty property = ItemsFactory.eINSTANCE.createModelProperty();
property.setKey(key);
if (value instanceof List<?> list) {
if (!list.isEmpty()) {
property.getValue().addAll(list);
} else {
property = null;
}
} else {
property.getValue().add(value);
}
return property;
}
/*
* Get the list of configuration parameters for a channel link.
*
* If a profile is set and a configuration description is found for this profile, the parameters are provided
* in the same order as in this configuration description, and any parameter having the default value is ignored.
* If no profile is set, the parameters are provided sorted by natural order of their names.
*/
private List<ConfigParameter> getConfigurationParameters(Metadata metadata, boolean hideDefaultParameters) {
List<ConfigParameter> parameters = new ArrayList<>();
Set<String> handledNames = new HashSet<>();
Map<String, Object> configParameters = metadata.getConfiguration();
Object profile = configParameters.get("profile");
List<ConfigDescriptionParameter> configDescriptionParameter = List.of();
if (profile instanceof String profileStr) {
parameters.add(new ConfigParameter("profile", profileStr));
handledNames.add("profile");
try {
ConfigDescription configDesc = configDescriptionRegistry
.getConfigDescription(new URI("profile:" + profileStr));
if (configDesc != null) {
configDescriptionParameter = configDesc.getParameters();
}
} catch (URISyntaxException e) {
// Ignored; in practice this will never be thrown
}
}
for (ConfigDescriptionParameter param : configDescriptionParameter) {
String paramName = param.getName();
if (handledNames.contains(paramName)) {
continue;
}
Object value = configParameters.get(paramName);
Object defaultValue = ConfigUtil.getDefaultValueAsCorrectType(param);
if (value != null && (!hideDefaultParameters || !value.equals(defaultValue))) {
parameters.add(new ConfigParameter(paramName, value));
}
handledNames.add(paramName);
}
for (String paramName : configParameters.keySet().stream().sorted().collect(Collectors.toList())) {
if (handledNames.contains(paramName)) {
continue;
}
Object value = configParameters.get(paramName);
if (value != null) {
parameters.add(new ConfigParameter(paramName, value));
}
handledNames.add(paramName);
}
return parameters;
}
}

View File

@ -21,9 +21,11 @@ Import-Package: org.apache.log4j,\
org.openhab.core.thing,\
org.openhab.core.thing.binding,\
org.openhab.core.thing.binding.builder,\
org.openhab.core.thing.fileconverter,\
org.openhab.core.thing.link,\
org.openhab.core.thing.type,\
org.openhab.core.thing.util,\
org.openhab.core.types,\
org.openhab.core.types.util,\
org.openhab.core.util,\
org.openhab.core.model.core,\

View File

@ -26,9 +26,9 @@ ModelBridge returns ModelThing:
properties+=ModelProperty? (',' properties+=ModelProperty)*
']')?
('{'
('Things:')?
(thingsHeader?='Things:')?
things+=(ModelThing|ModelBridge)*
('Channels:')?
(channelsHeader?='Channels:')?
channels+=ModelChannel*
'}')?
@ -43,7 +43,7 @@ ModelThing:
properties+=ModelProperty? (',' properties+=ModelProperty)*
']')?
('{'
('Channels:')?
(channelsHeader?='Channels:')?
channels+=ModelChannel*
'}')?
;

View File

@ -12,11 +12,13 @@
*/
package org.openhab.core.model.thing
import org.openhab.core.model.thing.valueconverter.ThingValueConverters
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.thing.formatting.ThingFormatter
import org.openhab.core.model.thing.valueconverter.ThingValueConverters
import org.eclipse.xtext.conversion.IValueConverterService
import org.eclipse.xtext.formatting.IFormatter
import org.eclipse.xtext.linking.lazy.LazyURIEncoder
/**
* Use this class to register components to be used at runtime / without the Equinox extension registry.
@ -30,6 +32,10 @@ import com.google.inject.name.Names
return org.openhab.core.model.thing.serializer.ThingSyntacticSequencerExtension
}
override Class<? extends IFormatter> bindIFormatter() {
return ThingFormatter
}
override void configureUseIndexFragmentsForLazyLinking(Binder binder) {
binder.bind(Boolean.TYPE).annotatedWith(Names.named(LazyURIEncoder.USE_INDEXED_FRAGMENTS_BINDING)).toInstance(
Boolean.FALSE)

View File

@ -17,8 +17,8 @@ package org.openhab.core.model.thing.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.thing.services.ThingGrammarAccess
import com.google.inject.Inject;
import org.openhab.core.model.thing.services.ThingGrammarAccess
/**
* This class contains custom formatting description.
@ -30,13 +30,53 @@ import org.eclipse.xtext.formatting.impl.FormattingConfig
*/
class ThingFormatter extends AbstractDeclarativeFormatter {
// @Inject extension ThingGrammarAccess
@Inject extension ThingGrammarAccess
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)
c.setLinewrap(1, 1, 2).before("Bridge", "Things:", "Channels:")
c.setLinewrap(1, 1, 2).before(modelThingRule)
c.setLinewrap(1, 1, 2).before(modelChannelRule)
c.setIndentationIncrement.after("{")
c.setIndentationDecrement.before("}")
c.setIndentationIncrement.before(modelChannelRule)
c.setIndentationDecrement.after(modelChannelRule)
c.setLinewrap().before("}")
c.setNoSpace().withinKeywordPairs("(", ")")
c.setNoSpace().withinKeywordPairs("[", "]")
c.setNoSpace().around("=")
c.setNoSpace().before(",")
c.autoLinewrap = 400
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)
}
}
}

View File

@ -0,0 +1,192 @@
/*
* 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.thing.internal.fileconverter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.core.ConfigDescriptionRegistry;
import org.openhab.core.model.core.ModelRepository;
import org.openhab.core.model.thing.thing.ModelBridge;
import org.openhab.core.model.thing.thing.ModelChannel;
import org.openhab.core.model.thing.thing.ModelProperty;
import org.openhab.core.model.thing.thing.ModelThing;
import org.openhab.core.model.thing.thing.ThingFactory;
import org.openhab.core.model.thing.thing.ThingModel;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.fileconverter.AbstractThingFileGenerator;
import org.openhab.core.thing.fileconverter.ThingFileGenerator;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.thing.type.ThingTypeRegistry;
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 DslThingFileConverter} is the DSL file converter for {@link Thing} object
* with the capabilities of parsing and generating file.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
@Component(immediate = true, service = ThingFileGenerator.class)
public class DslThingFileConverter extends AbstractThingFileGenerator {
private final Logger logger = LoggerFactory.getLogger(DslThingFileConverter.class);
private final ModelRepository modelRepository;
@Activate
public DslThingFileConverter(final @Reference ModelRepository modelRepository,
final @Reference ThingTypeRegistry thingTypeRegistry,
final @Reference ChannelTypeRegistry channelTypeRegistry,
final @Reference ConfigDescriptionRegistry configDescRegistry) {
super(thingTypeRegistry, channelTypeRegistry, configDescRegistry);
this.modelRepository = modelRepository;
}
@Override
public String getFileFormatGenerator() {
return "DSL";
}
@Override
public synchronized void generateFileFormat(OutputStream out, List<Thing> things, boolean hideDefaultParameters) {
ThingModel model = ThingFactory.eINSTANCE.createThingModel();
Set<Thing> handledThings = new HashSet<>();
for (Thing thing : things) {
if (handledThings.contains(thing)) {
continue;
}
model.getThings()
.add(buildModelThing(thing, hideDefaultParameters, things.size() > 1, true, things, handledThings));
}
// 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);
String syntax = new String(outputStream.toByteArray()).replaceAll(":\"([a-zA-Z0-9_][a-zA-Z0-9_-]*)\"", ":$1");
try {
out.write(syntax.getBytes());
} catch (IOException e) {
logger.warn("Exception when writing the generated syntax {}", e.getMessage());
}
}
private ModelThing buildModelThing(Thing thing, boolean hideDefaultParameters, boolean preferPresentationAsTree,
boolean topLevel, List<Thing> onlyThings, Set<Thing> handledThings) {
ModelThing model;
ModelBridge modelBridge;
if (preferPresentationAsTree && thing instanceof Bridge bridge && !bridge.getThings().isEmpty()) {
modelBridge = ThingFactory.eINSTANCE.createModelBridge();
modelBridge.setBridge(true);
model = modelBridge;
} else {
modelBridge = null;
model = ThingFactory.eINSTANCE.createModelThing();
}
if (!preferPresentationAsTree || topLevel) {
model.setId(thing.getUID().getAsString());
ThingUID bridgeUID = thing.getBridgeUID();
if (bridgeUID != null && modelBridge == null) {
model.setBridgeUID(bridgeUID.getAsString());
}
} else {
model.setThingTypeId(thing.getThingTypeUID().getId());
model.setThingId(thing.getUID().getId());
}
if (thing.getLabel() != null) {
model.setLabel(thing.getLabel());
}
if (thing.getLocation() != null) {
model.setLocation(thing.getLocation());
}
for (ConfigParameter param : getConfigurationParameters(thing, hideDefaultParameters)) {
ModelProperty property = buildModelProperty(param.name(), param.value());
if (property != null) {
model.getProperties().add(property);
}
}
if (preferPresentationAsTree && modelBridge != null) {
modelBridge.setThingsHeader(false);
for (Thing child : getChildThings(thing)) {
if (onlyThings.contains(child) && !handledThings.contains(child)) {
modelBridge.getThings()
.add(buildModelThing(child, hideDefaultParameters, true, false, onlyThings, handledThings));
}
}
}
List<Channel> channels = getNonDefaultChannels(thing);
model.setChannelsHeader(!channels.isEmpty());
for (Channel channel : channels) {
model.getChannels().add(buildModelChannel(channel, hideDefaultParameters));
}
handledThings.add(thing);
return model;
}
private ModelChannel buildModelChannel(Channel channel, boolean hideDefaultParameters) {
ModelChannel modelChannel = ThingFactory.eINSTANCE.createModelChannel();
ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
if (channelTypeUID != null) {
modelChannel.setChannelType(channelTypeUID.getId());
} else {
modelChannel.setChannelKind(channel.getKind() == ChannelKind.STATE ? "State" : "Trigger");
modelChannel.setType(channel.getAcceptedItemType());
}
modelChannel.setId(channel.getUID().getId());
if (channel.getLabel() != null) {
modelChannel.setLabel(channel.getLabel());
}
for (ConfigParameter param : getConfigurationParameters(channel, hideDefaultParameters)) {
ModelProperty property = buildModelProperty(param.name(), param.value());
if (property != null) {
modelChannel.getProperties().add(property);
}
}
return modelChannel;
}
private @Nullable ModelProperty buildModelProperty(String key, Object value) {
ModelProperty property = ThingFactory.eINSTANCE.createModelProperty();
property.setKey(key);
if (value instanceof List<?> list) {
if (!list.isEmpty()) {
property.getValue().addAll(list);
} else {
property = null;
}
} else {
property.getValue().add(value);
}
return property;
}
}

View File

@ -0,0 +1,217 @@
/*
* 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.thing.fileconverter;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.core.ConfigDescription;
import org.openhab.core.config.core.ConfigDescriptionParameter;
import org.openhab.core.config.core.ConfigDescriptionRegistry;
import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.thing.type.ThingType;
import org.openhab.core.thing.type.ThingTypeRegistry;
import org.osgi.service.component.annotations.Activate;
/**
* {@link AbstractThingFileGenerator} is the base class for any {@link Thing} file generator.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public abstract class AbstractThingFileGenerator implements ThingFileGenerator {
private final ThingTypeRegistry thingTypeRegistry;
private final ChannelTypeRegistry channelTypeRegistry;
private final ConfigDescriptionRegistry configDescRegistry;
@Activate
public AbstractThingFileGenerator(ThingTypeRegistry thingTypeRegistry, ChannelTypeRegistry channelTypeRegistry,
ConfigDescriptionRegistry configDescRegistry) {
this.thingTypeRegistry = thingTypeRegistry;
this.channelTypeRegistry = channelTypeRegistry;
this.configDescRegistry = configDescRegistry;
}
/**
* {@link ConfigParameter} is a container for any configuration parameter defined by a name and a value.
*/
protected record ConfigParameter(String name, Object value) {
}
/**
* Get the child things of a bridge thing, ordered by UID.
*
* @param thing the thing
* @return the sorted list of child things or an empty list if the thing is not a bridge thing
*/
protected List<Thing> getChildThings(Thing thing) {
if (thing instanceof Bridge bridge) {
return bridge.getThings().stream().sorted((thing1, thing2) -> {
return thing1.getUID().getAsString().compareTo(thing2.getUID().getAsString());
}).collect(Collectors.toList());
}
return List.of();
}
/**
* Get the list of configuration parameters for a thing.
*
* If a configuration description is found for the thing type, the parameters are provided in the same order
* as in this configuration description, and any parameter having the default value is ignored.
* If not, the parameters are provided sorted by natural order of their names.
*
* @param thing the thing
* @param hideDefaultParameters true to hide the configuration parameters having the default value
* @return the sorted list of configuration parameters for the thing
*/
protected List<ConfigParameter> getConfigurationParameters(Thing thing, boolean hideDefaultParameters) {
return getConfigurationParameters(getConfigDescriptionParameters(thing), thing.getConfiguration(),
hideDefaultParameters);
}
/**
* Get the list of configuration parameters for a channel.
*
* If a configuration description is found for the channel type, the parameters are provided in the same order
* as in this configuration description, and any parameter having the default value is ignored.
* If not, the parameters are provided sorted by natural order of their names.
*
* @param thing the channel
* @param hideDefaultParameters true to hide the configuration parameters having the default value
* @return the sorted list of configuration parameters for the channel
*/
protected List<ConfigParameter> getConfigurationParameters(Channel channel, boolean hideDefaultParameters) {
return getConfigurationParameters(getConfigDescriptionParameters(channel), channel.getConfiguration(),
hideDefaultParameters);
}
private List<ConfigParameter> getConfigurationParameters(
List<ConfigDescriptionParameter> configDescriptionParameter, Configuration configParameters,
boolean hideDefaultParameters) {
List<ConfigParameter> parameters = new ArrayList<>();
Set<String> handledNames = new HashSet<>();
for (ConfigDescriptionParameter param : configDescriptionParameter) {
String paramName = param.getName();
if (handledNames.contains(paramName)) {
continue;
}
Object value = configParameters.get(paramName);
Object defaultValue = ConfigUtil.getDefaultValueAsCorrectType(param);
if (value != null && (!hideDefaultParameters || !value.equals(defaultValue))) {
parameters.add(new ConfigParameter(paramName, value));
}
handledNames.add(paramName);
}
for (String paramName : configParameters.keySet().stream().sorted().collect(Collectors.toList())) {
if (handledNames.contains(paramName)) {
continue;
}
Object value = configParameters.get(paramName);
if (value != null) {
parameters.add(new ConfigParameter(paramName, value));
}
handledNames.add(paramName);
}
return parameters;
}
private List<ConfigDescriptionParameter> getConfigDescriptionParameters(Thing thing) {
List<ConfigDescriptionParameter> configParams = null;
ThingType thingType = thingTypeRegistry.getThingType(thing.getThingTypeUID());
if (thingType != null) {
configParams = getConfigDescriptionParameters(thingType.getConfigDescriptionURI());
}
return configParams != null ? configParams : List.of();
}
private List<ConfigDescriptionParameter> getConfigDescriptionParameters(Channel channel) {
List<ConfigDescriptionParameter> configParams = null;
ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
if (channelTypeUID != null) {
ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID);
if (channelType != null) {
configParams = getConfigDescriptionParameters(channelType.getConfigDescriptionURI());
}
}
return configParams != null ? configParams : List.of();
}
private @Nullable List<ConfigDescriptionParameter> getConfigDescriptionParameters(@Nullable URI descURI) {
if (descURI != null) {
ConfigDescription configDesc = configDescRegistry.getConfigDescription(descURI);
if (configDesc != null) {
return configDesc.getParameters();
}
}
return null;
}
/**
* Get non default channels.
* It includes extensible channels and channels with a non default configuration.
*
* Resulting channels are sorted in such a way that channels without channel type are after channels
* with a channel type. Sort is done first on the channel type and then on the channel UID.
*
* @param thing the thing
* @return the sorted list of channels
*/
protected List<Channel> getNonDefaultChannels(Thing thing) {
ThingType thingType = thingTypeRegistry.getThingType(thing.getThingTypeUID());
List<String> ids = thingType != null ? thingType.getExtensibleChannelTypeIds() : List.of();
List<Channel> channels = thing
.getChannels().stream().filter(ch -> ch.getChannelTypeUID() == null
|| ids.contains(ch.getChannelTypeUID().getId()) || channelWithNonDefaultConfig(ch))
.collect(Collectors.toList());
return channels.stream().sorted((ch1, ch2) -> {
ChannelTypeUID typeUID1 = ch1.getChannelTypeUID();
ChannelTypeUID typeUID2 = ch2.getChannelTypeUID();
if (typeUID1 != null && typeUID2 == null) {
return -1;
} else if (typeUID1 == null && typeUID2 != null) {
return 1;
} else if (typeUID1 != null && typeUID2 != null && !typeUID1.equals(typeUID2)) {
return typeUID1.getAsString().compareTo(typeUID2.getAsString());
} else {
return ch1.getUID().getAsString().compareTo(ch2.getUID().getAsString());
}
}).collect(Collectors.toList());
}
private boolean channelWithNonDefaultConfig(Channel channel) {
for (ConfigDescriptionParameter param : getConfigDescriptionParameters(channel)) {
Object value = channel.getConfiguration().get(param.getName());
if (value != null) {
value = ConfigUtil.normalizeType(value, param);
if (!value.equals(ConfigUtil.getDefaultValueAsCorrectType(param))) {
return true;
}
}
}
return false;
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.thing.fileconverter;
import java.io.OutputStream;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.Thing;
/**
* {@link ThingFileGenerator} is the interface to implement by any file generator for {@link Thing} object.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public interface ThingFileGenerator {
/**
* Returns the format of the file.
*
* @return the file format
*/
String getFileFormatGenerator();
/**
* Generate the file format for a sorted list of things.
*
* @param out the output stream to write the generated syntax to
* @param things the things
* @param hideDefaultParameters true to hide the configuration parameters having the default value
*/
void generateFileFormat(OutputStream out, List<Thing> things, boolean hideDefaultParameters);
}

View File

@ -0,0 +1,149 @@
/*
* 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.items.fileconverter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.Metadata;
import org.openhab.core.library.CoreItemFactory;
/**
* {@link AbstractItemFileGenerator} is the base class for any {@link Item} file generator.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public abstract class AbstractItemFileGenerator implements ItemFileGenerator {
public AbstractItemFileGenerator() {
}
/**
* {@link ConfigParameter} is a container for any configuration parameter defined by a name and a value.
*/
public record ConfigParameter(String name, Object value) {
}
/**
* Get the list of available channel links for an item, sorted by natural order of their channel UID.
*
* @param metadata a collection of metadata
* @param itemName the item name
* @return the sorted list of metadata representing the channel links for this item
*/
protected List<Metadata> getChannelLinks(Collection<Metadata> metadata, String itemName) {
return metadata.stream().filter(
md -> "channel".equals(md.getUID().getNamespace()) && md.getUID().getItemName().equals(itemName))
.sorted((md1, md2) -> {
return md1.getValue().compareTo(md2.getValue());
}).collect(Collectors.toList());
}
/**
* Get the list of available metadata for an item, sorted by natural order of their namespaces.
* The "semantics" and "channel" namespaces are ignored.
*
* @param metadata a collection of metadata
* @param itemName the item name
* @return the sorted list of metadata for this item
*/
protected List<Metadata> getMetadata(Collection<Metadata> metadata, String itemName) {
return metadata.stream()
.filter(md -> !"semantics".equals(md.getUID().getNamespace())
&& !"channel".equals(md.getUID().getNamespace()) && md.getUID().getItemName().equals(itemName))
.sorted((md1, md2) -> {
return md1.getUID().getNamespace().compareTo(md2.getUID().getNamespace());
}).collect(Collectors.toList());
}
/**
* Get the list of configuration parameters for a metadata, sorted by natural order of their names
* with the exception of the "stateDescription" namespace where "min", "max" and "step" parameters
* are provided at first in this order.
*
* @param metadata the metadata
* @return a sorted list of configuration parameters for the metadata
*/
protected List<ConfigParameter> getConfigurationParameters(Metadata metadata) {
String namespace = metadata.getUID().getNamespace();
Map<String, Object> configParams = metadata.getConfiguration();
List<String> paramNames = configParams.keySet().stream().sorted((key1, key2) -> {
if ("stateDescription".equals(namespace)) {
if ("min".equals(key1)) {
return -1;
} else if ("min".equals(key2)) {
return 1;
} else if ("max".equals(key1)) {
return -1;
} else if ("max".equals(key2)) {
return 1;
} else if ("step".equals(key1)) {
return -1;
} else if ("step".equals(key2)) {
return 1;
}
}
return key1.compareTo(key2);
}).collect(Collectors.toList());
List<ConfigParameter> parameters = new ArrayList<>();
for (String paramName : paramNames) {
Object value = configParams.get(paramName);
if (value != null) {
parameters.add(new ConfigParameter(paramName, value));
}
}
return parameters;
}
/**
* Get the default state pattern for an item.
*
* @param item the item
* @return the default state pattern of null if no default
*/
protected @Nullable String getDefaultStatePattern(Item item) {
String pattern = null;
if (item instanceof GroupItem group) {
Item baseItem = group.getBaseItem();
if (baseItem != null) {
pattern = getDefaultStatePattern(baseItem);
}
} else if (item.getType().startsWith(CoreItemFactory.NUMBER + ":")) {
pattern = "%.0f %unit%";
} else {
switch (item.getType()) {
case CoreItemFactory.STRING:
pattern = "%s";
break;
case CoreItemFactory.DATETIME:
pattern = "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS";
break;
case CoreItemFactory.NUMBER:
pattern = "%.0f";
break;
default:
break;
}
}
return pattern;
}
}

View File

@ -0,0 +1,48 @@
/*
* 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.items.fileconverter;
import java.io.OutputStream;
import java.util.Collection;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.items.Item;
import org.openhab.core.items.Metadata;
/**
* {@link ItemFileGenerator} is the interface to implement by any file generator for {@link Item} object.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public interface ItemFileGenerator {
/**
* Returns the format of the file.
*
* @return the file format
*/
String getFileFormatGenerator();
/**
* Generate the file format for a sorted list of items.
*
* @param out the output stream to write the generated syntax to
* @param items the items
* @param metadata the provided collection of metadata for these items (including channel links)
* @param hideDefaultParameters true to hide the configuration parameters having the default value
*/
void generateFileFormat(OutputStream out, List<Item> items, Collection<Metadata> metadata,
boolean hideDefaultParameters);
}