diff --git a/tools/pom.xml b/tools/pom.xml
index 14eab0fef..d427db4c4 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -19,6 +19,7 @@
archetype
i18n-plugin
+ upgradetool
diff --git a/tools/upgradetool/pom.xml b/tools/upgradetool/pom.xml
new file mode 100644
index 000000000..874ed8d18
--- /dev/null
+++ b/tools/upgradetool/pom.xml
@@ -0,0 +1,123 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.tools
+ org.openhab.core.reactor.tools
+ 4.0.0-SNAPSHOT
+
+
+ upgradetool
+
+ jar
+
+ openHAB Core :: Tools :: Upgrade tool
+ A tool for upgrading openHAB from 3.4 to 4.0
+
+
+
+ org.openhab.core.bundles
+ org.openhab.core
+ ${project.version}
+
+
+ org.openhab.core.bundles
+ org.openhab.core.thing
+ ${project.version}
+
+
+ org.openhab.core.bundles
+ org.openhab.core.storage.json
+ ${project.version}
+
+
+ commons-cli
+ commons-cli
+ 1.5.0
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j.version}
+
+
+ com.google.code.gson
+ gson
+ 2.9.1
+
+
+ javax.measure
+ unit-api
+ 2.1.3
+
+
+ si.uom
+ si-units
+ 2.1
+
+
+ tech.units
+ indriya
+ 2.1.2
+
+
+ org.eclipse.jdt
+ org.eclipse.jdt.annotation
+ 2.2.600
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ 3.3.0
+
+
+ unpack-eea
+
+ unpack
+
+
+
+
+ org.lastnpe.eea
+ eea-all
+ ${eea.version}
+ true
+
+
+
+
+
+
+
+ maven-assembly-plugin
+ 3.4.2
+
+
+
+ org.openhab.core.tools.UpgradeTool
+
+
+
+ jar-with-dependencies
+
+
+
+
+ make-assembly
+
+ single
+
+ package
+
+
+
+
+
+
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
new file mode 100644
index 000000000..19f009cd0
--- /dev/null
+++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/UpgradeTool.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2010-2023 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;
+
+import static org.openhab.core.tools.internal.Upgrader.*;
+
+import java.util.Set;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.tools.internal.Upgrader;
+
+/**
+ * The {@link UpgradeTool} is a tool for upgrading openHAB to mitigate breaking changes
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class UpgradeTool {
+ private static final Set LOG_LEVELS = Set.of("TRACE", "DEBUG", "INFO", "WARN", "ERROR");
+ private static final String OPT_COMMAND = "command";
+ private static final String OPT_DIR = "dir";
+ private static final String OPT_LOG = "log";
+ private static final String OPT_FORCE = "force";
+
+ private static Options getOptions() {
+ Options options = new Options();
+
+ options.addOption(
+ Option.builder().longOpt(OPT_DIR).desc("directory to process").numberOfArgs(1).required().build());
+ options.addOption(Option.builder().longOpt(OPT_COMMAND).numberOfArgs(1).desc("command to execute").build());
+ options.addOption(Option.builder().longOpt(OPT_LOG).numberOfArgs(1).desc("log verbosity").build());
+ options.addOption(Option.builder().longOpt(OPT_FORCE).desc("force execution (even if already done)").build());
+
+ return options;
+ }
+
+ public static void main(String[] args) {
+ Options options = getOptions();
+ try {
+ CommandLine commandLine = new DefaultParser().parse(options, args);
+
+ String loglevel = commandLine.hasOption(OPT_LOG) ? commandLine.getOptionValue(OPT_LOG).toUpperCase()
+ : "INFO";
+ if (!LOG_LEVELS.contains(loglevel)) {
+ System.out.println("Allowed log-levels are " + LOG_LEVELS);
+ System.exit(0);
+ }
+
+ System.setProperty(org.slf4j.impl.SimpleLogger.DEFAULT_LOG_LEVEL_KEY, loglevel);
+
+ String baseDir = commandLine.hasOption(OPT_DIR) ? commandLine.getOptionValue(OPT_DIR) : "";
+ boolean force = commandLine.hasOption(OPT_FORCE) ? true : false;
+
+ Upgrader upgrader = new Upgrader(baseDir, force);
+ if (commandLine.hasOption(ITEM_COPY_UNIT_TO_METADATA)) {
+ upgrader.itemCopyUnitToMetadata();
+ } else if (commandLine.hasOption(LINK_UPGRADE_JS_PROFILE)) {
+ upgrader.linkUpgradeJsProfile();
+ }
+ } catch (ParseException e) {
+ HelpFormatter formatter = new HelpFormatter();
+ String commands = Set.of(ITEM_COPY_UNIT_TO_METADATA, LINK_UPGRADE_JS_PROFILE).toString();
+ formatter.printHelp("upgradetool", "", options, "Available commands: " + commands, true);
+ }
+
+ System.exit(0);
+ }
+}
diff --git a/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/Upgrader.java b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/Upgrader.java
new file mode 100644
index 000000000..0c896ad24
--- /dev/null
+++ b/tools/upgradetool/src/main/java/org/openhab/core/tools/internal/Upgrader.java
@@ -0,0 +1,171 @@
+/**
+ * Copyright (c) 2010-2023 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.nio.file.Files;
+import java.nio.file.Path;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Objects;
+
+import javax.measure.Unit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.items.ManagedItemProvider;
+import org.openhab.core.items.Metadata;
+import org.openhab.core.items.MetadataKey;
+import org.openhab.core.storage.json.internal.JsonStorage;
+import org.openhab.core.thing.internal.link.ItemChannelLinkConfigDescriptionProvider;
+import org.openhab.core.thing.link.ItemChannelLink;
+import org.openhab.core.types.util.UnitUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Upgrader} contains the implementation of the upgrade methods
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class Upgrader {
+ public static final String ITEM_COPY_UNIT_TO_METADATA = "itemCopyUnitToMetadata";
+ public static final String LINK_UPGRADE_JS_PROFILE = "linkUpgradeJsProfile";
+
+ private final Logger logger = LoggerFactory.getLogger(Upgrader.class);
+ private final String baseDir;
+ private final boolean force;
+ private final JsonStorage upgradeRecords;
+
+ public Upgrader(String baseDir, boolean force) {
+ this.baseDir = baseDir;
+ this.force = force;
+
+ Path upgradeJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.tools.UpgradeTool");
+ upgradeRecords = new JsonStorage<>(upgradeJsonDatabasePath.toFile(), null, 5, 0, 0, List.of());
+ }
+
+ private boolean checkUpgradeRecord(String key) {
+ UpgradeRecord upgradeRecord = upgradeRecords.get(key);
+ if (upgradeRecord != null && !force) {
+ logger.info("Already executed '{}' on {}. Use '--force' to execute it again.", key,
+ upgradeRecord.executionDate);
+ return false;
+ }
+ return true;
+ }
+
+ public void itemCopyUnitToMetadata() {
+ if (checkUpgradeRecord(ITEM_COPY_UNIT_TO_METADATA)) {
+ return;
+ }
+ Path itemJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.items.Item.json");
+ Path metadataJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.items.Metadata.json");
+ logger.info("Copying item unit from state description to metadata in database '{}'", itemJsonDatabasePath);
+
+ if (!Files.isReadable(itemJsonDatabasePath)) {
+ logger.error("Cannot access item database '{}', check path and access rights.", itemJsonDatabasePath);
+ return;
+ }
+ if (!Files.isWritable(metadataJsonDatabasePath)) {
+ logger.error("Cannot access metadata database '{}', check path and access rights.",
+ metadataJsonDatabasePath);
+ return;
+ }
+
+ JsonStorage itemStorage = new JsonStorage<>(itemJsonDatabasePath.toFile(),
+ null, 5, 0, 0, List.of());
+ JsonStorage metadataStorage = new JsonStorage<>(metadataJsonDatabasePath.toFile(), null, 5, 0, 0,
+ List.of());
+
+ itemStorage.getKeys().forEach(itemName -> {
+ ManagedItemProvider.PersistedItem item = itemStorage.get(itemName);
+ if (item != null && item.itemType.startsWith("Number:")) {
+ if (metadataStorage.containsKey("unit" + ":" + itemName)) {
+ logger.debug("{}: already contains a 'unit' metadata, skipping it", itemName);
+ } else {
+ Metadata metadata = metadataStorage.get("stateDescription:" + itemName);
+ if (metadata == null) {
+ logger.debug("{}: Nothing to do, no state description found.", itemName);
+ } else {
+ String pattern = (String) metadata.getConfiguration().get("pattern");
+ if (pattern.contains(UnitUtils.UNIT_PLACEHOLDER)) {
+ logger.warn(
+ "{}: State description contains unit place-holder '%unit%', check if 'unit' metadata is needed!",
+ itemName);
+ } else {
+ Unit> stateDescriptionUnit = UnitUtils.parseUnit(pattern);
+ if (stateDescriptionUnit != null) {
+ String unit = stateDescriptionUnit.toString();
+ MetadataKey defaultUnitMetadataKey = new MetadataKey("unit", itemName);
+ Metadata defaultUnitMetadata = new Metadata(defaultUnitMetadataKey, unit, null);
+ metadataStorage.put(defaultUnitMetadataKey.toString(), defaultUnitMetadata);
+ logger.info("{}: Wrote 'unit={}' to metadata.", itemName, unit);
+ }
+ }
+ }
+ }
+ }
+ });
+
+ metadataStorage.flush();
+ upgradeRecords.put(ITEM_COPY_UNIT_TO_METADATA, new UpgradeRecord(ZonedDateTime.now()));
+ }
+
+ public void linkUpgradeJsProfile() {
+ if (checkUpgradeRecord(LINK_UPGRADE_JS_PROFILE)) {
+ return;
+ }
+
+ Path linkJsonDatabasePath = Path.of(baseDir, "jsondb", "org.openhab.core.thing.link.ItemChannelLink.json");
+ logger.info("Upgrading JS profile configuration in database '{}'", linkJsonDatabasePath);
+
+ if (!Files.isWritable(linkJsonDatabasePath)) {
+ logger.error("Cannot access link database '{}', check path and access rights.", linkJsonDatabasePath);
+ return;
+ }
+ JsonStorage linkStorage = new JsonStorage<>(linkJsonDatabasePath.toFile(), null, 5, 0, 0,
+ List.of());
+
+ List.copyOf(linkStorage.getKeys()).forEach(linkUid -> {
+ ItemChannelLink link = Objects.requireNonNull(linkStorage.get(linkUid));
+ Configuration configuration = link.getConfiguration();
+ String profileName = (String) configuration.get(ItemChannelLinkConfigDescriptionProvider.PARAM_PROFILE);
+ if ("transform:JS".equals(profileName)) {
+ String function = (String) configuration.get("function");
+ if (function != null) {
+ configuration.put("toItemScript", function);
+ configuration.put("toHandlerScript", "|input");
+ configuration.remove("function");
+ configuration.remove("sourceFormat");
+
+ linkStorage.put(linkUid, link);
+ logger.info("{}: rewrote JS profile link to new format", linkUid);
+ } else {
+ logger.info("{}: link already has correct configuration", linkUid);
+ }
+ }
+ });
+
+ linkStorage.flush();
+ upgradeRecords.put(LINK_UPGRADE_JS_PROFILE, new UpgradeRecord(ZonedDateTime.now()));
+ }
+
+ private static class UpgradeRecord {
+ public final ZonedDateTime executionDate;
+
+ public UpgradeRecord(ZonedDateTime executionDate) {
+ this.executionDate = executionDate;
+ }
+ }
+}