YAML configuration: add support for things (#4691)
Related to #3666 Signed-off-by: Laurent Garnier <lg.hc@free.fr>pull/4730/head
parent
1c955f3cb3
commit
b7b23db9a7
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue