diff --git a/tools/upgradetool/pom.xml b/tools/upgradetool/pom.xml
index 8a68d763ba..e9d064227a 100644
--- a/tools/upgradetool/pom.xml
+++ b/tools/upgradetool/pom.xml
@@ -17,6 +17,10 @@
openHAB Core :: Tools :: Upgrade tool
A tool for upgrading openHAB
+
+ 2.18.2
+
+
org.openhab.core.bundles
@@ -73,6 +77,18 @@
org.eclipse.jdt.annotation
2.2.600
+
+ com.fasterxml.jackson.core
+ jackson-core
+ ${jackson.version}
+ compile
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-yaml
+ ${jackson.version}
+ compile
+
diff --git a/tools/upgradetool/src/main/java/org/openhab/core/tools/UpgradeTool.java b/tools/upgradetool/src/main/java/org/openhab/core/tools/UpgradeTool.java
index a72c8b199c..2d419d84ee 100644
--- a/tools/upgradetool/src/main/java/org/openhab/core/tools/UpgradeTool.java
+++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/UpgradeTool.java
@@ -50,7 +50,8 @@ public class UpgradeTool {
private static final List UPGRADERS = List.of( //
new ItemUnitToMetadataUpgrader(), //
new JSProfileUpgrader(), //
- new ScriptProfileUpgrader() //
+ new ScriptProfileUpgrader(), //
+ new YamlConfigurationV1TagsUpgrader() // Added in 5.0
);
private static final Logger logger = LoggerFactory.getLogger(UpgradeTool.class);
diff --git a/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/YamlConfigurationV1TagsUpgrader.java b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/YamlConfigurationV1TagsUpgrader.java
new file mode 100644
index 0000000000..76bbfe4334
--- /dev/null
+++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/YamlConfigurationV1TagsUpgrader.java
@@ -0,0 +1,195 @@
+/*
+ * 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.tools.internal;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.tools.Upgrader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
+import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
+
+/**
+ * The {@link YamlConfigurationV1TagsUpgrader} upgrades YAML Tags Configuration from List to Map.
+ *
+ * Convert list to map format for tags in V1 configuration files.
+ *
+ * Input file criteria:
+ * - Search only in CONF/tags/, or in the given directory, and its subdirectories
+ * - Contains a version key with value 1
+ * - it must contain a tags key that is a list
+ * - The tags list must contain a uid key
+ * - If the above criteria are not met, the file will not be modified
+ *
+ * Output file will
+ * - Retain `version: 1`
+ * - convert tags list to a map with uid as key and the rest as map
+ * - Preserve the order of the tags
+ * - other keys will be unchanged
+ * - A backup of the original file will be created with the extension `.yaml.org`
+ * - If an .org file already exists, append a number to the end, e.g. `.org.1`
+ *
+ * @since 5.0.0
+ *
+ * @author Jimmy Tanagra - Initial contribution
+ */
+@NonNullByDefault
+public class YamlConfigurationV1TagsUpgrader implements Upgrader {
+ private static final String VERSION = "version";
+
+ private final Logger logger = LoggerFactory.getLogger(YamlConfigurationV1TagsUpgrader.class);
+
+ private final YAMLFactory yamlFactory;
+ private final ObjectMapper objectMapper;
+
+ public YamlConfigurationV1TagsUpgrader() {
+ // match the options used in {@link YamlModelRepositoryImpl}
+ yamlFactory = YAMLFactory.builder() //
+ .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) // omit "---" at file start
+ .disable(YAMLGenerator.Feature.SPLIT_LINES) // do not split long lines
+ .enable(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR) // indent arrays
+ .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES) // use quotes only where necessary
+ .enable(YAMLParser.Feature.PARSE_BOOLEAN_LIKE_WORDS_AS_STRINGS).build(); // do not parse ON/OFF/... as
+ // booleans
+ objectMapper = new ObjectMapper(yamlFactory);
+ objectMapper.findAndRegisterModules();
+ objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
+ objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
+ objectMapper.setSerializationInclusion(Include.NON_NULL);
+ objectMapper.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
+ }
+
+ @Override
+ public String getName() {
+ return "yamlTagsListToMap";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Upgrade YAML 'tags' list to map format on V1 configuration files";
+ }
+
+ @Override
+ public boolean execute(String userdataDir, String confDir) {
+ String confEnv = System.getenv("OPENHAB_CONF");
+ // If confDir is set to OPENHAB_CONF, look inside /tags/ subdirectory
+ // otherwise use the given confDir as is
+ if (confEnv != null && !confEnv.isBlank() && confEnv.equals(confDir)) {
+ confDir = Path.of(confEnv, "tags").toString();
+ }
+ Path configPath = Path.of(confDir).toAbsolutePath();
+ logger.info("Upgrading YAML tags configurations in '{}'", configPath);
+
+ try {
+ Files.walkFileTree(configPath, new SimpleFileVisitor<>() {
+ @Override
+ public FileVisitResult visitFile(@NonNullByDefault({}) Path file,
+ @NonNullByDefault({}) BasicFileAttributes attrs) throws IOException {
+ if (attrs.isRegularFile()) {
+ Path relativePath = configPath.relativize(file);
+ String modelName = relativePath.toString();
+ if (!relativePath.startsWith("automation") && modelName.endsWith(".yaml")) {
+ logger.info("Checking {}", file);
+ convertTagsListToMap(file);
+ }
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFileFailed(@NonNullByDefault({}) Path file,
+ @NonNullByDefault({}) IOException exc) throws IOException {
+ logger.warn("Failed to process {}: {}", file.toAbsolutePath(), exc.getClass().getSimpleName());
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ } catch (IOException e) {
+ logger.error("Failed to walk through the directory {}: {}", configPath, e.getMessage());
+ return false;
+ }
+ return true;
+ }
+
+ private void convertTagsListToMap(Path filePath) {
+ try {
+ JsonNode fileContent = objectMapper.readTree(filePath.toFile());
+
+ JsonNode versionNode = fileContent.get(VERSION);
+ if (versionNode == null || !versionNode.canConvertToInt() || versionNode.asInt() != 1) {
+ return;
+ }
+
+ JsonNode tagsNode = fileContent.get("tags");
+ if (tagsNode == null || !tagsNode.isArray()) {
+ return;
+ }
+
+ logger.info("Found v1 yaml file with tags list {}", filePath);
+ fileContent.properties().forEach(entry -> {
+ String key = entry.getKey();
+ JsonNode node = entry.getValue();
+ if (key.equals("tags")) {
+ ObjectNode tagsMap = objectMapper.createObjectNode();
+ for (JsonNode tag : node) {
+ if (tag.hasNonNull("uid")) {
+ String uid = tag.get("uid").asText();
+ ((ObjectNode) tag).remove("uid");
+ tagsMap.set(uid, tag);
+ } else {
+ logger.warn("Tag {} does not have a uid, skipping", tag);
+ }
+ }
+ ((ObjectNode) fileContent).set(key, tagsMap);
+ }
+ });
+
+ String output = objectMapper.writeValueAsString(fileContent);
+ saveFile(filePath, output);
+ } catch (IOException e) {
+ logger.error("Failed to read YAML file {}: {}", filePath, e.getMessage());
+ return;
+ }
+ }
+
+ private void saveFile(Path filePath, String content) {
+ Path backupPath = filePath.resolveSibling(filePath.getFileName() + ".org");
+ int i = 1;
+ while (Files.exists(backupPath)) {
+ backupPath = filePath.resolveSibling(filePath.getFileName() + ".org." + i);
+ i++;
+ }
+ try {
+ Files.move(filePath, backupPath);
+ Files.writeString(filePath, content);
+ logger.info("Converted {} to map format, and the original file saved as {}", filePath, backupPath);
+ } catch (IOException e) {
+ logger.error("Failed to save YAML file {}: {}", filePath, e.getMessage());
+ }
+ }
+}