YAML configuration: add support for things (#4691)

Related to #3666

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
pull/4730/head
lolodomo 2025-04-19 22:21:09 +02:00 committed by GitHub
parent 1c955f3cb3
commit b7b23db9a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 1790 additions and 41 deletions

View File

@ -94,6 +94,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
* This resource is registered with the Jersey servlet.
*
* @author Laurent Garnier - Initial contribution
* @author Laurent Garnier - Add YAML output for things
*/
@Component
@JaxrsResource
@ -108,6 +109,15 @@ public class FileFormatResource implements RESTResource {
/** The URI path to this resource */
public static final String PATH_FILE_FORMAT = "file-format";
private static final String DSL_THINGS_EXAMPLE = "Bridge binding:typeBridge:idBridge \"Label brigde\" @ \"Location bridge\" [stringParam=\"my value\"] {" //
+ "\n Thing type id \"Label thing\" @ \"Location thing\" [booleanParam=true, decimalParam=2.5]\n}";
private static final String DSL_THING_EXAMPLE = "Thing binding:type:idBridge:id \"Label thing\" @ \"Location thing\" (binding:typeBridge:idBridge) [stringParam=\"my value\", booleanParam=true, decimalParam=2.5]";
private static final String YAML_THINGS_EXAMPLE = "version: 2\nthings:\n" //
+ " binding:typeBridge:idBridge:\n isBridge: true\n label: Label bridge\n location: Location bridge\n config:\n stringParam: my value\n"
+ " binding:type:idBridge:id:\n bridge: binding:typeBridge:idBridge\n label: Label thing\n location: Location thing\n config:\n booleanParam: true\n decimalParam: 2.5";
private static final String YAML_THING_EXAMPLE = "version: 2\nthings:\n" //
+ " binding:type:idBridge:id:\n bridge: binding:typeBridge:idBridge\n label: Label thing\n location: Location thing\n config:\n stringParam: my value\n booleanParam: true\n decimalParam: 2.5";
private final Logger logger = LoggerFactory.getLogger(FileFormatResource.class);
private final ItemRegistry itemRegistry;
@ -216,16 +226,28 @@ public class FileFormatResource implements RESTResource {
@GET
@RolesAllowed({ Role.ADMIN })
@Path("/things")
@Produces("text/vnd.openhab.dsl.thing")
@Produces({ "text/vnd.openhab.dsl.thing", "application/yaml" })
@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 = "200", description = "OK", content = {
@Content(mediaType = "text/vnd.openhab.dsl.thing", schema = @Schema(example = DSL_THINGS_EXAMPLE)),
@Content(mediaType = "application/yaml", schema = @Schema(example = YAML_THINGS_EXAMPLE)) }),
@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);
ThingFileGenerator generator;
switch (acceptHeader) {
case "text/vnd.openhab.dsl.thing":
generator = thingFileGenerators.get("DSL");
break;
case "application/yaml":
generator = thingFileGenerators.get("YAML");
break;
default:
generator = null;
break;
}
if (generator == null) {
return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE)
.entity("Unsupported media type '" + acceptHeader + "'!").build();
@ -238,18 +260,30 @@ public class FileFormatResource implements RESTResource {
@GET
@RolesAllowed({ Role.ADMIN })
@Path("/things/{thingUID}")
@Produces("text/vnd.openhab.dsl.thing")
@Produces({ "text/vnd.openhab.dsl.thing", "application/yaml" })
@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 = "200", description = "OK", content = {
@Content(mediaType = "text/vnd.openhab.dsl.thing", schema = @Schema(example = DSL_THING_EXAMPLE)),
@Content(mediaType = "application/yaml", schema = @Schema(example = YAML_THING_EXAMPLE)) }),
@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);
ThingFileGenerator generator;
switch (acceptHeader) {
case "text/vnd.openhab.dsl.thing":
generator = thingFileGenerators.get("DSL");
break;
case "application/yaml":
generator = thingFileGenerators.get("YAML");
break;
default:
generator = null;
break;
}
if (generator == null) {
return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE)
.entity("Unsupported media type '" + acceptHeader + "'!").build();

View File

@ -25,5 +25,10 @@
<artifactId>org.openhab.core.semantics</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.thing</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -12,7 +12,10 @@
*/
package org.openhab.core.model.yaml;
import java.util.List;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.model.yaml.internal.YamlModelRepositoryImpl;
/**
@ -30,6 +33,7 @@ import org.openhab.core.model.yaml.internal.YamlModelRepositoryImpl;
* @author Laurent Garnier - Initial contribution
* @author Jan N. Klug - Refactoring and improvements to JavaDoc
* @author Laurent Garnier - Added methods setId and cloneWithoutId
* @author Laurent Garnier - Added parameters to method isValid
*/
public interface YamlElement {
@ -74,7 +78,10 @@ public interface YamlElement {
* <li>MAY perform additional checks</li>
* </ul>
*
* @param errors a list of error messages to fill with fatal invalid controls; can be null to ignore messages
* @param warnings a list of warning messages to fill with non-fatal invalid controls; can be null to ignore
* messages
* @return {@code true} if all the checks are completed successfully
*/
boolean isValid();
boolean isValid(@Nullable List<@NonNull String> errors, @Nullable List<@NonNull String> warnings);
}

View File

@ -12,12 +12,16 @@
*/
package org.openhab.core.model.yaml;
import java.io.OutputStream;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link YamlModelRepository} defines methods to update elements in a YAML model.
*
* @author Jan N. Klug - Initial contribution
* @author Laurent Garnier - Added methods refreshModelElements and generateSyntaxFromElements
*/
@NonNullByDefault
public interface YamlModelRepository {
@ -26,4 +30,20 @@ public interface YamlModelRepository {
void removeElementFromModel(String modelName, YamlElement element);
void updateElementInModel(String modelName, YamlElement element);
/**
* Triggers the refresh of a certain type of elements in a given model.
*
* @param modelName the model name
* @param elementName the type of elements to refresh
*/
void refreshModelElements(String modelName, String elementName);
/**
* Generate the YAML syntax from a provided list of elements.
*
* @param out the output stream to write the generated syntax to
* @param elements the list of elements to includ
*/
void generateSyntaxFromElements(OutputStream out, List<YamlElement> elements);
}

View File

@ -15,6 +15,7 @@ package org.openhab.core.model.yaml.internal;
import static org.openhab.core.service.WatchService.Kind.CREATE;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
@ -22,7 +23,9 @@ import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -71,6 +74,8 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
* @author Jan N. Klug - Refactored for multiple types per file and add modifying possibility
* @author Laurent Garnier - Introduce version 2 using map instead of table
* @author Laurent Garnier - Added basic version management
* @author Laurent Garnier - Added methods refreshModelElements and generateSyntaxFromElements + new parameters
* for method isValid
*/
@NonNullByDefault
@Component(immediate = true)
@ -164,7 +169,8 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
List<JsonNode> removedNodes = modelEntry.getValue();
if (!removedNodes.isEmpty()) {
getElementListeners(elementName, version).forEach(listener -> {
List removedElements = parseJsonNodesV1(removedNodes, listener.getElementClass());
List removedElements = parseJsonNodesV1(removedNodes, listener.getElementClass(), null,
null);
listener.removedModel(modelName, removedElements);
});
}
@ -174,7 +180,8 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
JsonNode removedMapNode = modelEntry.getValue();
if (removedMapNode != null) {
getElementListeners(elementName, version).forEach(listener -> {
List removedElements = parseJsonMapNode(removedMapNode, listener.getElementClass());
List removedElements = parseJsonMapNode(removedMapNode, listener.getElementClass(), null,
null);
listener.removedModel(modelName, removedElements);
});
}
@ -239,10 +246,20 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
for (YamlModelListener<?> elementListener : getElementListeners(elementName, modelVersion)) {
Class<? extends YamlElement> elementClass = elementListener.getElementClass();
List<String> errors = new ArrayList<>();
List<String> warnings = new ArrayList<>();
Map<String, ? extends YamlElement> oldElements = listToMap(
parseJsonNodes(oldNodeV1Elements, oldNodeElements, elementClass));
parseJsonNodes(oldNodeV1Elements, oldNodeElements, elementClass, null, null));
Map<String, ? extends YamlElement> newElements = listToMap(
parseJsonNodes(newNodeV1Elements, newNodeElements, elementClass));
parseJsonNodes(newNodeV1Elements, newNodeElements, elementClass, errors, warnings));
errors.forEach(error -> {
logger.warn("YAML model {}: {}", modelName, error);
});
warnings.forEach(warning -> {
logger.info("YAML model {}: {}", modelName, warning);
});
List addedElements = newElements.values().stream()
.filter(e -> !oldElements.containsKey(e.getId())).toList();
@ -314,7 +331,15 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
if (modelNodes.isEmpty() && modelMapNode == null) {
continue;
}
List modelElements = parseJsonNodes(modelNodes, modelMapNode, elementClass);
List<String> errors = new ArrayList<>();
List<String> warnings = new ArrayList<>();
List modelElements = parseJsonNodes(modelNodes, modelMapNode, elementClass, errors, warnings);
errors.forEach(error -> {
logger.warn("YAML model {}: {}", modelName, error);
});
warnings.forEach(warning -> {
logger.info("YAML model {}: {}", modelName, warning);
});
listener.addedModel(modelName, modelElements);
}
}
@ -362,7 +387,15 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
}
// notify listeners
for (YamlModelListener<?> l : getElementListeners(elementName, model.getVersion())) {
List newElements = parseJsonNodes(addedNodes, mapAddedNode, l.getElementClass());
List<String> errors = new ArrayList<>();
List<String> warnings = new ArrayList<>();
List newElements = parseJsonNodes(addedNodes, mapAddedNode, l.getElementClass(), errors, warnings);
errors.forEach(error -> {
logger.warn("YAML model {}: {}", modelName, error);
});
warnings.forEach(warning -> {
logger.info("YAML model {}: {}", modelName, warning);
});
if (!newElements.isEmpty()) {
l.addedModel(modelName, newElements);
}
@ -417,7 +450,7 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
}
// notify listeners
for (YamlModelListener<?> l : getElementListeners(elementName, model.getVersion())) {
List oldElements = parseJsonNodes(removedNodes, mapRemovedNode, l.getElementClass());
List oldElements = parseJsonNodes(removedNodes, mapRemovedNode, l.getElementClass(), null, null);
if (!oldElements.isEmpty()) {
l.removedModel(modelName, oldElements);
}
@ -474,7 +507,15 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
}
// notify listeners
for (YamlModelListener<?> l : getElementListeners(elementName, model.getVersion())) {
List newElements = parseJsonNodes(updatedNodes, mapUpdatedNode, l.getElementClass());
List<String> errors = new ArrayList<>();
List<String> warnings = new ArrayList<>();
List newElements = parseJsonNodes(updatedNodes, mapUpdatedNode, l.getElementClass(), errors, warnings);
errors.forEach(error -> {
logger.warn("YAML model {}: {}", modelName, error);
});
warnings.forEach(warning -> {
logger.info("YAML model {}: {}", modelName, warning);
});
if (!newElements.isEmpty()) {
l.updatedModel(modelName, newElements);
}
@ -483,6 +524,35 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
writeModel(modelName);
}
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public void refreshModelElements(String modelName, String elementName) {
logger.info("Refreshing {} from YAML model {}", elementName, modelName);
YamlModelWrapper model = modelCache.get(modelName);
if (model == null) {
logger.warn("Failed to refresh model {} because it is not known.", modelName);
return;
}
List<JsonNode> modelNodes = model.getNodesV1().get(elementName);
JsonNode modelMapNode = model.getNodes().get(elementName);
if (modelNodes == null && modelMapNode == null) {
logger.warn("Failed to refresh model {} because type {} is not known in the model.", modelName,
elementName);
return;
}
getElementListeners(elementName, model.getVersion()).forEach(listener -> {
Class<? extends YamlElement> elementClass = listener.getElementClass();
List elements = parseJsonNodes(modelNodes != null ? modelNodes : List.of(), modelMapNode, elementClass,
null, null);
if (!elements.isEmpty()) {
listener.updatedModel(modelName, elements);
}
});
}
private void writeModel(String modelName) {
YamlModelWrapper model = modelCache.get(modelName);
if (model == null) {
@ -530,6 +600,44 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
}
}
@Override
public void generateSyntaxFromElements(OutputStream out, List<YamlElement> elements) {
// create the model
JsonNodeFactory nodeFactory = objectMapper.getNodeFactory();
ObjectNode rootNode = nodeFactory.objectNode();
rootNode.put("version", DEFAULT_MODEL_VERSION);
// First separate elements per type
Map<String, List<YamlElement>> elementsPerTypes = new HashMap<>();
elements.forEach(element -> {
YamlElementName annotation = element.getClass().getAnnotation(YamlElementName.class);
if (annotation != null) {
String elementName = annotation.value();
List<YamlElement> elts = elementsPerTypes.get(elementName);
if (elts == null) {
elts = new ArrayList<>();
elementsPerTypes.put(elementName, elts);
}
elts.add(element);
}
});
// Generate one entry for each element type
elementsPerTypes.entrySet().forEach(entry -> {
Map<String, YamlElement> mapElts = new LinkedHashMap<>();
entry.getValue().forEach(elt -> {
mapElts.put(elt.getId(), elt.cloneWithoutId());
});
rootNode.set(entry.getKey(), objectMapper.valueToTree(mapElts));
});
try {
objectMapper.writeValue(out, rootNode);
} catch (IOException e) {
logger.warn("Failed to serialize model: {}", e.getMessage());
}
}
private List<YamlModelListener<?>> getElementListeners(String elementName) {
return Objects.requireNonNull(elementListeners.computeIfAbsent(elementName, k -> new CopyOnWriteArrayList<>()));
}
@ -542,7 +650,7 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
private <T extends YamlElement> @Nullable JsonNode findNodeById(List<JsonNode> nodes, Class<T> elementClass,
String id) {
return nodes.stream().filter(node -> {
Optional<T> parsedNode = parseJsonNode(node, elementClass);
Optional<T> parsedNode = parseJsonNode(node, elementClass, null, null);
return parsedNode.filter(yamlDTO -> id.equals(yamlDTO.getId())).isPresent();
}).findAny().orElse(null);
}
@ -552,23 +660,29 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
}
// To be used only for version 1
private <T extends YamlElement> List<T> parseJsonNodesV1(List<JsonNode> nodes, Class<T> elementClass) {
return nodes.stream().map(nE -> parseJsonNode(nE, elementClass)).flatMap(Optional::stream)
.filter(YamlElement::isValid).toList();
private <T extends YamlElement> List<T> parseJsonNodesV1(List<JsonNode> nodes, Class<T> elementClass,
@Nullable List<String> errors, @Nullable List<String> warnings) {
return nodes.stream().map(nE -> parseJsonNode(nE, elementClass, errors, warnings)).flatMap(Optional::stream)
.filter(elt -> elt.isValid(errors, warnings)).toList();
}
// To be used only for version 1
private <T extends YamlElement> Optional<T> parseJsonNode(JsonNode node, Class<T> elementClass) {
private <T extends YamlElement> Optional<T> parseJsonNode(JsonNode node, Class<T> elementClass,
@Nullable List<String> errors, @Nullable List<String> warnings) {
try {
return Optional.of(objectMapper.treeToValue(node, elementClass));
} catch (JsonProcessingException e) {
logger.warn("Could not parse element {} to {}: {}", node, elementClass, e.getMessage());
if (errors != null) {
errors.add("Could not parse element %s to %s: %s".formatted(node.toPrettyString(),
elementClass.getSimpleName(), e.getMessage()));
}
return Optional.empty();
}
}
// To be not used for version 1
private <T extends YamlElement> List<T> parseJsonMapNode(@Nullable JsonNode mapNode, Class<T> elementClass) {
private <T extends YamlElement> List<T> parseJsonMapNode(@Nullable JsonNode mapNode, Class<T> elementClass,
@Nullable List<String> errors, @Nullable List<String> warnings) {
List<T> elements = new ArrayList<>();
if (mapNode != null) {
Iterator<String> it = mapNode.fieldNames();
@ -583,7 +697,9 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
elt.setId(id);
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
logger.warn("Could not create new instance of {}", elementClass);
if (errors != null) {
errors.add("could not create new instance of %s".formatted(elementClass.getSimpleName()));
}
}
} else {
@ -591,10 +707,13 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
elt = objectMapper.treeToValue(node, elementClass);
elt.setId(id);
} catch (JsonProcessingException e) {
logger.warn("Could not parse element {} to {}: {}", node, elementClass, e.getMessage());
if (errors != null) {
errors.add("could not parse element %s to %s: %s".formatted(node.toPrettyString(),
elementClass.getSimpleName(), e.getMessage()));
}
}
}
if (elt != null && elt.isValid()) {
if (elt != null && elt.isValid(errors, warnings)) {
elements.add(elt);
}
}
@ -604,10 +723,10 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
// Usable whatever the format version
private <T extends YamlElement> List<T> parseJsonNodes(List<JsonNode> nodes, @Nullable JsonNode mapNode,
Class<T> elementClass) {
Class<T> elementClass, @Nullable List<String> errors, @Nullable List<String> warnings) {
List<T> elements = new ArrayList<>();
elements.addAll(parseJsonNodesV1(nodes, elementClass));
elements.addAll(parseJsonMapNode(mapNode, elementClass));
elements.addAll(parseJsonNodesV1(nodes, elementClass, errors, warnings));
elements.addAll(parseJsonMapNode(mapNode, elementClass, errors, warnings));
return elements;
}
}

View File

@ -19,8 +19,6 @@ import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.model.yaml.YamlElement;
import org.openhab.core.model.yaml.YamlElementName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link YamlSemanticTagDTO} is a data transfer object used to serialize a semantic tag
@ -32,8 +30,6 @@ import org.slf4j.LoggerFactory;
@YamlElementName("tags")
public class YamlSemanticTagDTO implements YamlElement, Cloneable {
private final Logger logger = LoggerFactory.getLogger(YamlSemanticTagDTO.class);
public String uid;
public String label;
public String description;
@ -66,9 +62,11 @@ public class YamlSemanticTagDTO implements YamlElement, Cloneable {
}
@Override
public boolean isValid() {
public boolean isValid(@Nullable List<@NonNull String> errors, @Nullable List<@NonNull String> warnings) {
if (uid == null || uid.isBlank()) {
logger.debug("uid missing");
if (errors != null) {
errors.add("tag uid is missing");
}
return false;
}
return true;

View File

@ -0,0 +1,117 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.model.yaml.internal.things;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.model.yaml.internal.util.YamlElementUtils;
import org.openhab.core.thing.type.ChannelKind;
/**
* The {@link YamlChannelDTO} is a data transfer object used to serialize a channel in a YAML configuration file.
*
* @author Laurent Garnier - Initial contribution
*/
public class YamlChannelDTO {
private static final Pattern CHANNEL_TYPE_PATTERN = Pattern.compile("^[a-zA-Z0-9_][a-zA-Z0-9_-]*$");
public String type;
public String kind;
public String itemType;
public String itemDimension;
public String label;
public Map<@NonNull String, @NonNull Object> config;
public YamlChannelDTO() {
}
public boolean isValid(@NonNull List<@NonNull String> errors, @NonNull List<@NonNull String> warnings) {
boolean ok = true;
if (type != null) {
if (!CHANNEL_TYPE_PATTERN.matcher(type).find()) {
errors.add(
"type \"%s\" is not matching the expected syntax [a-zA-Z0-9_][a-zA-Z0-9_-]*".formatted(type));
ok = false;
}
if (kind != null) {
warnings.add(
"kind \"%s\" is ignored as a type is also provided; kind will be retrieved from the channel type"
.formatted(kind));
}
if (itemType != null) {
warnings.add(
"itemType \"%s\" is ignored as a type is also provided; item type will be retrieved from the channel type"
.formatted(itemType));
}
} else if (itemType != null) {
if (!YamlElementUtils.isValidItemType(itemType)) {
errors.add("itemType \"%s\" is invalid".formatted(itemType));
ok = false;
} else if (YamlElementUtils.isNumberItemType(itemType)) {
if (!YamlElementUtils.isValidItemDimension(itemDimension)) {
errors.add("itemDimension \"%s\" is invalid".formatted(itemDimension));
ok = false;
}
} else if (itemDimension != null) {
warnings.add("itemDimension \"%s\" is is ignored as item type is not Number".formatted(itemDimension));
}
try {
ChannelKind.parse(kind);
} catch (IllegalArgumentException e) {
warnings.add(
"kind \"%s\" is invalid (only \"state\" and \"trigger\" whatever the case are valid); \"state\" will be considered"
.formatted(kind != null ? kind : "null"));
}
} else {
errors.add("type or itemType is mandatory");
ok = false;
}
return ok;
}
public ChannelKind getKind() {
try {
return ChannelKind.parse(kind);
} catch (IllegalArgumentException e) {
return ChannelKind.STATE;
}
}
public @Nullable String getItemType() {
return YamlElementUtils.getItemTypeWithDimension(itemType, itemDimension);
}
@Override
public int hashCode() {
return Objects.hash(type, getKind(), getItemType(), label);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
} else if (obj == null || getClass() != obj.getClass()) {
return false;
}
YamlChannelDTO other = (YamlChannelDTO) obj;
return Objects.equals(type, other.type) && getKind() == other.getKind()
&& Objects.equals(getItemType(), other.getItemType()) && Objects.equals(label, other.label)
&& YamlElementUtils.equalsConfig(config, other.config);
}
}

View File

@ -0,0 +1,185 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.model.yaml.internal.things;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.AbstractUID;
import org.openhab.core.model.yaml.YamlElement;
import org.openhab.core.model.yaml.YamlElementName;
import org.openhab.core.model.yaml.internal.util.YamlElementUtils;
/**
* The {@link YamlThingDTO} is a data transfer object used to serialize a thing in a YAML configuration file.
*
* @author Laurent Garnier - Initial contribution
*/
@YamlElementName("things")
public class YamlThingDTO implements YamlElement, Cloneable {
private static final Pattern THING_UID_SEGMENT_PATTERN = Pattern.compile("^[a-zA-Z0-9_][a-zA-Z0-9_-]*$");
private static final Pattern CHANNEL_ID_PATTERN = Pattern
.compile("^[a-zA-Z0-9_][a-zA-Z0-9_-]*(#[a-zA-Z0-9_][a-zA-Z0-9_-]*)?$");
public String uid;
public Boolean isBridge;
public String bridge;
public String label;
public String location;
public Map<@NonNull String, @NonNull Object> config;
public Map<@NonNull String, @NonNull YamlChannelDTO> channels;
public YamlThingDTO() {
}
@Override
public @NonNull String getId() {
return uid == null ? "" : uid;
}
@Override
public void setId(@NonNull String id) {
uid = id;
}
@Override
public YamlElement cloneWithoutId() {
YamlThingDTO copy;
try {
copy = (YamlThingDTO) super.clone();
copy.uid = null;
return copy;
} catch (CloneNotSupportedException e) {
// Will never happen
return new YamlThingDTO();
}
}
@Override
public boolean isValid(@Nullable List<@NonNull String> errors, @Nullable List<@NonNull String> warnings) {
// Check that uid is present
if (uid == null || uid.isBlank()) {
if (errors != null) {
errors.add("thing uid is missing");
}
return false;
}
boolean ok = true;
// Check that uid has at least 3 segments and each segment respects the expected syntax
String[] segments = uid.split(AbstractUID.SEPARATOR);
if (segments.length < 3) {
if (errors != null) {
errors.add("thing %s: uid contains insufficient segments".formatted(uid));
}
ok = false;
}
for (String segment : segments) {
if (!THING_UID_SEGMENT_PATTERN.matcher(segment).find()) {
if (errors != null) {
errors.add(
"thing %s: segement \"%s\" in uid is not matching the expected syntax [a-zA-Z0-9_][a-zA-Z0-9_-]*"
.formatted(uid, segment));
}
ok = false;
}
}
if (bridge != null && !bridge.isBlank()) {
// Check that bridge has at least 3 segments and each segment respects the expected syntax
segments = bridge.split(AbstractUID.SEPARATOR);
if (segments.length < 3) {
if (errors != null) {
errors.add("thing %s: bridge \"%s\" contains insufficient segments".formatted(uid, bridge));
}
ok = false;
}
for (String segment : segments) {
if (!THING_UID_SEGMENT_PATTERN.matcher(segment).find()) {
if (errors != null) {
errors.add(
"thing %s: segement \"%s\" in bridge is not matching the expected syntax [a-zA-Z0-9_][a-zA-Z0-9_-]*"
.formatted(uid, segment));
}
ok = false;
}
}
}
if (channels != null) {
for (Map.Entry<@NonNull String, @NonNull YamlChannelDTO> entry : channels.entrySet()) {
String channelId = entry.getKey();
if (!CHANNEL_ID_PATTERN.matcher(channelId).find()) {
if (errors != null) {
errors.add(
"thing %s: channel id \"%s\" is not matching the expected syntax [a-zA-Z0-9_][a-zA-Z0-9_-]*(#[a-zA-Z0-9_][a-zA-Z0-9_-]*)?"
.formatted(uid, channelId));
}
ok = false;
}
List<String> channelErrors = new ArrayList<>();
List<String> channelWarnings = new ArrayList<>();
ok &= entry.getValue().isValid(channelErrors, channelWarnings);
if (errors != null) {
channelErrors.forEach(error -> {
errors.add("thing %s channel %s: %s".formatted(uid, channelId, error));
});
}
if (warnings != null) {
channelWarnings.forEach(warning -> {
warnings.add("thing %s channel %s: %s".formatted(uid, channelId, warning));
});
}
}
}
return ok;
}
public boolean isBridge() {
return isBridge == null ? false : isBridge.booleanValue();
}
@Override
public int hashCode() {
return Objects.hash(uid, bridge, label, location);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
} else if (obj == null || getClass() != obj.getClass()) {
return false;
}
YamlThingDTO other = (YamlThingDTO) obj;
return Objects.equals(uid, other.uid) && isBridge() == other.isBridge() && Objects.equals(bridge, other.bridge)
&& Objects.equals(label, other.label) && Objects.equals(location, other.location)
&& YamlElementUtils.equalsConfig(config, other.config) && equalsChannels(channels, other.channels);
}
private boolean equalsChannels(@Nullable Map<@NonNull String, @NonNull YamlChannelDTO> first,
@Nullable Map<@NonNull String, @NonNull YamlChannelDTO> second) {
if (first != null && second != null) {
if (first.size() != second.size()) {
return false;
} else {
return first.entrySet().stream().allMatch(e -> e.getValue().equals(second.get(e.getKey())));
}
} else {
return first == null && second == null;
}
}
}

View File

@ -0,0 +1,442 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.model.yaml.internal.things;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.AbstractUID;
import org.openhab.core.common.registry.AbstractProvider;
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.i18n.LocaleProvider;
import org.openhab.core.model.yaml.YamlElementName;
import org.openhab.core.model.yaml.YamlModelListener;
import org.openhab.core.model.yaml.YamlModelRepository;
import org.openhab.core.service.ReadyMarker;
import org.openhab.core.service.ReadyService;
import org.openhab.core.service.StartLevelService;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingProvider;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.openhab.core.thing.binding.builder.BridgeBuilder;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.AutoUpdatePolicy;
import org.openhab.core.thing.type.ChannelDefinition;
import org.openhab.core.thing.type.ChannelKind;
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.openhab.core.thing.util.ThingHelper;
import org.openhab.core.util.BundleResolver;
import org.osgi.framework.Bundle;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link YamlThingProvider} is an OSGi service, that allows to define things in YAML configuration files.
* Files can be added, updated or removed at runtime.
* These things are automatically exposed to the {@link org.openhab.core.thing.ThingRegistry}.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
@Component(immediate = true, service = { ThingProvider.class, YamlThingProvider.class, YamlModelListener.class })
public class YamlThingProvider extends AbstractProvider<Thing>
implements ThingProvider, YamlModelListener<YamlThingDTO>, ReadyService.ReadyTracker {
private static final String XML_THING_TYPE = "openhab.xmlThingTypes";
private final Logger logger = LoggerFactory.getLogger(YamlThingProvider.class);
private final YamlModelRepository modelRepository;
private final BundleResolver bundleResolver;
private final ThingTypeRegistry thingTypeRegistry;
private final ChannelTypeRegistry channelTypeRegistry;
private final ConfigDescriptionRegistry configDescriptionRegistry;
private final LocaleProvider localeProvider;
private final List<ThingHandlerFactory> thingHandlerFactories = new CopyOnWriteArrayList<>();
private final Set<String> loadedXmlThingTypes = new CopyOnWriteArraySet<>();
private final Map<String, Collection<Thing>> thingsMap = new ConcurrentHashMap<>();
private final List<QueueContent> queue = new CopyOnWriteArrayList<>();
private final Runnable lazyRetryRunnable = new Runnable() {
@Override
public void run() {
logger.debug("Starting lazy retry thread");
while (!queue.isEmpty()) {
for (QueueContent qc : queue) {
logger.trace("Retry creating thing {}", qc.thingUID);
Thing newThing = qc.thingHandlerFactory.createThing(qc.thingTypeUID, qc.configuration, qc.thingUID,
qc.bridgeUID);
if (newThing != null) {
logger.debug("Successfully loaded thing \'{}\' during retry", qc.thingUID);
Thing oldThing = null;
for (Map.Entry<String, Collection<Thing>> entry : thingsMap.entrySet()) {
oldThing = entry.getValue().stream().filter(t -> t.getUID().equals(newThing.getUID()))
.findFirst().orElse(null);
if (oldThing != null) {
mergeThing(newThing, oldThing);
Collection<Thing> thingsForModel = Objects
.requireNonNull(thingsMap.get(entry.getKey()));
thingsForModel.remove(oldThing);
thingsForModel.add(newThing);
logger.debug("Refreshing thing \'{}\' after successful retry", newThing.getUID());
if (!ThingHelper.equals(oldThing, newThing)) {
notifyListenersAboutUpdatedElement(oldThing, newThing);
}
break;
}
}
if (oldThing == null) {
logger.debug("Refreshing thing \'{}\' after retry failed because thing is not found",
newThing.getUID());
}
queue.remove(qc);
}
}
if (!queue.isEmpty()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
logger.debug("Lazy retry thread ran out of work. Good bye.");
}
};
private boolean modelLoaded = false;
private @Nullable Thread lazyRetryThread;
private record QueueContent(ThingHandlerFactory thingHandlerFactory, ThingTypeUID thingTypeUID,
Configuration configuration, ThingUID thingUID, @Nullable ThingUID bridgeUID) {
}
@Activate
public YamlThingProvider(final @Reference YamlModelRepository modelRepository,
final @Reference BundleResolver bundleResolver, final @Reference ThingTypeRegistry thingTypeRegistry,
final @Reference ChannelTypeRegistry channelTypeRegistry,
final @Reference ConfigDescriptionRegistry configDescriptionRegistry,
final @Reference LocaleProvider localeProvider) {
this.modelRepository = modelRepository;
this.bundleResolver = bundleResolver;
this.thingTypeRegistry = thingTypeRegistry;
this.channelTypeRegistry = channelTypeRegistry;
this.configDescriptionRegistry = configDescriptionRegistry;
this.localeProvider = localeProvider;
}
@Deactivate
public void deactivate() {
queue.clear();
thingsMap.clear();
loadedXmlThingTypes.clear();
}
@Override
public Collection<Thing> getAll() {
return thingsMap.values().stream().flatMap(list -> list.stream()).collect(Collectors.toList());
}
@Override
public Class<YamlThingDTO> getElementClass() {
return YamlThingDTO.class;
}
@Override
public boolean isVersionSupported(int version) {
return version >= 2;
}
@Override
public boolean isDeprecated() {
return false;
}
@Override
public void addedModel(String modelName, Collection<YamlThingDTO> elements) {
List<Thing> added = elements.stream().map(this::mapThing).filter(Objects::nonNull).toList();
Collection<Thing> modelThings = Objects
.requireNonNull(thingsMap.computeIfAbsent(modelName, k -> new ArrayList<>()));
modelThings.addAll(added);
added.forEach(t -> {
logger.debug("model {} added thing {}", modelName, t.getUID());
notifyListenersAboutAddedElement(t);
});
}
@Override
public void updatedModel(String modelName, Collection<YamlThingDTO> elements) {
List<Thing> updated = elements.stream().map(this::mapThing).filter(Objects::nonNull).toList();
Collection<Thing> modelThings = Objects
.requireNonNull(thingsMap.computeIfAbsent(modelName, k -> new ArrayList<>()));
updated.forEach(t -> {
modelThings.stream().filter(th -> th.getUID().equals(t.getUID())).findFirst().ifPresentOrElse(oldThing -> {
modelThings.remove(oldThing);
modelThings.add(t);
logger.debug("model {} updated thing {}", modelName, t.getUID());
notifyListenersAboutUpdatedElement(oldThing, t);
}, () -> {
modelThings.add(t);
logger.debug("model {} added thing {}", modelName, t.getUID());
notifyListenersAboutAddedElement(t);
});
});
}
@Override
public void removedModel(String modelName, Collection<YamlThingDTO> elements) {
List<Thing> removed = elements.stream().map(this::mapThing).filter(Objects::nonNull).toList();
Collection<Thing> modelThings = Objects
.requireNonNull(thingsMap.computeIfAbsent(modelName, k -> new ArrayList<>()));
removed.forEach(t -> {
modelThings.stream().filter(th -> th.getUID().equals(t.getUID())).findFirst().ifPresentOrElse(oldThing -> {
modelThings.remove(oldThing);
logger.debug("model {} removed thing {}", modelName, t.getUID());
notifyListenersAboutRemovedElement(oldThing);
}, () -> logger.debug("model {} thing {} not found", modelName, t.getUID()));
});
}
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
protected void addThingHandlerFactory(final ThingHandlerFactory thingHandlerFactory) {
thingHandlerFactories.add(thingHandlerFactory);
thingHandlerFactoryAdded(thingHandlerFactory);
}
protected void removeThingHandlerFactory(final ThingHandlerFactory thingHandlerFactory) {
thingHandlerFactories.remove(thingHandlerFactory);
}
@Reference
public void setReadyService(final ReadyService readyService) {
readyService.registerTracker(this);
}
public void unsetReadyService(final ReadyService readyService) {
readyService.unregisterTracker(this);
}
@Override
public void onReadyMarkerAdded(ReadyMarker readyMarker) {
String type = readyMarker.getType();
if (StartLevelService.STARTLEVEL_MARKER_TYPE.equals(type)) {
modelLoaded = Integer.parseInt(readyMarker.getIdentifier()) >= StartLevelService.STARTLEVEL_MODEL;
} else if (XML_THING_TYPE.equals(type)) {
String bsn = readyMarker.getIdentifier();
loadedXmlThingTypes.add(bsn);
thingHandlerFactories.stream().filter(factory -> bsn.equals(getBundleName(factory))).forEach(factory -> {
thingHandlerFactoryAdded(factory);
});
}
}
@Override
public void onReadyMarkerRemoved(ReadyMarker readyMarker) {
loadedXmlThingTypes.remove(readyMarker.getIdentifier());
}
private void thingHandlerFactoryAdded(ThingHandlerFactory thingHandlerFactory) {
String bundleName = getBundleName(thingHandlerFactory);
if (bundleName != null && loadedXmlThingTypes.contains(bundleName)) {
logger.debug("Refreshing models due to new thing handler factory {}",
thingHandlerFactory.getClass().getSimpleName());
thingsMap.keySet().forEach(modelName -> {
modelRepository.refreshModelElements(modelName,
getElementClass().getAnnotation(YamlElementName.class).value());
});
}
}
private @Nullable String getBundleName(ThingHandlerFactory thingHandlerFactory) {
Bundle bundle = bundleResolver.resolveBundle(thingHandlerFactory.getClass());
return bundle == null ? null : bundle.getSymbolicName();
}
private @Nullable Thing mapThing(YamlThingDTO thingDto) {
ThingUID thingUID = new ThingUID(thingDto.uid);
String[] segments = thingUID.getAsString().split(AbstractUID.SEPARATOR);
ThingTypeUID thingTypeUID = new ThingTypeUID(thingUID.getBindingId(), segments[1]);
ThingHandlerFactory handlerFactory = thingHandlerFactories.stream()
.filter(thf -> thf.supportsThingType(thingTypeUID)).findFirst().orElse(null);
if (handlerFactory == null) {
if (modelLoaded) {
logger.info("No ThingHandlerFactory found for thing {} (thing-type is {}). Deferring initialization.",
thingUID, thingTypeUID);
}
return null;
}
String bundleName = getBundleName(handlerFactory);
if (bundleName == null || !loadedXmlThingTypes.contains(bundleName)) {
return null;
}
ThingType thingType = thingTypeRegistry.getThingType(thingTypeUID, localeProvider.getLocale());
ThingUID bridgeUID = thingDto.bridge != null ? new ThingUID(thingDto.bridge) : null;
Configuration configuration = new Configuration(thingDto.config);
ThingBuilder thingBuilder = thingDto.isBridge() ? BridgeBuilder.create(thingTypeUID, thingUID)
: ThingBuilder.create(thingTypeUID, thingUID);
thingBuilder
.withLabel(thingDto.label != null ? thingDto.label : (thingType != null ? thingType.getLabel() : null));
thingBuilder.withLocation(thingDto.location);
thingBuilder.withBridge(bridgeUID);
thingBuilder.withConfiguration(configuration);
List<Channel> channels = createChannels(thingTypeUID, thingUID,
thingDto.channels != null ? thingDto.channels : Map.of(),
thingType != null ? thingType.getChannelDefinitions() : List.of());
thingBuilder.withChannels(channels);
Thing thing = thingBuilder.build();
Thing thingFromHandler = handlerFactory.createThing(thingTypeUID, configuration, thingUID, bridgeUID);
if (thingFromHandler != null) {
mergeThing(thingFromHandler, thing);
logger.debug("Successfully loaded thing \'{}\'", thingUID);
} else {
// Possible cause: Asynchronous loading of the XML files
// Add the data to the queue in order to retry it later
logger.debug(
"ThingHandlerFactory \'{}\' claimed it can handle \'{}\' type but actually did not. Queued for later refresh.",
handlerFactory.getClass().getSimpleName(), thingTypeUID);
queue.add(new QueueContent(handlerFactory, thingTypeUID, configuration, thingUID, bridgeUID));
Thread thread = lazyRetryThread;
if (thread == null || !thread.isAlive()) {
thread = new Thread(lazyRetryRunnable);
lazyRetryThread = thread;
thread.start();
}
}
return thingFromHandler != null ? thingFromHandler : thing;
}
private List<Channel> createChannels(ThingTypeUID thingTypeUID, ThingUID thingUID,
Map<String, YamlChannelDTO> channelsDto, List<ChannelDefinition> channelDefinitions) {
Set<String> addedChannelIds = new HashSet<>();
List<Channel> channels = new ArrayList<>();
channelsDto.entrySet().forEach(entry -> {
YamlChannelDTO channelDto = entry.getValue();
ChannelTypeUID channelTypeUID = null;
ChannelKind kind = channelDto.getKind();
String itemType = channelDto.getItemType();
String label = channelDto.label;
AutoUpdatePolicy autoUpdatePolicy = null;
Configuration configuration = new Configuration(channelDto.config);
if (channelDto.type != null) {
channelTypeUID = new ChannelTypeUID(thingUID.getBindingId(), channelDto.type);
ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID,
localeProvider.getLocale());
if (channelType != null) {
kind = channelType.getKind();
itemType = channelType.getItemType();
if (label == null) {
label = channelType.getLabel();
}
autoUpdatePolicy = channelType.getAutoUpdatePolicy();
URI descUriO = channelType.getConfigDescriptionURI();
if (descUriO != null) {
ConfigUtil.applyDefaultConfiguration(configuration,
configDescriptionRegistry.getConfigDescription(descUriO));
}
} else {
logger.warn("Channel type {} could not be found.", channelTypeUID);
}
}
Channel channel = ChannelBuilder.create(new ChannelUID(thingUID, entry.getKey()), itemType).withKind(kind)
.withConfiguration(configuration).withType(channelTypeUID).withLabel(label)
.withAutoUpdatePolicy(autoUpdatePolicy).build();
channels.add(channel);
addedChannelIds.add(entry.getKey());
});
channelDefinitions.forEach(channelDef -> {
String id = channelDef.getId();
if (addedChannelIds.add(id)) {
ChannelType channelType = channelTypeRegistry.getChannelType(channelDef.getChannelTypeUID(),
localeProvider.getLocale());
if (channelType != null) {
Channel channel = ChannelBuilder.create(new ChannelUID(thingUID, id), channelType.getItemType())
.withType(channelDef.getChannelTypeUID())
.withAutoUpdatePolicy(channelType.getAutoUpdatePolicy()).build();
channels.add(channel);
} else {
logger.warn(
"Could not create channel '{}' for thing '{}', because channel type '{}' could not be found.",
id, thingUID, channelDef.getChannelTypeUID());
}
}
});
return channels;
}
private void mergeThing(Thing target, Thing source) {
target.setLabel(source.getLabel());
target.setLocation(source.getLocation());
target.setBridgeUID(source.getBridgeUID());
source.getConfiguration().keySet().forEach(paramName -> {
target.getConfiguration().put(paramName, source.getConfiguration().get(paramName));
});
List<Channel> channelsToAdd = new ArrayList<>();
source.getChannels().forEach(channel -> {
Channel targetChannel = target.getChannels().stream().filter(c -> c.getUID().equals(channel.getUID()))
.findFirst().orElse(null);
if (targetChannel != null) {
channel.getConfiguration().keySet().forEach(paramName -> {
targetChannel.getConfiguration().put(paramName, channel.getConfiguration().get(paramName));
});
} else {
channelsToAdd.add(channel);
}
});
// add the channels only defined in source list to the target list
ThingHelper.addChannelsToThing(target, channelsToAdd);
}
}

View File

@ -0,0 +1,143 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.model.yaml.internal.things.fileconverter;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.config.core.ConfigDescriptionRegistry;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.items.ItemUtil;
import org.openhab.core.model.yaml.YamlElement;
import org.openhab.core.model.yaml.YamlModelRepository;
import org.openhab.core.model.yaml.internal.things.YamlChannelDTO;
import org.openhab.core.model.yaml.internal.things.YamlThingDTO;
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.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;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* {@link YamlThingFileConverter} is the YAML file converter for {@link Thing} object.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
@Component(immediate = true, service = ThingFileGenerator.class)
public class YamlThingFileConverter extends AbstractThingFileGenerator {
private final YamlModelRepository modelRepository;
private final LocaleProvider localeProvider;
@Activate
public YamlThingFileConverter(final @Reference YamlModelRepository modelRepository,
final @Reference ThingTypeRegistry thingTypeRegistry,
final @Reference ChannelTypeRegistry channelTypeRegistry,
final @Reference ConfigDescriptionRegistry configDescRegistry,
final @Reference LocaleProvider localeProvider) {
super(thingTypeRegistry, channelTypeRegistry, configDescRegistry);
this.modelRepository = modelRepository;
this.localeProvider = localeProvider;
}
@Override
public String getFileFormatGenerator() {
return "YAML";
}
@Override
public synchronized void generateFileFormat(OutputStream out, List<Thing> things, boolean hideDefaultParameters) {
List<YamlElement> elements = new ArrayList<>();
things.forEach(thing -> {
elements.add(buildThingDTO(thing, hideDefaultParameters));
});
modelRepository.generateSyntaxFromElements(out, elements);
}
private YamlThingDTO buildThingDTO(Thing thing, boolean hideDefaultParameters) {
YamlThingDTO dto = new YamlThingDTO();
dto.uid = thing.getUID().getAsString();
dto.isBridge = thing instanceof Bridge ? true : null;
ThingUID bridgeUID = thing.getBridgeUID();
dto.bridge = bridgeUID == null ? null : bridgeUID.getAsString();
ThingType thingType = thingTypeRegistry.getThingType(thing.getThingTypeUID(), localeProvider.getLocale());
dto.label = thingType != null && thingType.getLabel().equals(thing.getLabel()) ? null : thing.getLabel();
dto.location = thing.getLocation();
Map<String, Object> config = new LinkedHashMap<>();
getConfigurationParameters(thing, hideDefaultParameters).forEach(param -> {
if (param.value() instanceof List<?> list) {
if (!list.isEmpty()) {
config.put(param.name(), param.value());
}
} else {
config.put(param.name(), param.value());
}
});
dto.config = config.isEmpty() ? null : config;
Map<String, YamlChannelDTO> channels = new LinkedHashMap<>();
getNonDefaultChannels(thing).forEach(channel -> {
channels.put(channel.getUID().getId(), buildChannelDTO(channel, hideDefaultParameters));
});
dto.channels = channels.isEmpty() ? null : channels;
return dto;
}
private YamlChannelDTO buildChannelDTO(Channel channel, boolean hideDefaultParameters) {
YamlChannelDTO dto = new YamlChannelDTO();
ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
if (channelTypeUID != null) {
dto.type = channelTypeUID.getId();
ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID, localeProvider.getLocale());
dto.label = channelType != null && channelType.getLabel().equals(channel.getLabel()) ? null
: channel.getLabel();
} else {
dto.kind = channel.getKind() == ChannelKind.STATE ? null : "trigger";
String itemType = channel.getAcceptedItemType();
dto.itemType = itemType != null ? ItemUtil.getMainItemType(itemType) : null;
dto.itemDimension = ItemUtil.getItemTypeExtension(itemType);
dto.label = channel.getLabel();
}
Map<String, Object> config = new LinkedHashMap<>();
getConfigurationParameters(channel, hideDefaultParameters).forEach(param -> {
if (param.value() instanceof List<?> list) {
if (!list.isEmpty()) {
config.put(param.name(), param.value());
}
} else {
config.put(param.name(), param.value());
}
});
dto.config = config.isEmpty() ? null : config;
return dto;
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.model.yaml.internal.util;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.types.util.UnitUtils;
import org.openhab.core.util.StringUtils;
/**
* Static utility methods that are helpful when dealing with YAML elements.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class YamlElementUtils {
private static final Set<String> VALID_ITEM_TYPES = Set.of(CoreItemFactory.SWITCH, CoreItemFactory.ROLLERSHUTTER,
CoreItemFactory.CONTACT, CoreItemFactory.STRING, CoreItemFactory.NUMBER, CoreItemFactory.DIMMER,
CoreItemFactory.DATETIME, CoreItemFactory.COLOR, CoreItemFactory.IMAGE, CoreItemFactory.PLAYER,
CoreItemFactory.LOCATION, CoreItemFactory.CALL);
public static boolean equalsConfig(@Nullable Map<String, Object> first, @Nullable Map<String, Object> second) {
if (first != null && second != null) {
if (first.size() != second.size()) {
return false;
} else {
return first.entrySet().stream().allMatch(e -> equalsConfigValue(e.getValue(), second.get(e.getKey())));
}
} else {
return first == null && second == null;
}
}
private static boolean equalsConfigValue(Object first, @Nullable Object second) {
return (first instanceof List firstList && second instanceof List secondList)
? Arrays.equals(firstList.toArray(), secondList.toArray())
: first.equals(second);
}
public static @Nullable String getAdjustedItemType(@Nullable String type) {
return type == null ? null : StringUtils.capitalize(type);
}
public static boolean isValidItemType(@Nullable String type) {
String adjustedType = getAdjustedItemType(type);
return adjustedType == null ? true : VALID_ITEM_TYPES.contains(adjustedType);
}
public static boolean isNumberItemType(@Nullable String type) {
return CoreItemFactory.NUMBER.equals(getAdjustedItemType(type));
}
public static @Nullable String getAdjustedItemDimension(@Nullable String dimension) {
return dimension == null ? null : StringUtils.capitalize(dimension);
}
public static boolean isValidItemDimension(@Nullable String dimension) {
String adjustedDimension = getAdjustedItemDimension(dimension);
if (adjustedDimension != null) {
try {
UnitUtils.parseDimension(adjustedDimension);
} catch (IllegalArgumentException e) {
return false;
}
}
return true;
}
public static @Nullable String getItemTypeWithDimension(@Nullable String type, @Nullable String dimension) {
String adjustedType = getAdjustedItemType(type);
String adjustedDimension = getAdjustedItemDimension(dimension);
return adjustedType != null ? adjustedType + (adjustedDimension == null ? "" : ":" + adjustedDimension) : null;
}
}

View File

@ -0,0 +1,263 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.model.yaml.internal.things;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
* The {@link YamlChannelDTOTest} contains tests for the {@link YamlChannelDTO} class.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class YamlChannelDTOTest {
@Test
public void testIsValid() throws IOException {
List<String> err = new ArrayList<>();
List<String> warn = new ArrayList<>();
YamlChannelDTO ch = new YamlChannelDTO();
assertFalse(ch.isValid(err, warn));
ch.type = "channel-type";
assertTrue(ch.isValid(err, warn));
ch.type = "$channel-type";
assertFalse(ch.isValid(err, warn));
ch.type = null;
ch.itemType = "String";
assertTrue(ch.isValid(err, warn));
ch.itemType = "string";
assertTrue(ch.isValid(err, warn));
ch.itemType = "Other";
assertFalse(ch.isValid(err, warn));
ch.itemType = "Number";
assertTrue(ch.isValid(err, warn));
ch.itemDimension = "Dimensionless";
assertTrue(ch.isValid(err, warn));
ch.itemType = "number";
ch.itemDimension = "dimensionless";
assertTrue(ch.isValid(err, warn));
ch.itemDimension = "Other";
assertFalse(ch.isValid(err, warn));
ch.itemType = "Color";
ch.itemDimension = null;
ch.kind = "wrong";
assertTrue(ch.isValid(err, warn));
}
@Test
public void testEquals() throws IOException {
YamlChannelDTO ch1 = new YamlChannelDTO();
YamlChannelDTO ch2 = new YamlChannelDTO();
ch1.type = "channel-type";
ch2.type = "channel-type";
assertTrue(ch1.equals(ch2));
ch1.type = null;
ch1.itemType = "String";
ch2.type = null;
ch2.itemType = "String";
assertTrue(ch1.equals(ch2));
ch1.kind = "trigger";
ch2.kind = "TRIGGER";
assertTrue(ch1.equals(ch2));
ch1.label = "A label";
ch2.label = "A label";
assertTrue(ch1.equals(ch2));
ch1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
ch2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
assertTrue(ch1.equals(ch2));
}
@Test
public void testEqualsWithTypeOrItemType() throws IOException {
YamlChannelDTO ch1 = new YamlChannelDTO();
YamlChannelDTO ch2 = new YamlChannelDTO();
ch1.type = "channel-type";
ch2.type = "channel-type";
assertTrue(ch1.equals(ch2));
ch2.type = "channel-type2";
assertFalse(ch1.equals(ch2));
ch1.type = null;
ch1.itemType = "String";
ch2.type = null;
ch2.itemType = "String";
assertTrue(ch1.equals(ch2));
ch1.itemType = "String";
ch2.itemType = "Number";
assertFalse(ch1.equals(ch2));
ch1.itemType = "Number";
ch2.itemType = "Number";
assertTrue(ch1.equals(ch2));
ch2.itemType = "number";
assertTrue(ch1.equals(ch2));
ch1.itemDimension = "Temperature";
assertFalse(ch1.equals(ch2));
ch2.itemDimension = "Humidity";
assertFalse(ch1.equals(ch2));
ch2.itemDimension = "Temperature";
assertTrue(ch1.equals(ch2));
ch2.itemDimension = "temperature";
assertTrue(ch1.equals(ch2));
ch1.type = "channel-type";
ch1.itemType = null;
ch1.itemDimension = null;
ch2.type = null;
ch2.itemType = "String";
ch2.itemDimension = null;
assertFalse(ch1.equals(ch2));
ch1.type = null;
ch1.itemType = "String";
ch2.type = "channel-type";
ch2.itemType = null;
assertFalse(ch1.equals(ch2));
ch1.type = null;
ch1.itemType = "Switch";
ch1.kind = "state";
ch2.type = null;
ch2.itemType = "Switch";
ch2.kind = "state";
assertTrue(ch1.equals(ch2));
ch1.kind = "trigger";
ch2.kind = "trigger";
assertTrue(ch1.equals(ch2));
ch1.kind = "trigger";
ch2.kind = "Trigger";
assertTrue(ch1.equals(ch2));
ch1.kind = "Trigger";
ch2.kind = "TRIGGER";
assertTrue(ch1.equals(ch2));
ch1.kind = "state";
ch2.kind = "trigger";
assertFalse(ch1.equals(ch2));
ch1.kind = "trigger";
ch2.kind = "state";
assertFalse(ch1.equals(ch2));
ch1.kind = null;
ch2.kind = "trigger";
assertFalse(ch1.equals(ch2));
ch1.kind = "trigger";
ch2.kind = null;
assertFalse(ch1.equals(ch2));
ch1.kind = null;
ch2.kind = "state";
assertTrue(ch1.equals(ch2));
ch1.kind = "state";
ch2.kind = null;
assertTrue(ch1.equals(ch2));
}
@Test
public void testEqualsWithLabel() throws IOException {
YamlChannelDTO ch1 = new YamlChannelDTO();
YamlChannelDTO ch2 = new YamlChannelDTO();
ch1.itemType = "String";
ch2.itemType = "String";
ch1.label = null;
ch2.label = null;
assertTrue(ch1.equals(ch2));
ch1.label = "A label";
ch2.label = null;
assertFalse(ch1.equals(ch2));
ch1.label = null;
ch2.label = "A label";
assertFalse(ch1.equals(ch2));
ch1.label = "A label";
ch2.label = "A different label";
assertFalse(ch1.equals(ch2));
ch1.label = "A label";
ch2.label = "A label";
assertTrue(ch1.equals(ch2));
}
@Test
public void testEqualsWithConfigurations() throws IOException {
YamlChannelDTO ch1 = new YamlChannelDTO();
YamlChannelDTO ch2 = new YamlChannelDTO();
ch1.type = "channel-type";
ch2.type = "channel-type";
ch1.config = null;
ch2.config = null;
assertTrue(ch1.equals(ch2));
ch1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
ch2.config = null;
assertFalse(ch1.equals(ch2));
ch1.config = null;
ch2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
assertFalse(ch1.equals(ch2));
ch1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
ch2.config = Map.of("param1", "other value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
assertFalse(ch1.equals(ch2));
ch1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
ch2.config = Map.of("param1", "value", "param2", 25, "param3", true, "param4", List.of("val 1", "val 2"));
assertFalse(ch1.equals(ch2));
ch1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
ch2.config = Map.of("param1", "value", "param2", 50, "param3", false, "param4", List.of("val 1", "val 2"));
assertFalse(ch1.equals(ch2));
ch1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
ch2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "value 2"));
assertFalse(ch1.equals(ch2));
ch1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
ch2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1"));
assertFalse(ch1.equals(ch2));
ch1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
ch2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", 75);
assertFalse(ch1.equals(ch2));
ch1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
ch2.config = Map.of("param1", "value", "param2", 50, "param3", true);
assertFalse(ch1.equals(ch2));
ch1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
ch2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
assertTrue(ch1.equals(ch2));
}
}

View File

@ -0,0 +1,321 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.model.yaml.internal.things;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
* The {@link YamlThingDTOTest} contains tests for the {@link YamlThingDTO} class.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public class YamlThingDTOTest {
@Test
public void testIsValid() throws IOException {
YamlThingDTO th = new YamlThingDTO();
assertFalse(th.isValid(null, null));
th.uid = "id";
assertFalse(th.isValid(null, null));
th.uid = "binding:id";
assertFalse(th.isValid(null, null));
th.uid = "binding:type:@id";
assertFalse(th.isValid(null, null));
th.uid = "binding:type:id";
assertTrue(th.isValid(null, null));
th.uid = "binding:type:$idBridge:id";
assertFalse(th.isValid(null, null));
th.uid = "binding:type:idBridge:id";
assertTrue(th.isValid(null, null));
th.bridge = "idBridge";
assertFalse(th.isValid(null, null));
th.bridge = "binding:idBridge";
assertFalse(th.isValid(null, null));
th.bridge = "binding:type:-idBridge";
assertFalse(th.isValid(null, null));
th.bridge = "binding:type:idBridge";
assertTrue(th.isValid(null, null));
YamlChannelDTO ch = new YamlChannelDTO();
th.channels = Map.of("channel", ch);
assertFalse(th.isValid(null, null));
ch.type = "channel-type";
th.channels = Map.of("channel", ch);
assertTrue(th.isValid(null, null));
th.channels = Map.of("channel@name", ch);
assertFalse(th.isValid(null, null));
th.channels = Map.of("group#channel", ch);
assertTrue(th.isValid(null, null));
}
@Test
public void testEquals() throws IOException {
YamlThingDTO th1 = new YamlThingDTO();
YamlThingDTO th2 = new YamlThingDTO();
th1.uid = "binding:type:id";
th2.uid = "binding:type:id2";
assertFalse(th1.equals(th2));
th2.uid = "binding:type:id";
assertTrue(th1.equals(th2));
th1.isBridge = true;
th2.isBridge = true;
assertTrue(th1.equals(th2));
th1.bridge = "binding:bridge:idBridge";
th2.bridge = "binding:bridge:idBridge";
assertTrue(th1.equals(th2));
th1.label = "A label";
th2.label = "A label";
assertTrue(th1.equals(th2));
th1.location = "A location";
th2.location = "A location";
assertTrue(th1.equals(th2));
th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
th2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
assertTrue(th1.equals(th2));
YamlChannelDTO ch1 = new YamlChannelDTO();
ch1.type = "channel-type";
YamlChannelDTO ch2 = new YamlChannelDTO();
ch2.type = "channel-type";
th1.channels = Map.of("channel", ch1);
th2.channels = Map.of("channel", ch2);
assertTrue(th1.equals(th2));
}
@Test
public void testEqualsWithLabel() throws IOException {
YamlThingDTO th1 = new YamlThingDTO();
YamlThingDTO th2 = new YamlThingDTO();
th1.uid = "binding:type:id";
th2.uid = "binding:type:id";
th1.label = null;
th2.label = null;
assertTrue(th1.equals(th2));
th1.label = "A label";
th2.label = null;
assertFalse(th1.equals(th2));
th1.label = null;
th2.label = "A label";
assertFalse(th1.equals(th2));
th1.label = "A label";
th2.label = "A different label";
assertFalse(th1.equals(th2));
th1.label = "A label";
th2.label = "A label";
assertTrue(th1.equals(th2));
}
@Test
public void testEqualsWithLocation() throws IOException {
YamlThingDTO th1 = new YamlThingDTO();
YamlThingDTO th2 = new YamlThingDTO();
th1.uid = "binding:type:id";
th2.uid = "binding:type:id";
th1.location = null;
th2.location = null;
assertTrue(th1.equals(th2));
th1.location = "A location";
th2.location = null;
assertFalse(th1.equals(th2));
th1.location = null;
th2.location = "A location";
assertFalse(th1.equals(th2));
th1.location = "A location";
th2.location = "A different location";
assertFalse(th1.equals(th2));
th1.location = "A location";
th2.location = "A location";
assertTrue(th1.equals(th2));
}
@Test
public void testEqualsWithIsBridge() throws IOException {
YamlThingDTO th1 = new YamlThingDTO();
YamlThingDTO th2 = new YamlThingDTO();
th1.uid = "binding:type:id";
th2.uid = "binding:type:id";
th1.isBridge = null;
th2.isBridge = null;
assertTrue(th1.equals(th2));
th1.isBridge = false;
th2.isBridge = true;
assertFalse(th1.equals(th2));
th1.isBridge = true;
th2.isBridge = false;
assertFalse(th1.equals(th2));
th1.isBridge = true;
th2.isBridge = null;
assertFalse(th1.equals(th2));
th1.isBridge = null;
th2.isBridge = true;
assertFalse(th1.equals(th2));
th1.isBridge = false;
th2.isBridge = null;
assertTrue(th1.equals(th2));
th1.isBridge = null;
th2.isBridge = false;
assertTrue(th1.equals(th2));
th1.isBridge = false;
th2.isBridge = false;
assertTrue(th1.equals(th2));
th1.isBridge = true;
th2.isBridge = true;
assertTrue(th1.equals(th2));
}
@Test
public void testEqualsWithBridge() throws IOException {
YamlThingDTO th1 = new YamlThingDTO();
YamlThingDTO th2 = new YamlThingDTO();
th1.uid = "binding:type:id";
th2.uid = "binding:type:id";
th1.bridge = null;
th2.bridge = null;
assertTrue(th1.equals(th2));
th1.bridge = "binding:bridge:idBridge";
th2.bridge = null;
assertFalse(th1.equals(th2));
th1.bridge = null;
th2.bridge = "binding:bridge:idBridge";
assertFalse(th1.equals(th2));
th1.bridge = "binding:bridge:idBridge";
th2.bridge = "binding:bridge:idBridge2";
assertFalse(th1.equals(th2));
th1.bridge = "binding:bridge:idBridge";
th2.bridge = "binding:bridge:idBridge";
assertTrue(th1.equals(th2));
}
@Test
public void testEqualsWithConfigurations() throws IOException {
YamlThingDTO th1 = new YamlThingDTO();
YamlThingDTO th2 = new YamlThingDTO();
th1.uid = "binding:type:id";
th2.uid = "binding:type:id";
th1.config = null;
th2.config = null;
assertTrue(th1.equals(th2));
th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
th2.config = null;
assertFalse(th1.equals(th2));
th1.config = null;
th2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
assertFalse(th1.equals(th2));
th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
th2.config = Map.of("param1", "other value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
assertFalse(th1.equals(th2));
th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
th2.config = Map.of("param1", "value", "param2", 25, "param3", true, "param4", List.of("val 1", "val 2"));
assertFalse(th1.equals(th2));
th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
th2.config = Map.of("param1", "value", "param2", 50, "param3", false, "param4", List.of("val 1", "val 2"));
assertFalse(th1.equals(th2));
th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
th2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "value 2"));
assertFalse(th1.equals(th2));
th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
th2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1"));
assertFalse(th1.equals(th2));
th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
th2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", 75);
assertFalse(th1.equals(th2));
th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
th2.config = Map.of("param1", "value", "param2", 50, "param3", true);
assertFalse(th1.equals(th2));
th1.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
th2.config = Map.of("param1", "value", "param2", 50, "param3", true, "param4", List.of("val 1", "val 2"));
assertTrue(th1.equals(th2));
}
@Test
public void testEqualsWithChannels() throws IOException {
YamlThingDTO th1 = new YamlThingDTO();
th1.uid = "binding:type:id";
YamlThingDTO th2 = new YamlThingDTO();
th2.uid = "binding:type:id";
YamlChannelDTO ch1 = new YamlChannelDTO();
ch1.type = "channel-type";
YamlChannelDTO ch2 = new YamlChannelDTO();
ch2.type = "channel-other-type";
YamlChannelDTO ch3 = new YamlChannelDTO();
ch3.type = "channel-type";
YamlChannelDTO ch4 = new YamlChannelDTO();
ch4.type = "channel-other-type";
th1.channels = Map.of("channel1", ch1);
th2.channels = Map.of("channel1", ch3);
assertTrue(th1.equals(th2));
th1.channels = Map.of("channel1", ch1, "channel2", ch2);
th2.channels = Map.of("channel1", ch3, "channel2", ch4);
assertTrue(th1.equals(th2));
th1.channels = Map.of("channel1", ch1);
th2.channels = null;
assertFalse(th1.equals(th2));
th1.channels = null;
th2.channels = Map.of("channel1", ch3);
assertFalse(th1.equals(th2));
th1.channels = Map.of("channel1", ch1, "channel2", ch2);
th2.channels = Map.of("channel1", ch3);
assertFalse(th1.equals(th2));
th1.channels = Map.of("channel1", ch1);
th2.channels = Map.of("channel1", ch4);
assertFalse(th1.equals(th2));
th1.channels = Map.of("channel", ch1);
th2.channels = Map.of("channel1", ch3);
assertFalse(th1.equals(th2));
}
}

View File

@ -12,9 +12,11 @@
*/
package org.openhab.core.model.yaml.test;
import java.util.List;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.model.yaml.YamlElement;
import org.openhab.core.model.yaml.YamlElementName;
@ -59,7 +61,7 @@ public class FirstTypeDTO implements YamlElement, Cloneable {
}
@Override
public boolean isValid() {
public boolean isValid(@Nullable List<@NonNull String> errors, @Nullable List<@NonNull String> warnings) {
return uid != null && !uid.isBlank();
}

View File

@ -12,9 +12,11 @@
*/
package org.openhab.core.model.yaml.test;
import java.util.List;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.model.yaml.YamlElement;
import org.openhab.core.model.yaml.YamlElementName;
@ -59,7 +61,7 @@ public class SecondTypeDTO implements YamlElement, Cloneable {
}
@Override
public boolean isValid() {
public boolean isValid(@Nullable List<@NonNull String> errors, @Nullable List<@NonNull String> warnings) {
return id != null && !id.isBlank();
}

View File

@ -44,9 +44,9 @@ import org.osgi.service.component.annotations.Activate;
@NonNullByDefault
public abstract class AbstractThingFileGenerator implements ThingFileGenerator {
private final ThingTypeRegistry thingTypeRegistry;
private final ChannelTypeRegistry channelTypeRegistry;
private final ConfigDescriptionRegistry configDescRegistry;
protected final ThingTypeRegistry thingTypeRegistry;
protected final ChannelTypeRegistry channelTypeRegistry;
protected final ConfigDescriptionRegistry configDescRegistry;
@Activate
public AbstractThingFileGenerator(ThingTypeRegistry thingTypeRegistry, ChannelTypeRegistry channelTypeRegistry,