Skip to content

UpgradeTool add semantics upgrader and OH version#5379

Open
mherwege wants to merge 21 commits intoopenhab:mainfrom
mherwege:upgrade_semantics
Open

UpgradeTool add semantics upgrader and OH version#5379
mherwege wants to merge 21 commits intoopenhab:mainfrom
mherwege:upgrade_semantics

Conversation

@mherwege
Copy link
Contributor

Closes #5363

OH 5.0 introduced additional semantic tags and moved some tags between Point and Property. For some users, this is leading to an inconsistent semantic model that is not always easy to correct directly in the UI. Double semantic tags for one class where not visible and cannot be corrected in the UI (the UI only showed one).
Secondly, when custom tags where built as children of these moved tags, the parentID's in the custom tag database did not match anymore, leading to issues.

The semantic tag upgrader will, for managed custom semantic tags and managed items:

  • remove custom semantic tags that have been added to the provided default semantic tags.
  • adjust the parentID of a custom semantic tag if the parent now has a different hierarchy.
  • clean item tags: only allow a single Location, Equipment, Point, Property tag and remove the extra ones.
  • clean items: if there is a Property tag without Point tag, add a "Status" tag.

This is not necessarily making the model fully consistent again (e.g. it is still possible to have both an Equipment and a Point tag), but the remaining issues would be visible in the logs and the health checks, and can be corrected in the UI. As there are always assumptions to be made when we would automatically remove tags, I did not do that, but leave it to the user. The changes are the minimum changes to make it possible to edit the semantic tags through the UI again.

As I foresee that this upgrader may have to run again in future versions of OH, when new tags are added, I put in place infrastructure to have a target version for an upgrader. Upgraders will now store the OH version they run with and this can be checked against the target version to see if an upgrader should be run again (on top of the execution date which was already stored). This will make it possible to just change the target version in the upgrader to run again in a future version upgrade.

And as I am picking up the OH version the upgrader runs with, I store that version information in a separate file in the JSON storage. I see potential to use that in the future to keep track of the JSON DB version vs the OH version and make installation/upgrade decisions based on that (see discussion in https://community.openhab.org/t/openhab-reliability-and-upgrade-experience/167835/72). Having the version of the DB stored in the DB would be requirement for that. There may be a need for multiple (e.g. UpgradeTool here stores a version but it may not have run, so no version from there, a fresh installation should also have a version that could be written in the same file, OH startup would also have a version...). I put in the "UpgradeTool" key to be able to store multiple and decide on what to do based on that.

Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
@mherwege mherwege requested a review from a team as a code owner February 21, 2026 14:12
Signed-off-by: Mark Herwege <[email protected]>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a semantic tag upgrader to address inconsistencies in the semantic model introduced by OH 5.0, and implements infrastructure for tracking openHAB versions to enable version-aware upgrader re-execution. The PR also includes several refactoring improvements to existing upgraders.

Changes:

  • Added SemanticTagUpgrader to clean up custom semantic tags and item semantic tags affected by OH 5.0 changes
  • Implemented version tracking system that stores OH version information and allows upgraders to specify target versions for conditional re-execution
  • Refactored existing upgraders with improved code organization, added @Since annotations, and minor code improvements

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
SemanticTagUpgrader.java New upgrader that removes duplicate custom semantic tags, updates parent relationships, and cleans item semantic tags to fix OH 5.0 semantic model inconsistencies
UpgradeTool.java Added version tracking infrastructure, version comparison logic, new --version CLI option, and VersionRecord/UpgradeRecord structures
Upgrader.java Added getTargetVersion() interface method to allow upgraders to specify target versions
YamlConfigurationV1TagsUpgrader.java Refactored to use YAMLMapper builder pattern and improved code organization
JSProfileUpgrader.java Renamed variable for clarity and added @Since annotation
ItemUnitToMetadataUpgrader.java Changed to wildcard static import and renamed variable for clarity
PersistenceUpgrader.java Extracted updateConfig method and added @Since annotation
ScriptProfileUpgrader.java Added @Since annotation
HomeAssistantAddonUpgrader.java Added @Since annotation
HomieAddonUpgrader.java Added @Since annotation
pom.xml Added org.openhab.core.semantics dependency

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +300 to +302
return version1.compareTo(version2) < 0;
}

Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version comparison logic uses simple string comparison which may not work correctly for all semantic version formats. For example, "5.10.0" would be considered less than "5.2.0" in lexicographic comparison. Consider using a proper semantic version comparison library or splitting and comparing version components numerically.

Suggested change
return version1.compareTo(version2) < 0;
}
return compareSemanticVersions(version1, version2) < 0;
}
/**
* Compares two semantic version strings numerically.
* <p>
* The versions are split on {@code '.'} and each component is compared as an integer.
* Missing components are treated as {@code 0}. Non-numeric suffixes within a component are
* ignored (e.g. {@code "3beta"} is treated as {@code 3}).
* </p>
*
* @return a negative value if {@code versionA < versionB}, zero if equal, and a positive value otherwise.
*/
private static int compareSemanticVersions(String versionA, String versionB) {
String[] partsA = versionA.split("\\\\.");
String[] partsB = versionB.split("\\\\.");
int length = Math.max(partsA.length, partsB.length);
for (int i = 0; i < length; i++) {
int partA = i < partsA.length ? parseVersionPart(partsA[i]) : 0;
int partB = i < partsB.length ? parseVersionPart(partsB[i]) : 0;
if (partA != partB) {
return Integer.compare(partA, partB);
}
}
return 0;
}
/**
* Parses a single numeric component of a version string, ignoring any non-digit suffix.
*
* @param part the version component (e.g. {@code "10"}, {@code "3beta"})
* @return the numeric value of the leading digits, or {@code 0} if none are present or parsing fails
*/
private static int parseVersionPart(String part) {
int endIndex = 0;
int length = part.length();
while (endIndex < length && Character.isDigit(part.charAt(endIndex))) {
endIndex++;
}
if (endIndex == 0) {
return 0;
}
try {
return Integer.parseInt(part.substring(0, endIndex));
} catch (NumberFormatException e) {
return 0;
}
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

@mherwege mherwege Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My comparison indeed was not correct. To avoid issues with comparing versions, I prefer using org.apache.maven.artifact.versioning.ComparableVersion instead.

Signed-off-by: Mark Herwege <[email protected]>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

tools/upgradetool/src/main/java/org/openhab/core/tools/UpgradeTool.java:355

  • The fallback logic in lastExecutedVersion has a flaw: it falls back to checking the UPGRADE_TOOL_VERSION_KEY in ohVersionRecords (lines 347-353) even when the upgradeRecord exists but executionVersion is null. This means that if an upgrader was executed before version tracking was implemented (executionVersion would be null), the method will return the global OH version instead of null. This could cause the upgrader to incorrectly skip execution. The fallback should only apply when upgradeRecord itself is null, not when executionVersion is null.
    private static @Nullable String lastExecutedVersion(String upgrader) {
        JsonStorage<UpgradeRecord> upgrades = upgradeRecords;
        if (upgrades != null) {
            UpgradeRecord upgradeRecord = upgrades.get(upgrader);
            if (upgradeRecord != null) {
                return upgradeRecord.executionVersion();
            }
        }
        JsonStorage<VersionRecord> versions = ohVersionRecords;
        if (versions != null) {
            VersionRecord versionRecord = versions.get(UPGRADE_TOOL_VERSION_KEY);
            if (versionRecord != null) {
                return versionRecord.core();
            }
        }
        return null;
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: Mark Herwege <[email protected]>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: Mark Herwege <[email protected]>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: Mark Herwege <[email protected]>
Signed-off-by: Mark Herwege <[email protected]>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +82 to +86
.findAndAddModules() //
.visibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) //
.visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) //
.defaultPropertyInclusion(Value.construct(Include.NON_NULL, Include.ALWAYS)) //
.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN) //
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The YAML mapper configuration here is intended to match YamlModelRepositoryImpl, but the inclusion settings differ: YamlModelRepositoryImpl uses setSerializationInclusion(Include.NON_NULL) (applies to both property values and container contents), while defaultPropertyInclusion(Value.construct(Include.NON_NULL, Include.ALWAYS)) keeps nulls in container contents. This can change the serialized YAML output. Align the inclusion settings with YamlModelRepositoryImpl to avoid behavioral differences.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

@mherwege mherwege Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a change in behaviour between Jackson 2.8 and 2.9 that changed the behavior of setSerializationInclusion(Include.NON_NULL) to also remove nulls in container contents. I have changed it to match the Jackson 2.9 and later behavior. This behavior change may have gone unnoticed in the code base.

Signed-off-by: Mark Herwege <[email protected]>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +267 to +313
private static VersionRecord getTargetVersion(@Nullable Path userdataPath) {
if (userdataPath != null) {
Path versionFilePath = userdataPath.resolve(Path.of("etc", "version.properties"));
try (BufferedReader reader = Files.newBufferedReader(versionFilePath, StandardCharsets.UTF_8)) {
String build = null;
String distro = null;
String core = null;
String addons = null;
String karaf = null;

String line;
while ((line = reader.readLine()) != null) {
Matcher matcher = BUILD_VERSION_PATTERN.matcher(line.trim());
if (matcher.matches()) {
build = matcher.group(1);
continue;
}
matcher = DISTRO_VERSION_PATTERN.matcher(line.trim());
if (matcher.matches()) {
distro = matcher.group(1);
continue;
}
matcher = CORE_VERSION_PATTERN.matcher(line.trim());
if (matcher.matches()) {
core = matcher.group(1);
continue;
}
matcher = ADDONS_VERSION_PATTERN.matcher(line.trim());
if (matcher.matches()) {
addons = matcher.group(1);
continue;
}
matcher = KARAF_VERSION_PATTERN.matcher(line.trim());
if (matcher.matches()) {
karaf = matcher.group(1);
continue;
}
}
return new VersionRecord(build, distro, core, addons, karaf);
} catch (IOException | SecurityException e) {
LOGGER.warn(
"Cannot retrieve OH core version from '{}' file. Some tasks may fail. You can provide the target version through the --version option.",
versionFilePath);
}
}
return new VersionRecord();
}
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getTargetVersion() parses etc/version.properties using regexes that expect key: value lines, but the runtime codebase reads this file with java.util.Properties (i.e., key=value, e.g. build-no=...). As a result core will typically stay null without throwing, so version tracking and the per-upgrader target-version rerun logic won’t work (and no warning is logged). Consider switching to Properties.load(...) (as in OpenHAB.buildString() / FeatureInstaller) or at least accepting both '=' and ':' separators, and warn when openhab-core cannot be extracted.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

@mherwege mherwege Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file parsed is not a real properties file. Still, as it is the pattern used in OpenHAB.java, I adjust to use the same pattern. It simplifies the code avoiding the regex constants.

Signed-off-by: Mark Herwege <[email protected]>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: Mark Herwege <[email protected]>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@mherwege
Copy link
Contributor Author

Copilot finally looks happy and build succeeds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inconsistencies in semantic model after upgrade to 5.1

2 participants