diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/pom.xml b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/pom.xml
new file mode 100644
index 000000000..aa75f6c5d
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/pom.xml
@@ -0,0 +1,50 @@
+
+ 4.0.0
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelservice
+ ${revision}
+
+ basyx.submodelservice-feature-mqtt
+ BaSyx submodelservice-feature-mqtt
+ BaSyx submodelservice-feature-mqtt
+
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelservice-core
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.mqttcore
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelservice-core
+ tests
+ test
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelservice-backend-inmemory
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ io.moquette
+ moquette-broker
+ test
+
+
+ org.eclipse.paho
+ org.eclipse.paho.client.mqttv3
+
+
+ org.eclipse.digitaltwin.basyx
+ basyx.submodelservice-backend-inmemory
+
+
+
\ No newline at end of file
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelService.java b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelService.java
new file mode 100644
index 000000000..69e41f74c
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelService.java
@@ -0,0 +1,192 @@
+/*******************************************************************************
+ * Copyright (C) 2024 the Eclipse BaSyx Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.mqtt;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.List;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.OperationVariable;
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
+import org.eclipse.digitaltwin.basyx.common.mqttcore.serializer.SubmodelElementSerializer;
+import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException;
+import org.eclipse.digitaltwin.basyx.core.exceptions.ElementNotAFileException;
+import org.eclipse.digitaltwin.basyx.core.exceptions.FileDoesNotExistException;
+import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult;
+import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo;
+import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelService;
+import org.eclipse.digitaltwin.basyx.submodelservice.value.SubmodelElementValue;
+import org.eclipse.paho.client.mqttv3.IMqttClient;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.eclipse.paho.client.mqttv3.MqttPersistenceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Service decorator for the MQTT eventing on the submodel level.
+ *
+ * @author rana
+ */
+public class MqttSubmodelService implements SubmodelService {
+
+ private static Logger logger = LoggerFactory.getLogger(MqttSubmodelService.class);
+ private MqttSubmodelServiceTopicFactory topicFactory;
+ private SubmodelService decorated;
+ private IMqttClient mqttClient;
+
+ public MqttSubmodelService(SubmodelService decorated, IMqttClient mqttClient, MqttSubmodelServiceTopicFactory topicFactory) {
+ this.topicFactory = topicFactory;
+ this.decorated = decorated;
+ this.mqttClient = mqttClient;
+ }
+
+ @Override
+ public Submodel getSubmodel() {
+ return decorated.getSubmodel();
+ }
+
+ @Override
+ public CursorResult> getSubmodelElements(PaginationInfo pInfo) {
+ return decorated.getSubmodelElements(pInfo);
+ }
+
+ @Override
+ public SubmodelElement getSubmodelElement(String idShortPath) throws ElementDoesNotExistException {
+ return decorated.getSubmodelElement(idShortPath);
+ }
+
+ @Override
+ public SubmodelElementValue getSubmodelElementValue(String idShortPath) throws ElementDoesNotExistException {
+ return decorated.getSubmodelElementValue(idShortPath);
+ }
+
+ @Override
+ public void setSubmodelElementValue(String idShortPath, SubmodelElementValue value) throws ElementDoesNotExistException {
+ decorated.setSubmodelElementValue(idShortPath, value);
+ SubmodelElement submodelElement = decorated.getSubmodelElement(idShortPath);
+ submodelElementUpdated(submodelElement, idShortPath);
+ }
+
+ @Override
+ public void createSubmodelElement(SubmodelElement submodelElement) {
+ decorated.createSubmodelElement(submodelElement);
+ SubmodelElement smElement = decorated.getSubmodelElement(submodelElement.getIdShort());
+ submodelElementCreated(submodelElement, smElement.getIdShort());
+ }
+
+ @Override
+ public void createSubmodelElement(String idShortPath, SubmodelElement submodelElement) throws ElementDoesNotExistException {
+
+ decorated.createSubmodelElement(idShortPath, submodelElement);
+
+ SubmodelElement smElement = decorated.getSubmodelElement(submodelElement.getIdShort());
+ submodelElementCreated(smElement, idShortPath);
+ }
+
+ @Override
+ public void updateSubmodelElement(String idShortPath, SubmodelElement submodelElement) throws ElementDoesNotExistException {
+
+ decorated.updateSubmodelElement(idShortPath, submodelElement);
+ SubmodelElement smElement = decorated.getSubmodelElement(submodelElement.getIdShort());
+ submodelElementUpdated(smElement, submodelElement.getIdShort());
+ }
+
+ @Override
+ public void deleteSubmodelElement(String idShortPath) throws ElementDoesNotExistException {
+
+ SubmodelElement smElement = decorated.getSubmodelElement(idShortPath);
+ decorated.deleteSubmodelElement(idShortPath);
+ submodelElementDeleted(smElement, idShortPath);
+ }
+
+ @Override
+ public void patchSubmodelElements(List submodelElementList) {
+ decorated.patchSubmodelElements(submodelElementList);
+ }
+
+ @Override
+ public OperationVariable[] invokeOperation(String idShortPath, OperationVariable[] input) throws ElementDoesNotExistException {
+ return decorated.invokeOperation(idShortPath, input);
+ }
+
+ @Override
+ public File getFileByPath(String idShortPath) throws ElementDoesNotExistException, ElementNotAFileException, FileDoesNotExistException {
+ return decorated.getFileByPath(idShortPath);
+ }
+
+ @Override
+ public void setFileValue(String idShortPath, String fileName, InputStream inputStream) throws ElementDoesNotExistException, ElementNotAFileException {
+ decorated.setFileValue(idShortPath, fileName, inputStream);
+ }
+
+ @Override
+ public void deleteFileValue(String idShortPath) throws ElementDoesNotExistException, ElementNotAFileException, FileDoesNotExistException {
+ decorated.deleteFileValue(idShortPath);
+ }
+
+ private void submodelElementCreated(SubmodelElement submodelElement, String idShort) {
+ sendMqttMessage(topicFactory.createCreateSubmodelElementTopic(idShort), SubmodelElementSerializer.serializeSubmodelElement(submodelElement));
+ }
+
+ private void submodelElementUpdated(SubmodelElement submodelElement, String idShortPath) {
+ sendMqttMessage(topicFactory.createUpdateSubmodelElementTopic(idShortPath), SubmodelElementSerializer.serializeSubmodelElement(submodelElement));
+ }
+
+ private void submodelElementDeleted(SubmodelElement submodelElement, String idShort) {
+ sendMqttMessage(topicFactory.createDeleteSubmodelElementTopic(idShort), SubmodelElementSerializer.serializeSubmodelElement(submodelElement));
+ }
+
+ /**
+ * Sends MQTT message to connected broker
+ *
+ * @param topic
+ * in which the message will be published
+ * @param payload
+ * the actual message
+ */
+ private void sendMqttMessage(String topic, String payload) {
+ MqttMessage msg = createMqttMessage(payload);
+
+ try {
+ logger.debug("Send MQTT message to " + topic + ": " + payload);
+ mqttClient.publish(topic, msg);
+ } catch (MqttPersistenceException e) {
+ logger.error("Could not persist mqtt message", e);
+ } catch (MqttException e) {
+ logger.error("Could not send mqtt message", e);
+ }
+ }
+
+ private MqttMessage createMqttMessage(String payload) {
+ if (payload == null) {
+ return new MqttMessage();
+ } else {
+ return new MqttMessage(payload.getBytes());
+ }
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceConfiguration.java b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceConfiguration.java
new file mode 100644
index 000000000..1984dfb94
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceConfiguration.java
@@ -0,0 +1,68 @@
+/*******************************************************************************
+ * Copyright (C) 2024 the Eclipse BaSyx Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.mqtt;
+
+import org.eclipse.paho.client.mqttv3.IMqttClient;
+import org.eclipse.paho.client.mqttv3.MqttClient;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * MQTT configuration to allow for the automatic enable of the feature using the
+ * config file.
+ *
+ * @author rana
+ */
+@ConditionalOnExpression("#{${" + MqttSubmodelServiceFeature.FEATURENAME + ".enabled:false} or ${basyx.feature.mqtt.enabled:false}}")
+@Configuration
+public class MqttSubmodelServiceConfiguration {
+
+ @ConditionalOnMissingBean
+ @Bean
+ public IMqttClient mqttClient(@Value("${mqtt.clientId}") String clientId, @Value("${mqtt.hostname}") String hostname, @Value("${mqtt.port}") int port) throws MqttException {
+ IMqttClient mqttClient = new MqttClient("tcp://" + hostname + ":" + port, clientId, new MemoryPersistence());
+
+ mqttClient.connect(mqttConnectOptions());
+
+ return mqttClient;
+ }
+
+ @ConditionalOnMissingBean
+ @Bean
+ @ConfigurationProperties(prefix = "mqtt")
+ public MqttConnectOptions mqttConnectOptions() {
+ MqttConnectOptions mqttConceptOptions = new MqttConnectOptions();
+ mqttConceptOptions.setAutomaticReconnect(true);
+ return mqttConceptOptions;
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceFactory.java b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceFactory.java
new file mode 100644
index 000000000..9e3a0f1ca
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceFactory.java
@@ -0,0 +1,54 @@
+/*******************************************************************************
+ * Copyright (C) 2024 the Eclipse BaSyx Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.mqtt;
+
+import org.eclipse.digitaltwin.aas4j.v3.model.Submodel;
+import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelService;
+import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelServiceFactory;
+import org.eclipse.paho.client.mqttv3.IMqttClient;
+
+/**
+ * Service factory for the MQTT eventing on the submodel level.
+ *
+ * @author rana
+ */
+public class MqttSubmodelServiceFactory implements SubmodelServiceFactory {
+
+ private SubmodelServiceFactory decorated;
+ private IMqttClient client;
+ private MqttSubmodelServiceTopicFactory topicFactory;
+
+ public MqttSubmodelServiceFactory(SubmodelServiceFactory decorated, IMqttClient client, MqttSubmodelServiceTopicFactory topicFactory) {
+ this.decorated = decorated;
+ this.client = client;
+ this.topicFactory = topicFactory;
+ }
+
+ @Override
+ public SubmodelService create(Submodel submodel) {
+ return new MqttSubmodelService(decorated.create(submodel), client, topicFactory);
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceFeature.java b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceFeature.java
new file mode 100644
index 000000000..7e056450b
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceFeature.java
@@ -0,0 +1,81 @@
+/*******************************************************************************
+ * Copyright (C) 2024 the Eclipse BaSyx Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.mqtt;
+
+import org.eclipse.digitaltwin.basyx.common.mqttcore.encoding.Base64URLEncoder;
+import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelServiceFactory;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.SubmodelServiceFeature;
+import org.eclipse.paho.client.mqttv3.IMqttClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.stereotype.Component;
+
+/**
+ * Service feature for the MQTT eventing on the submodel level.
+ *
+ * @author rana
+ */
+@ConditionalOnExpression("#{${" + MqttSubmodelServiceFeature.FEATURENAME + ".enabled:false} or ${basyx.feature.mqtt.enabled:false}}")
+@Component
+public class MqttSubmodelServiceFeature implements SubmodelServiceFeature {
+
+ public final static String FEATURENAME = "basyx.submodelservice.feature.mqtt";
+
+ @Value("#{${" + FEATURENAME + ".enabled:false} or ${basyx.feature.mqtt.enabled:false}}")
+ private boolean enabled;
+
+ private IMqttClient mqttClient;
+
+ @Autowired
+ public MqttSubmodelServiceFeature(IMqttClient mqttClient) {
+ this.mqttClient = mqttClient;
+ }
+
+ @Override
+ public SubmodelServiceFactory decorate(SubmodelServiceFactory component) {
+ return new MqttSubmodelServiceFactory(component, mqttClient, new MqttSubmodelServiceTopicFactory(new Base64URLEncoder()));
+ }
+
+ @Override
+ public void initialize() {
+ }
+
+ @Override
+ public void cleanUp() {
+
+ }
+
+ @Override
+ public String getName() {
+ return "SubmodelService MQTT";
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled;
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceTopicFactory.java b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceTopicFactory.java
new file mode 100644
index 000000000..31296f4d0
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/feature/mqtt/MqttSubmodelServiceTopicFactory.java
@@ -0,0 +1,83 @@
+/*******************************************************************************
+ * Copyright (C) 2024 the Eclipse BaSyx Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+
+package org.eclipse.digitaltwin.basyx.submodelservice.feature.mqtt;
+
+import java.util.StringJoiner;
+
+import org.eclipse.digitaltwin.basyx.common.mqttcore.AbstractMqttTopicFactory;
+import org.eclipse.digitaltwin.basyx.common.mqttcore.encoding.Encoder;
+
+/**
+ * MQTT topic factory for the eventing on the submodel level.
+ *
+ * @author rana
+ */
+public class MqttSubmodelServiceTopicFactory extends AbstractMqttTopicFactory {
+
+ private static final String SUBMODEL_SERVICE = "sm-service";
+ private static final String SUBMODELS = "submodels";
+ private static final String CREATED = "created";
+ private static final String UPDATED = "updated";
+ private static final String DELETED = "deleted";
+ private static final String SUBMODEL_ELEMENTS = "submodelElements";
+
+ /**
+ * Used for encoding the idShort
+ *
+ * @param encoder
+ */
+ public MqttSubmodelServiceTopicFactory(Encoder encoder) {
+ super(encoder);
+ }
+
+ /**
+ * Creates the hierarchical topic for the create event of submodelElements
+ *
+ * @param idShort
+ */
+ public String createCreateSubmodelElementTopic(String idShort) {
+ return new StringJoiner("/", "", "").add(SUBMODEL_SERVICE).add(SUBMODELS).add(SUBMODEL_ELEMENTS).add(idShort).add(CREATED).toString();
+ }
+
+ /**
+ * Creates the hierarchical topic for the update event of submodelElements
+ *
+ * @param idShort
+ */
+ public String createUpdateSubmodelElementTopic(String idShort) {
+
+ return new StringJoiner("/", "", "").add(SUBMODEL_SERVICE).add(SUBMODELS).add(SUBMODEL_ELEMENTS).add(idShort).add(UPDATED).toString();
+ }
+
+ /**
+ * Creates the hierarchical topic for the delete event of submodelElements
+ *
+ * @param idShort
+ */
+ public String createDeleteSubmodelElementTopic(String idShort) {
+ return new StringJoiner("/", "", "").add(SUBMODEL_SERVICE).add(SUBMODELS).add(SUBMODEL_ELEMENTS).add(idShort).add(DELETED).toString();
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttTestListener.java b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttTestListener.java
new file mode 100644
index 000000000..c7cc88b3b
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttTestListener.java
@@ -0,0 +1,97 @@
+/*******************************************************************************
+ * Copyright (C) 2024 the Eclipse BaSyx Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+
+package org.eclipse.digitaltwin.basyx.submodelrepository.feature.mqtt;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+
+import io.moquette.interception.InterceptHandler;
+import io.moquette.interception.messages.InterceptAcknowledgedMessage;
+import io.moquette.interception.messages.InterceptConnectMessage;
+import io.moquette.interception.messages.InterceptConnectionLostMessage;
+import io.moquette.interception.messages.InterceptDisconnectMessage;
+import io.moquette.interception.messages.InterceptPublishMessage;
+import io.moquette.interception.messages.InterceptSubscribeMessage;
+import io.moquette.interception.messages.InterceptUnsubscribeMessage;
+
+/**
+ * Very simple MQTT broker listener for testing API events. Stores the last
+ * received event and makes its topic and payload available for reading.
+ *
+ * @author espen
+ *
+ */
+public class MqttTestListener implements InterceptHandler {
+ // Topic and payload of the most recent event
+ public String lastTopic;
+ public String lastPayload;
+ private ArrayList topics = new ArrayList<>();
+
+ @Override
+ public String getID() {
+ return null;
+ }
+
+ @Override
+ public Class>[] getInterceptedMessageTypes() {
+ return null;
+ }
+
+ @Override
+ public void onConnect(InterceptConnectMessage arg0) {
+ }
+
+ @Override
+ public void onConnectionLost(InterceptConnectionLostMessage arg0) {
+ }
+
+ @Override
+ public void onDisconnect(InterceptDisconnectMessage arg0) {
+ }
+
+ @Override
+ public void onMessageAcknowledged(InterceptAcknowledgedMessage arg0) {
+ }
+
+ @Override
+ public synchronized void onPublish(InterceptPublishMessage msg) {
+ topics.add(msg.getTopicName());
+ lastTopic = msg.getTopicName();
+ lastPayload = msg.getPayload().toString(StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public void onSubscribe(InterceptSubscribeMessage arg0) {
+ }
+
+ @Override
+ public void onUnsubscribe(InterceptUnsubscribeMessage arg0) {
+ }
+
+ public ArrayList getTopics() {
+ return topics;
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/TestMqttSubmodelObserver.java b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/TestMqttSubmodelObserver.java
new file mode 100644
index 000000000..75eab2b75
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/TestMqttSubmodelObserver.java
@@ -0,0 +1,191 @@
+/*******************************************************************************
+ * Copyright (C) 2024 the Eclipse BaSyx Authors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ ******************************************************************************/
+
+package org.eclipse.digitaltwin.basyx.submodelrepository.feature.mqtt;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.DeserializationException;
+import org.eclipse.digitaltwin.aas4j.v3.dataformat.json.JsonDeserializer;
+import org.eclipse.digitaltwin.aas4j.v3.model.Property;
+import org.eclipse.digitaltwin.aas4j.v3.model.Qualifier;
+import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty;
+import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultQualifier;
+import org.eclipse.digitaltwin.basyx.common.mqttcore.encoding.Base64URLEncoder;
+import org.eclipse.digitaltwin.basyx.common.mqttcore.serializer.SubmodelElementSerializer;
+import org.eclipse.digitaltwin.basyx.core.filerepository.InMemoryFileRepository;
+import org.eclipse.digitaltwin.basyx.submodelservice.DummySubmodelFactory;
+import org.eclipse.digitaltwin.basyx.submodelservice.InMemorySubmodelServiceFactory;
+import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelService;
+import org.eclipse.digitaltwin.basyx.submodelservice.SubmodelServiceFactory;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.mqtt.MqttSubmodelServiceFactory;
+import org.eclipse.digitaltwin.basyx.submodelservice.feature.mqtt.MqttSubmodelServiceTopicFactory;
+import org.eclipse.digitaltwin.basyx.submodelservice.value.PropertyValue;
+import org.eclipse.digitaltwin.basyx.submodelservice.value.SubmodelElementValue;
+import org.eclipse.paho.client.mqttv3.MqttClient;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.MqttSecurityException;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import io.moquette.broker.Server;
+import io.moquette.broker.config.ClasspathResourceLoader;
+import io.moquette.broker.config.IConfig;
+import io.moquette.broker.config.IResourceLoader;
+import io.moquette.broker.config.ResourceLoaderConfig;
+
+/**
+ * Tests events for submodelElements in SM Service
+ *
+ * @author rana
+ */
+public class TestMqttSubmodelObserver {
+
+ private static Server mqttBroker;
+ private static MqttClient mqttClient;
+ private static MqttTestListener listener;
+ private static MqttSubmodelServiceTopicFactory topicFactory = new MqttSubmodelServiceTopicFactory(new Base64URLEncoder());
+ private static SubmodelService submodelService;
+
+ @BeforeClass
+ public static void setUpClass() throws MqttException, IOException {
+ mqttBroker = startBroker();
+
+ listener = configureInterceptListener(mqttBroker);
+
+ mqttClient = createAndConnectClient();
+
+ submodelService = createMqttSubmodelService(mqttClient);
+ }
+
+ @AfterClass
+ public static void tearDownClass() {
+ mqttBroker.removeInterceptHandler(listener);
+ mqttBroker.stopServer();
+ }
+
+ @Test
+ public void createSubmodelElementEvent() throws DeserializationException {
+
+ SubmodelElement submodelElement = createSubmodelElementDummy("createSubmodelElementEventId");
+ submodelService.createSubmodelElement(submodelElement);
+
+ assertEquals(topicFactory.createCreateSubmodelElementTopic(submodelElement.getIdShort()), listener.lastTopic);
+ assertEquals(submodelElement, deserializeSubmodelElementPayload(listener.lastPayload));
+ }
+
+ @Test
+ public void updateSubmodelElementEvent() throws DeserializationException {
+
+ SubmodelElement submodelElement = createSubmodelElementDummy("updateSubmodelElementEventId");
+ submodelService.createSubmodelElement(submodelElement);
+
+ SubmodelElementValue value = new PropertyValue("updatedValue");
+ submodelService.setSubmodelElementValue(submodelElement.getIdShort(), value);
+
+ assertEquals(topicFactory.createUpdateSubmodelElementTopic(submodelElement.getIdShort()), listener.lastTopic);
+ assertEquals(submodelElement, deserializeSubmodelElementPayload(listener.lastPayload));
+ }
+
+ @Test
+ public void deleteSubmodelElementEvent() throws DeserializationException {
+
+ SubmodelElement submodelElement = createSubmodelElementDummy("deleteSubmodelElementEventId");
+ submodelService.createSubmodelElement(submodelElement);
+ submodelService.deleteSubmodelElement(submodelElement.getIdShort());
+
+ assertEquals(topicFactory.createDeleteSubmodelElementTopic(submodelElement.getIdShort()), listener.lastTopic);
+ assertEquals(submodelElement, deserializeSubmodelElementPayload(listener.lastPayload));
+ }
+
+ @Test
+ public void createSubmodelElementWithoutValueEvent() throws DeserializationException {
+
+ SubmodelElement submodelElement = createSubmodelElementDummy("noValueSubmodelElementEventId");
+ List qualifierList = createNoValueQualifierList();
+ submodelElement.setQualifiers(qualifierList);
+ submodelService.createSubmodelElement(submodelElement);
+
+ assertEquals(topicFactory.createCreateSubmodelElementTopic(submodelElement.getIdShort()), listener.lastTopic);
+ assertNotEquals(submodelElement, deserializeSubmodelElementPayload(listener.lastPayload));
+
+ ((Property) submodelElement).setValue(null);
+ assertEquals(submodelElement, deserializeSubmodelElementPayload(listener.lastPayload));
+ }
+
+ private List createNoValueQualifierList() {
+
+ Qualifier emptyValueQualifier = new DefaultQualifier.Builder().type(SubmodelElementSerializer.EMPTYVALUEUPDATE_TYPE).value("true").build();
+ return Arrays.asList(emptyValueQualifier);
+ }
+
+ private static SubmodelElement deserializeSubmodelElementPayload(String payload) throws DeserializationException {
+
+ return new JsonDeserializer().read(payload, SubmodelElement.class);
+ }
+
+ private SubmodelElement createSubmodelElementDummy(String submodelElementId) {
+ return new DefaultProperty.Builder().idShort(submodelElementId).value("defaultValue").build();
+ }
+
+ private static SubmodelService createMqttSubmodelService(MqttClient client) {
+
+ SubmodelServiceFactory repoFactory = new InMemorySubmodelServiceFactory(new InMemoryFileRepository());
+ return new MqttSubmodelServiceFactory(repoFactory, client, new MqttSubmodelServiceTopicFactory(new Base64URLEncoder())).create(DummySubmodelFactory.createSubmodelWithAllSubmodelElements());
+ }
+
+ private static MqttTestListener configureInterceptListener(Server broker) {
+
+ MqttTestListener testListener = new MqttTestListener();
+ broker.addInterceptHandler(testListener);
+
+ return testListener;
+ }
+
+ private static MqttClient createAndConnectClient() throws MqttException, MqttSecurityException {
+
+ MqttClient client = new MqttClient("tcp://localhost:1884", "testClient");
+ client.connect();
+ return client;
+ }
+
+ private static Server startBroker() throws IOException {
+
+ Server broker = new Server();
+ IResourceLoader classpathLoader = new ClasspathResourceLoader();
+
+ IConfig classPathConfig = new ResourceLoaderConfig(classpathLoader);
+ broker.startServer(classPathConfig);
+
+ return broker;
+ }
+}
diff --git a/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/resources/config/moquette.conf b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/resources/config/moquette.conf
new file mode 100644
index 000000000..8511afcd1
--- /dev/null
+++ b/basyx.submodelservice/basyx.submodelservice-feature-mqtt/src/test/resources/config/moquette.conf
@@ -0,0 +1,6 @@
+# Moquette Java Broker configuration file for testing
+
+# Do not use the default 1883 port
+port 1884
+host 0.0.0.0
+allow_anonymous true
\ No newline at end of file
diff --git a/basyx.submodelservice/pom.xml b/basyx.submodelservice/pom.xml
index dc199230c..f5916f017 100644
--- a/basyx.submodelservice/pom.xml
+++ b/basyx.submodelservice/pom.xml
@@ -18,6 +18,7 @@
basyx.submodelservice-core
basyx.submodelservice-http
basyx.submodelservice-backend-inmemory
+ basyx.submodelservice-feature-mqtt
basyx.submodelservice.example
basyx.submodelservice-client