[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;
|
package org.openhab.core.model.core;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.eclipse.emf.ecore.EObject;
|
import org.eclipse.emf.ecore.EObject;
|
||||||
|
@ -26,6 +27,7 @@ import org.eclipse.jdt.annotation.Nullable;
|
||||||
* come from.
|
* come from.
|
||||||
*
|
*
|
||||||
* @author Kai Kreuzer - Initial contribution
|
* @author Kai Kreuzer - Initial contribution
|
||||||
|
* @author Laurent Garnier - Added method generateSyntaxFromModel
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public interface ModelRepository {
|
public interface ModelRepository {
|
||||||
|
@ -92,4 +94,14 @@ public interface ModelRepository {
|
||||||
* @param listener the listener to remove
|
* @param listener the listener to remove
|
||||||
*/
|
*/
|
||||||
void removeModelRepositoryChangeListener(ModelRepositoryChangeListener listener);
|
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.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -50,6 +51,7 @@ import org.slf4j.LoggerFactory;
|
||||||
* @author Kai Kreuzer - Initial contribution
|
* @author Kai Kreuzer - Initial contribution
|
||||||
* @author Oliver Libutzki - Added reloadAllModelsOfType method
|
* @author Oliver Libutzki - Added reloadAllModelsOfType method
|
||||||
* @author Simon Kaufmann - added validation of models before loading them
|
* @author Simon Kaufmann - added validation of models before loading them
|
||||||
|
* @author Laurent Garnier - Added method generateSyntaxFromModel
|
||||||
*/
|
*/
|
||||||
@Component(immediate = true)
|
@Component(immediate = true)
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
|
@ -64,6 +66,8 @@ public class ModelRepositoryImpl implements ModelRepository {
|
||||||
|
|
||||||
private final SafeEMF safeEmf;
|
private final SafeEMF safeEmf;
|
||||||
|
|
||||||
|
private int counter;
|
||||||
|
|
||||||
@Activate
|
@Activate
|
||||||
public ModelRepositoryImpl(final @Reference SafeEMF safeEmf) {
|
public ModelRepositoryImpl(final @Reference SafeEMF safeEmf) {
|
||||||
this.safeEmf = safeEmf;
|
this.safeEmf = safeEmf;
|
||||||
|
@ -171,7 +175,8 @@ public class ModelRepositoryImpl implements ModelRepository {
|
||||||
|
|
||||||
return resourceListCopy.stream()
|
return resourceListCopy.stream()
|
||||||
.filter(input -> input.getURI().lastSegment().contains(".") && input.isLoaded()
|
.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();
|
.map(from -> from.getURI().path()).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -227,6 +232,23 @@ public class ModelRepositoryImpl implements ModelRepository {
|
||||||
listeners.remove(listener);
|
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) {
|
private @Nullable Resource getResource(String name) {
|
||||||
return resourceSet.getResource(URI.createURI(name), false);
|
return resourceSet.getResource(URI.createURI(name), false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ Import-Package: javax.measure,\
|
||||||
org.openhab.core.i18n,\
|
org.openhab.core.i18n,\
|
||||||
org.openhab.core.items,\
|
org.openhab.core.items,\
|
||||||
org.openhab.core.items.dto,\
|
org.openhab.core.items.dto,\
|
||||||
|
org.openhab.core.items.fileconverter,\
|
||||||
org.openhab.core.library.items,\
|
org.openhab.core.library.items,\
|
||||||
org.openhab.core.library.types,\
|
org.openhab.core.library.types,\
|
||||||
org.openhab.core.thing.util,\
|
org.openhab.core.thing.util,\
|
||||||
|
|
|
@ -18,19 +18,24 @@ org.openhab.core.model
|
||||||
|
|
||||||
import com.google.inject.Binder
|
import com.google.inject.Binder
|
||||||
import com.google.inject.name.Names
|
import com.google.inject.name.Names
|
||||||
|
import org.openhab.core.model.formatting.ItemsFormatter
|
||||||
import org.openhab.core.model.internal.valueconverter.ItemValueConverters
|
import org.openhab.core.model.internal.valueconverter.ItemValueConverters
|
||||||
import org.eclipse.xtext.conversion.IValueConverterService
|
import org.eclipse.xtext.conversion.IValueConverterService
|
||||||
|
import org.eclipse.xtext.formatting.IFormatter
|
||||||
import org.eclipse.xtext.linking.lazy.LazyURIEncoder
|
import org.eclipse.xtext.linking.lazy.LazyURIEncoder
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use this class to register components to be used at runtime / without the Equinox extension registry.
|
* Use this class to register components to be used at runtime / without the Equinox extension registry.
|
||||||
*/
|
*/
|
||||||
class ItemsRuntimeModule extends AbstractItemsRuntimeModule {
|
class ItemsRuntimeModule extends AbstractItemsRuntimeModule {
|
||||||
|
|
||||||
override Class<? extends IValueConverterService> bindIValueConverterService() {
|
override Class<? extends IValueConverterService> bindIValueConverterService() {
|
||||||
return ItemValueConverters
|
return ItemValueConverters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override Class<? extends IFormatter> bindIFormatter() {
|
||||||
|
return ItemsFormatter
|
||||||
|
}
|
||||||
|
|
||||||
override void configureUseIndexFragmentsForLazyLinking(Binder binder) {
|
override void configureUseIndexFragmentsForLazyLinking(Binder binder) {
|
||||||
binder.bind(Boolean.TYPE).annotatedWith(Names.named(LazyURIEncoder.USE_INDEXED_FRAGMENTS_BINDING)).toInstance(
|
binder.bind(Boolean.TYPE).annotatedWith(Names.named(LazyURIEncoder.USE_INDEXED_FRAGMENTS_BINDING)).toInstance(
|
||||||
Boolean.FALSE)
|
Boolean.FALSE)
|
||||||
|
|
|
@ -26,17 +26,17 @@ class ItemsFormatter extends AbstractDeclarativeFormatter {
|
||||||
|
|
||||||
override protected void configureFormatting(FormattingConfig c) {
|
override protected void configureFormatting(FormattingConfig c) {
|
||||||
c.setLinewrap(1, 1, 2).before(modelGroupItemRule)
|
c.setLinewrap(1, 1, 2).before(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.setNoSpace().withinKeywordPairs("(", ")")
|
||||||
|
c.setNoSpace().withinKeywordPairs("[", "]")
|
||||||
|
|
||||||
c.setIndentationIncrement.after(modelItemTypeRule)
|
c.setNoSpace().around(":", "=")
|
||||||
c.setIndentationDecrement.before(modelItemTypeRule)
|
c.setNoSpace().before(",")
|
||||||
c.setIndentationIncrement.after(modelGroupItemRule)
|
|
||||||
c.setIndentationDecrement.before(modelGroupItemRule)
|
c.autoLinewrap = 400
|
||||||
|
|
||||||
c.autoLinewrap = 160
|
|
||||||
c.setLinewrap(0, 1, 2).before(SL_COMMENTRule)
|
c.setLinewrap(0, 1, 2).before(SL_COMMENTRule)
|
||||||
c.setLinewrap(0, 1, 2).before(ML_COMMENTRule)
|
c.setLinewrap(0, 1, 2).before(ML_COMMENTRule)
|
||||||
c.setLinewrap(0, 1, 1).after(ML_COMMENTRule)
|
c.setLinewrap(0, 1, 1).after(ML_COMMENTRule)
|
||||||
|
@ -48,4 +48,16 @@ class ItemsFormatter extends AbstractDeclarativeFormatter {
|
||||||
locator.before(pair.second)
|
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,\
|
||||||
org.openhab.core.thing.binding,\
|
org.openhab.core.thing.binding,\
|
||||||
org.openhab.core.thing.binding.builder,\
|
org.openhab.core.thing.binding.builder,\
|
||||||
|
org.openhab.core.thing.fileconverter,\
|
||||||
org.openhab.core.thing.link,\
|
org.openhab.core.thing.link,\
|
||||||
org.openhab.core.thing.type,\
|
org.openhab.core.thing.type,\
|
||||||
org.openhab.core.thing.util,\
|
org.openhab.core.thing.util,\
|
||||||
|
org.openhab.core.types,\
|
||||||
org.openhab.core.types.util,\
|
org.openhab.core.types.util,\
|
||||||
org.openhab.core.util,\
|
org.openhab.core.util,\
|
||||||
org.openhab.core.model.core,\
|
org.openhab.core.model.core,\
|
||||||
|
|
|
@ -26,9 +26,9 @@ ModelBridge returns ModelThing:
|
||||||
properties+=ModelProperty? (',' properties+=ModelProperty)*
|
properties+=ModelProperty? (',' properties+=ModelProperty)*
|
||||||
']')?
|
']')?
|
||||||
('{'
|
('{'
|
||||||
('Things:')?
|
(thingsHeader?='Things:')?
|
||||||
things+=(ModelThing|ModelBridge)*
|
things+=(ModelThing|ModelBridge)*
|
||||||
('Channels:')?
|
(channelsHeader?='Channels:')?
|
||||||
channels+=ModelChannel*
|
channels+=ModelChannel*
|
||||||
|
|
||||||
'}')?
|
'}')?
|
||||||
|
@ -43,7 +43,7 @@ ModelThing:
|
||||||
properties+=ModelProperty? (',' properties+=ModelProperty)*
|
properties+=ModelProperty? (',' properties+=ModelProperty)*
|
||||||
']')?
|
']')?
|
||||||
('{'
|
('{'
|
||||||
('Channels:')?
|
(channelsHeader?='Channels:')?
|
||||||
channels+=ModelChannel*
|
channels+=ModelChannel*
|
||||||
'}')?
|
'}')?
|
||||||
;
|
;
|
||||||
|
|
|
@ -12,11 +12,13 @@
|
||||||
*/
|
*/
|
||||||
package org.openhab.core.model.thing
|
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.Binder
|
||||||
import com.google.inject.name.Names
|
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.
|
* 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
|
return org.openhab.core.model.thing.serializer.ThingSyntacticSequencerExtension
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override Class<? extends IFormatter> bindIFormatter() {
|
||||||
|
return ThingFormatter
|
||||||
|
}
|
||||||
|
|
||||||
override void configureUseIndexFragmentsForLazyLinking(Binder binder) {
|
override void configureUseIndexFragmentsForLazyLinking(Binder binder) {
|
||||||
binder.bind(Boolean.TYPE).annotatedWith(Names.named(LazyURIEncoder.USE_INDEXED_FRAGMENTS_BINDING)).toInstance(
|
binder.bind(Boolean.TYPE).annotatedWith(Names.named(LazyURIEncoder.USE_INDEXED_FRAGMENTS_BINDING)).toInstance(
|
||||||
Boolean.FALSE)
|
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.AbstractDeclarativeFormatter
|
||||||
import org.eclipse.xtext.formatting.impl.FormattingConfig
|
import org.eclipse.xtext.formatting.impl.FormattingConfig
|
||||||
// import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
// import org.openhab.core.model.thing.services.ThingGrammarAccess
|
import org.openhab.core.model.thing.services.ThingGrammarAccess
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class contains custom formatting description.
|
* This class contains custom formatting description.
|
||||||
|
@ -30,13 +30,53 @@ import org.eclipse.xtext.formatting.impl.FormattingConfig
|
||||||
*/
|
*/
|
||||||
class ThingFormatter extends AbstractDeclarativeFormatter {
|
class ThingFormatter extends AbstractDeclarativeFormatter {
|
||||||
|
|
||||||
// @Inject extension ThingGrammarAccess
|
@Inject extension ThingGrammarAccess
|
||||||
|
|
||||||
override protected void configureFormatting(FormattingConfig c) {
|
override protected void configureFormatting(FormattingConfig c) {
|
||||||
// It's usually a good idea to activate the following three statements.
|
c.setLinewrap(1, 1, 2).before("Bridge", "Things:", "Channels:")
|
||||||
// They will add and preserve newlines around comments
|
c.setLinewrap(1, 1, 2).before(modelThingRule)
|
||||||
// c.setLinewrap(0, 1, 2).before(SL_COMMENTRule)
|
c.setLinewrap(1, 1, 2).before(modelChannelRule)
|
||||||
// c.setLinewrap(0, 1, 2).before(ML_COMMENTRule)
|
|
||||||
// c.setLinewrap(0, 1, 1).after(ML_COMMENTRule)
|
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