[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
parent
e16b52d601
commit
67303fa5f3
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,\
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,\
|
||||
|
|
|
@ -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*
|
||||
'}')?
|
||||
;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
Loading…
Reference in New Issue