diff --git a/bom/openhab-core/pom.xml b/bom/openhab-core/pom.xml
index 4a96fcf9a91..8346e8dd549 100644
--- a/bom/openhab-core/pom.xml
+++ b/bom/openhab-core/pom.xml
@@ -514,6 +514,12 @@
${project.version}
compile
+
+ org.openhab.core.bundles
+ org.openhab.core.model.yaml
+ ${project.version}
+ compile
+
org.openhab.core.bundles
org.openhab.core.ui
diff --git a/bundles/org.openhab.core.model.yaml/.classpath b/bundles/org.openhab.core.model.yaml/.classpath
new file mode 100644
index 00000000000..58cd399d639
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/.classpath
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.core.model.yaml/.project b/bundles/org.openhab.core.model.yaml/.project
new file mode 100644
index 00000000000..1b130d3a787
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.core.model.yaml
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bundles/org.openhab.core.model.yaml/NOTICE b/bundles/org.openhab.core.model.yaml/NOTICE
new file mode 100644
index 00000000000..19bf5f2cfa8
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/NOTICE
@@ -0,0 +1,14 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-core
+
diff --git a/bundles/org.openhab.core.model.yaml/pom.xml b/bundles/org.openhab.core.model.yaml/pom.xml
new file mode 100644
index 00000000000..00941ec4b21
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/pom.xml
@@ -0,0 +1,29 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.core.bundles
+ org.openhab.core.reactor.bundles
+ 4.1.0-SNAPSHOT
+
+
+ org.openhab.core.model.yaml
+
+ openHAB Core :: Bundles :: Model YAML
+
+
+
+ org.openhab.core.bundles
+ org.openhab.core
+ ${project.version}
+
+
+ org.openhab.core.bundles
+ org.openhab.core.semantics
+ ${project.version}
+
+
+
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/AbstractYamlFile.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/AbstractYamlFile.java
new file mode 100644
index 00000000000..d99c8151ca1
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/AbstractYamlFile.java
@@ -0,0 +1,65 @@
+/**
+ * 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.model.yaml;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link AbstractYamlFile} is the DTO base class used to map a YAML configuration file.
+ *
+ * A YAML configuration file consists of a version and a list of elements.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public abstract class AbstractYamlFile implements YamlFile {
+
+ private final Logger logger = LoggerFactory.getLogger(AbstractYamlFile.class);
+
+ /**
+ * YAML file version
+ */
+ public int version;
+
+ @Override
+ public abstract List extends YamlElement> getElements();
+
+ @Override
+ public int getVersion() {
+ return version;
+ }
+
+ @Override
+ public boolean isValid() {
+ // Checking duplicated elements
+ List extends YamlElement> elts = getElements();
+ long nbDistinctIds = elts.stream().map(YamlElement::getId).distinct().count();
+ if (nbDistinctIds < elts.size()) {
+ logger.debug("Elements with same ids detected in the file");
+ return false;
+ }
+
+ // Checking each element
+ for (int i = 0; i < elts.size(); i++) {
+ if (!elts.get(i).isValid()) {
+ logger.debug("Error in element {}", i + 1);
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlElement.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlElement.java
new file mode 100644
index 00000000000..2b1d8b16a03
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlElement.java
@@ -0,0 +1,39 @@
+/**
+ * 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.model.yaml;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link YamlElement} interface offers an identifier and a check validity method
+ * to any element defined in a YAML configuration file.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public interface YamlElement {
+
+ /**
+ * Get the identifier of the YAML element
+ *
+ * @return the identifier as a string
+ */
+ String getId();
+
+ /**
+ * Check that the YAML element is valid
+ *
+ * @return true if all the checks are OK
+ */
+ boolean isValid();
+}
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlFile.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlFile.java
new file mode 100644
index 00000000000..40f5ce4c9c8
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlFile.java
@@ -0,0 +1,48 @@
+/**
+ * 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.model.yaml;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link YamlFile} is the interface to manage the generic content of a YAML configuration file.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public interface YamlFile {
+
+ /**
+ * Get the list of elements present in the YAML file.
+ *
+ * @return the list of elements
+ */
+ List extends YamlElement> getElements();
+
+ /**
+ * Get the version present in the YAML file.
+ *
+ * @return the version in the file
+ */
+ int getVersion();
+
+ /**
+ * Check that the file content is valid.
+ * It includes the check of duplicated elements (same identifier) and the check of each element.
+ *
+ * @return true if all the checks are OK
+ */
+ boolean isValid();
+}
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlModelListener.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlModelListener.java
new file mode 100644
index 00000000000..b15a8d9d639
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlModelListener.java
@@ -0,0 +1,75 @@
+/**
+ * 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.model.yaml;
+
+import java.util.Collection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link YamlModelListener} interface is responsible for managing a particular model type
+ * with data processed from YAML configuration files.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public interface YamlModelListener {
+
+ /**
+ * Method called by the model repository when elements from a model are added.
+ *
+ * @param modelName the name of the model
+ * @param elements the collection of added elements
+ */
+ void addedModel(String modelName, Collection extends YamlElement> elements);
+
+ /**
+ * Method called by the model repository when elements from a model are updated.
+ *
+ * @param modelName the name of the model
+ * @param elements the collection of updated elements
+ */
+ void updatedModel(String modelName, Collection extends YamlElement> elements);
+
+ /**
+ * Method called by the model repository when elements from a model are removed.
+ *
+ * @param modelName the name of the model
+ * @param elements the collection of removed elements
+ */
+ void removedModel(String modelName, Collection extends YamlElement> elements);
+
+ /**
+ * Get the root name of this model type which is also the name of the root folder
+ * containing the user files for this model type.
+ *
+ * A path is unexpected. What is expected is for example "items" or "things".
+ *
+ * @return the model root name
+ */
+ String getRootName();
+
+ /**
+ * Get the DTO class to be used for a file providing objects for this model type.
+ *
+ * @return the DTO file class
+ */
+ Class extends AbstractYamlFile> getFileClass();
+
+ /**
+ * Get the DTO class to be used for each object of this model type.
+ *
+ * @return the DTO element class
+ */
+ Class getElementClass();
+}
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlParseException.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlParseException.java
new file mode 100644
index 00000000000..bae53ba78d9
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/YamlParseException.java
@@ -0,0 +1,39 @@
+/**
+ * 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.model.yaml;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link YamlParseException} is used when an error is detected when parsing the content
+ * of a YAML configuration file.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class YamlParseException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public YamlParseException(String message) {
+ super(message);
+ }
+
+ public YamlParseException(Throwable cause) {
+ super(cause);
+ }
+
+ public YamlParseException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/YamlModelRepository.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/YamlModelRepository.java
new file mode 100644
index 00000000000..a7d04138762
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/YamlModelRepository.java
@@ -0,0 +1,198 @@
+/**
+ * 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.model.yaml.internal;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.model.yaml.AbstractYamlFile;
+import org.openhab.core.model.yaml.YamlElement;
+import org.openhab.core.model.yaml.YamlModelListener;
+import org.openhab.core.model.yaml.YamlParseException;
+import org.openhab.core.service.WatchService;
+import org.openhab.core.service.WatchService.Kind;
+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;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+
+/**
+ * The {@link YamlModelRepository} is an OSGi service, that encapsulates all YAML file processing
+ * including file monitoring to detect created, updated and removed YAML configuration files.
+ * Data processed from these files are consumed by registered OSGi services that implement {@link YamlModelListener}.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+@Component(immediate = true)
+public class YamlModelRepository implements WatchService.WatchEventListener {
+
+ private final Logger logger = LoggerFactory.getLogger(YamlModelRepository.class);
+
+ private final WatchService watchService;
+ private final Path watchPath;
+ private final ObjectMapper yamlReader;
+
+ private final Map>> listeners = new ConcurrentHashMap<>();
+ private final Map> objects = new ConcurrentHashMap<>();
+
+ @Activate
+ public YamlModelRepository(@Reference(target = WatchService.CONFIG_WATCHER_FILTER) WatchService watchService) {
+ this.watchService = watchService;
+ this.yamlReader = new ObjectMapper(new YAMLFactory());
+ yamlReader.findAndRegisterModules();
+
+ watchService.registerListener(this, Path.of(""));
+ watchPath = watchService.getWatchPath();
+ }
+
+ @Deactivate
+ public void deactivate() {
+ watchService.unregisterListener(this);
+ }
+
+ // The method is "synchronized" to avoid concurrent files processing
+ @Override
+ public synchronized void processWatchEvent(Kind kind, Path path) {
+ Path fullPath = watchPath.resolve(path);
+ String dirName = path.subpath(0, 1).toString();
+
+ if (Files.isDirectory(fullPath) || fullPath.toFile().isHidden() || !fullPath.toString().endsWith(".yaml")) {
+ logger.trace("Ignored {}", fullPath);
+ return;
+ }
+
+ getListeners(dirName).forEach(listener -> processWatchEvent(dirName, kind, fullPath, listener));
+ }
+
+ private void processWatchEvent(String dirName, Kind kind, Path fullPath, YamlModelListener> listener) {
+ logger.debug("processWatchEvent dirName={} kind={} fullPath={} listener={}", dirName, kind, fullPath,
+ listener.getClass().getSimpleName());
+ Map oldObjects;
+ Map newObjects;
+ if (kind == WatchService.Kind.DELETE) {
+ newObjects = Map.of();
+
+ List extends YamlElement> oldListObjects = objects.remove(fullPath);
+ if (oldListObjects == null) {
+ oldListObjects = List.of();
+ }
+ oldObjects = oldListObjects.stream().collect(Collectors.toMap(YamlElement::getId, obj -> obj));
+ } else {
+ AbstractYamlFile yamlData;
+ try {
+ yamlData = readYamlFile(fullPath, listener.getFileClass());
+ } catch (YamlParseException e) {
+ logger.warn("Failed to parse Yaml file {} with DTO class {}: {}", fullPath,
+ listener.getFileClass().getName(), e.getMessage());
+ return;
+ }
+ List extends YamlElement> newListObjects = yamlData.getElements();
+ newObjects = newListObjects.stream().collect(Collectors.toMap(YamlElement::getId, obj -> obj));
+
+ List extends YamlElement> oldListObjects = objects.get(fullPath);
+ if (oldListObjects == null) {
+ oldListObjects = List.of();
+ }
+ oldObjects = oldListObjects.stream().collect(Collectors.toMap(YamlElement::getId, obj -> obj));
+
+ objects.put(fullPath, newListObjects);
+ }
+
+ String modelName = fullPath.toFile().getName();
+ modelName = modelName.substring(0, modelName.indexOf(".yaml"));
+ List extends YamlElement> listElements;
+ listElements = oldObjects.entrySet().stream()
+ .filter(entry -> entry.getValue().getClass().equals(listener.getElementClass())
+ && !newObjects.containsKey(entry.getKey()))
+ .map(Map.Entry::getValue).toList();
+ if (!listElements.isEmpty()) {
+ listener.removedModel(modelName, listElements);
+ }
+
+ listElements = newObjects.entrySet().stream()
+ .filter(entry -> entry.getValue().getClass().equals(listener.getElementClass())
+ && !oldObjects.containsKey(entry.getKey()))
+ .map(Map.Entry::getValue).toList();
+ if (!listElements.isEmpty()) {
+ listener.addedModel(modelName, listElements);
+ }
+
+ // Object is ignored if unchanged
+ listElements = newObjects.entrySet().stream()
+ .filter(entry -> entry.getValue().getClass().equals(listener.getElementClass())
+ && oldObjects.containsKey(entry.getKey())
+ && !entry.getValue().equals(oldObjects.get(entry.getKey())))
+ .map(Map.Entry::getValue).toList();
+ if (!listElements.isEmpty()) {
+ listener.updatedModel(modelName, listElements);
+ }
+ }
+
+ private AbstractYamlFile readYamlFile(Path path, Class extends AbstractYamlFile> dtoClass)
+ throws YamlParseException {
+ logger.debug("readYamlFile {} with {}", path.toFile().getAbsolutePath(), dtoClass.getName());
+ try {
+ AbstractYamlFile dto = yamlReader.readValue(path.toFile(), dtoClass);
+ if (!dto.isValid()) {
+ throw new YamlParseException("The file is not valid, some checks failed!");
+ }
+ return dto;
+ } catch (IOException e) {
+ throw new YamlParseException(e);
+ }
+ }
+
+ @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
+ protected void addYamlModelListener(YamlModelListener> listener) {
+ String dirName = listener.getRootName();
+ logger.debug("Adding model listener for {}", dirName);
+ getListeners(dirName).add(listener);
+
+ // Load all existing YAML files
+ try (Stream stream = Files.walk(watchPath.resolve(dirName))) {
+ stream.forEach(path -> {
+ if (!Files.isDirectory(path) && !path.toFile().isHidden() && path.toString().endsWith(".yaml")) {
+ processWatchEvent(dirName, Kind.CREATE, path, listener);
+ }
+ });
+ } catch (IOException ignored) {
+ }
+ }
+
+ protected void removeYamlModelListener(YamlModelListener> listener) {
+ String dirName = listener.getRootName();
+ logger.debug("Removing model listener for {}", dirName);
+ getListeners(dirName).remove(listener);
+ }
+
+ private List> getListeners(String dirName) {
+ return Objects.requireNonNull(listeners.computeIfAbsent(dirName, k -> new CopyOnWriteArrayList<>()));
+ }
+}
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTag.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTag.java
new file mode 100644
index 00000000000..9d8f0865abd
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTag.java
@@ -0,0 +1,66 @@
+/**
+ * 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.model.yaml.internal.semantics;
+
+import java.util.List;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.model.yaml.YamlElement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link YamlSemanticTag} is a data transfer object used to serialize a semantic tag
+ * in a YAML configuration file.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+public class YamlSemanticTag implements YamlElement {
+
+ private final Logger logger = LoggerFactory.getLogger(YamlSemanticTag.class);
+
+ public String uid;
+ public String label;
+ public String description;
+ public List synonyms;
+
+ public YamlSemanticTag() {
+ }
+
+ @Override
+ public String getId() {
+ return uid;
+ }
+
+ @Override
+ public boolean isValid() {
+ if (uid == null) {
+ logger.debug("uid missing");
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ YamlSemanticTag that = (YamlSemanticTag) obj;
+ return Objects.equals(uid, that.uid) && Objects.equals(label, that.label)
+ && Objects.equals(description, that.description) && Objects.equals(synonyms, that.synonyms);
+ }
+}
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTagProvider.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTagProvider.java
new file mode 100644
index 00000000000..4f13120b9a1
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTagProvider.java
@@ -0,0 +1,126 @@
+/**
+ * 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.model.yaml.internal.semantics;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.common.registry.AbstractProvider;
+import org.openhab.core.model.yaml.AbstractYamlFile;
+import org.openhab.core.model.yaml.YamlElement;
+import org.openhab.core.model.yaml.YamlModelListener;
+import org.openhab.core.semantics.SemanticTag;
+import org.openhab.core.semantics.SemanticTagImpl;
+import org.openhab.core.semantics.SemanticTagProvider;
+import org.openhab.core.semantics.SemanticTagRegistry;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link YamlSemanticTagProvider} is an OSGi service, that allows to define semantic tags
+ * in YAML configuration files in folder conf/tags.
+ * Files can be added, updated or removed at runtime.
+ * These semantic tags are automatically exposed to the {@link SemanticTagRegistry}.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+@Component(immediate = true, service = { SemanticTagProvider.class, YamlSemanticTagProvider.class,
+ YamlModelListener.class })
+public class YamlSemanticTagProvider extends AbstractProvider
+ implements SemanticTagProvider, YamlModelListener {
+
+ private final Logger logger = LoggerFactory.getLogger(YamlSemanticTagProvider.class);
+
+ private final Set tags = new TreeSet<>(Comparator.comparing(SemanticTag::getUID));
+
+ @Activate
+ public YamlSemanticTagProvider() {
+ }
+
+ @Deactivate
+ public void deactivate() {
+ tags.clear();
+ }
+
+ @Override
+ public Collection getAll() {
+ return tags;
+ }
+
+ @Override
+ public String getRootName() {
+ return "tags";
+ }
+
+ @Override
+ public Class extends AbstractYamlFile> getFileClass() {
+ return YamlSemanticTags.class;
+ }
+
+ @Override
+ public Class getElementClass() {
+ return YamlSemanticTag.class;
+ }
+
+ @Override
+ public void addedModel(String modelName, Collection extends YamlElement> elements) {
+ List added = elements.stream().map(e -> mapSemanticTag((YamlSemanticTag) e))
+ .sorted(Comparator.comparing(SemanticTag::getUID)).toList();
+ tags.addAll(added);
+ added.forEach(t -> {
+ logger.debug("model {} added tag {}", modelName, t.getUID());
+ notifyListenersAboutAddedElement(t);
+ });
+ }
+
+ @Override
+ public void updatedModel(String modelName, Collection extends YamlElement> elements) {
+ List updated = elements.stream().map(e -> mapSemanticTag((YamlSemanticTag) e)).toList();
+ updated.forEach(t -> {
+ tags.stream().filter(tag -> tag.getUID().equals(t.getUID())).findFirst().ifPresentOrElse(oldTag -> {
+ tags.remove(oldTag);
+ tags.add(t);
+ logger.debug("model {} updated tag {}", modelName, t.getUID());
+ notifyListenersAboutUpdatedElement(oldTag, t);
+ }, () -> logger.debug("model {} tag {} not found", modelName, t.getUID()));
+ });
+ }
+
+ @Override
+ public void removedModel(String modelName, Collection extends YamlElement> elements) {
+ List removed = elements.stream().map(e -> mapSemanticTag((YamlSemanticTag) e))
+ .sorted(Comparator.comparing(SemanticTag::getUID).reversed()).toList();
+ removed.forEach(t -> {
+ tags.stream().filter(tag -> tag.getUID().equals(t.getUID())).findFirst().ifPresentOrElse(oldTag -> {
+ tags.remove(oldTag);
+ logger.debug("model {} removed tag {}", modelName, t.getUID());
+ notifyListenersAboutRemovedElement(oldTag);
+ }, () -> logger.debug("model {} tag {} not found", modelName, t.getUID()));
+ });
+ }
+
+ private SemanticTag mapSemanticTag(YamlSemanticTag tagDTO) {
+ if (tagDTO.uid == null) {
+ throw new IllegalArgumentException("The argument 'tagDTO.uid' must not be null.");
+ }
+ return new SemanticTagImpl(tagDTO.uid, tagDTO.label, tagDTO.description, tagDTO.synonyms);
+ }
+}
diff --git a/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTags.java b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTags.java
new file mode 100644
index 00000000000..3c2a4ee9609
--- /dev/null
+++ b/bundles/org.openhab.core.model.yaml/src/main/java/org/openhab/core/model/yaml/internal/semantics/YamlSemanticTags.java
@@ -0,0 +1,36 @@
+/**
+ * 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.model.yaml.internal.semantics;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.model.yaml.AbstractYamlFile;
+import org.openhab.core.model.yaml.YamlElement;
+
+/**
+ * The {@link YamlSemanticTags} is a data transfer object used to serialize a list of semantic tags
+ * in a YAML configuration file.
+ *
+ * @author Laurent Garnier - Initial contribution
+ */
+@NonNullByDefault
+public class YamlSemanticTags extends AbstractYamlFile {
+
+ public List tags = List.of();
+
+ @Override
+ public List extends YamlElement> getElements() {
+ return tags;
+ }
+}
diff --git a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java
index 863594d15b8..93c842f06a4 100644
--- a/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java
+++ b/bundles/org.openhab.core.semantics/src/main/java/org/openhab/core/semantics/internal/SemanticsMetadataProvider.java
@@ -22,6 +22,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.common.registry.AbstractProvider;
+import org.openhab.core.common.registry.RegistryChangeListener;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemRegistry;
@@ -33,6 +34,7 @@
import org.openhab.core.semantics.Location;
import org.openhab.core.semantics.Point;
import org.openhab.core.semantics.Property;
+import org.openhab.core.semantics.SemanticTag;
import org.openhab.core.semantics.SemanticTagRegistry;
import org.openhab.core.semantics.SemanticTags;
import org.openhab.core.semantics.Tag;
@@ -72,11 +74,16 @@ public class SemanticsMetadataProvider extends AbstractProvider
private final Map semantics = new TreeMap<>(String::compareTo);
private final ItemRegistry itemRegistry;
+ private final SemanticTagRegistry semanticTagRegistry;
+
+ private SemanticTagRegistryChangeListener listener;
@Activate
public SemanticsMetadataProvider(final @Reference ItemRegistry itemRegistry,
final @Reference SemanticTagRegistry semanticTagRegistry) {
this.itemRegistry = itemRegistry;
+ this.semanticTagRegistry = semanticTagRegistry;
+ this.listener = new SemanticTagRegistryChangeListener(this);
}
@Activate
@@ -86,10 +93,12 @@ protected void activate() {
processItem(item);
}
itemRegistry.addRegistryChangeListener(this);
+ semanticTagRegistry.addRegistryChangeListener(listener);
}
@Deactivate
protected void deactivate() {
+ semanticTagRegistry.removeRegistryChangeListener(listener);
itemRegistry.removeRegistryChangeListener(this);
semantics.clear();
}
@@ -280,4 +289,28 @@ public void removed(Item item) {
public void updated(Item oldItem, Item item) {
processItem(item);
}
+
+ private class SemanticTagRegistryChangeListener implements RegistryChangeListener {
+
+ private SemanticsMetadataProvider provider;
+
+ public SemanticTagRegistryChangeListener(SemanticsMetadataProvider provider) {
+ this.provider = provider;
+ }
+
+ @Override
+ public void added(SemanticTag element) {
+ provider.allItemsChanged(List.of());
+ }
+
+ @Override
+ public void removed(SemanticTag element) {
+ provider.allItemsChanged(List.of());
+ }
+
+ @Override
+ public void updated(SemanticTag oldElement, SemanticTag element) {
+ provider.allItemsChanged(List.of());
+ }
+ }
}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index ad744143289..01799d77195 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -102,6 +102,7 @@
org.openhab.core.model.thing
org.openhab.core.model.thing.ide
org.openhab.core.model.thing.runtime
+ org.openhab.core.model.yaml
org.openhab.core.storage.json
org.openhab.core.test
org.openhab.core.test.magic
diff --git a/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml
index 2f919eb97a1..5062c2ead67 100644
--- a/features/karaf/openhab-core/src/main/feature/feature.xml
+++ b/features/karaf/openhab-core/src/main/feature/feature.xml
@@ -390,6 +390,13 @@
mvn:org.openhab.core.bundles/org.openhab.core.model.lsp/${project.version}
+
+ openhab-core-base
+ mvn:org.openhab.core.bundles/org.openhab.core.model.yaml/${project.version}
+ openhab.tp;filter:="(feature=jackson)"
+ openhab.tp-jackson
+
+
openhab-core-base
@@ -434,6 +441,7 @@
openhab-core-model-script
openhab-core-model-sitemap
openhab-core-model-thing
+ openhab-core-model-yaml
openhab-core-ui-icon
openhab-core-storage-json
openhab-runtime-certificate