This is an automated email from the ASF dual-hosted git repository.

ldywicki pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git


The following commit(s) were added to refs/heads/develop by this push:
     new 862efa519b Draft of metadata API with timestamp tracking capabilities.
862efa519b is described below

commit 862efa519bce134348a4ef40f0bea4e0795992a0
Author: Łukasz Dywicki <[email protected]>
AuthorDate: Fri Aug 9 19:46:44 2024 +0200

    Draft of metadata API with timestamp tracking capabilities.
    
    Signed-off-by: Łukasz Dywicki <[email protected]>
---
 .../{PlcTagResponse.java => PlcMetadataKeys.java}  |  24 +-
 .../plc4x/java/api/messages/PlcTagResponse.java    |   6 +
 .../apache/plc4x/java/api/metadata/Metadata.java   |  89 ++++++
 .../plc4x/java/api/metadata/time/TimeSource.java}  |  20 +-
 .../plc4x/java/ads/protocol/AdsProtocolLogic.java  |  65 ++++-
 .../apache/plc4x/java/opcua/OpcMetadataKeys.java}  |  25 +-
 .../java/opcua/protocol/OpcuaProtocolLogic.java    |  41 ++-
 .../opcua/protocol/OpcuaSubscriptionHandle.java    |  17 +-
 .../plc4x/java/opcua/tag/OpcuaQualityStatus.java   |  58 ++++
 .../apache/plc4x/java/s7/events/S7AlarmEvent.java  | 317 +++++++++++----------
 .../apache/plc4x/java/s7/events/S7CyclicEvent.java | 217 +++++---------
 .../apache/plc4x/java/s7/events/S7EventBase.java   |  59 ++++
 .../apache/plc4x/java/s7/events/S7ModeEvent.java   |  18 +-
 .../apache/plc4x/java/s7/events/S7SysEvent.java    |  55 ++--
 .../apache/plc4x/java/s7/events/S7UserEvent.java   |  13 +-
 .../s7/readwrite/protocol/S7ProtocolLogic.java     |   6 +-
 .../java/spi/messages/DefaultPlcReadResponse.java  |  30 ++
 .../spi/messages/DefaultPlcSubscriptionEvent.java  |  13 +-
 .../java/spi/messages/DefaultPlcWriteResponse.java |  30 ++
 .../plc4x/java/spi/metadata/DefaultMetadata.java   | 132 +++++++++
 .../driver/internal/validator/ApiValidator.java    |   5 +
 .../test/driver/xmlunit/SkipAttributeFilter.java}  |  22 +-
 .../driver/xmlunit/SkipDifferenceEvaluator.java    |  56 ++++
 .../resources/protocols/ads/DriverTestsuite.xml    |  12 +
 24 files changed, 907 insertions(+), 423 deletions(-)

diff --git 
a/plc4j/api/src/main/java/org/apache/plc4x/java/api/messages/PlcTagResponse.java
 
b/plc4j/api/src/main/java/org/apache/plc4x/java/api/messages/PlcMetadataKeys.java
similarity index 61%
copy from 
plc4j/api/src/main/java/org/apache/plc4x/java/api/messages/PlcTagResponse.java
copy to 
plc4j/api/src/main/java/org/apache/plc4x/java/api/messages/PlcMetadataKeys.java
index 3aa2d642de..e88ca207f3 100644
--- 
a/plc4j/api/src/main/java/org/apache/plc4x/java/api/messages/PlcTagResponse.java
+++ 
b/plc4j/api/src/main/java/org/apache/plc4x/java/api/messages/PlcMetadataKeys.java
@@ -7,7 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   https://www.apache.org/licenses/LICENSE-2.0
+ *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
@@ -16,26 +16,20 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.plc4x.java.api.messages;
 
-import org.apache.plc4x.java.api.model.PlcTag;
-import org.apache.plc4x.java.api.types.PlcResponseCode;
+package org.apache.plc4x.java.api.messages;
 
-import java.util.Collection;
+import org.apache.plc4x.java.api.metadata.Metadata.Key;
+import org.apache.plc4x.java.api.metadata.time.TimeSource;
 
 /**
- * Base type for all response messages sent as response for a prior request
- * from a plc to the plc4x system.
+ * High level definition of common metadata keys which can occur across 
multiple drivers.
  */
-public interface PlcTagResponse extends PlcResponse {
-
-    @Override
-    PlcTagRequest getRequest();
-
-    Collection<String> getTagNames();
+public interface PlcMetadataKeys {
 
-    PlcTag getTag(String name);
+    Key<Long> TIMESTAMP = Key.of("timestamp", Long.class);
+    Key<TimeSource> TIMESTAMP_SOURCE = Key.of("timestamp_source", 
TimeSource.class);
 
-    PlcResponseCode getResponseCode(String name);
+    Key<Long> RECEIVE_TIMESTAMP = Key.of("receive_timestamp", Long.class);
 
 }
diff --git 
a/plc4j/api/src/main/java/org/apache/plc4x/java/api/messages/PlcTagResponse.java
 
b/plc4j/api/src/main/java/org/apache/plc4x/java/api/messages/PlcTagResponse.java
index 3aa2d642de..1feae9f3bb 100644
--- 
a/plc4j/api/src/main/java/org/apache/plc4x/java/api/messages/PlcTagResponse.java
+++ 
b/plc4j/api/src/main/java/org/apache/plc4x/java/api/messages/PlcTagResponse.java
@@ -18,6 +18,7 @@
  */
 package org.apache.plc4x.java.api.messages;
 
+import org.apache.plc4x.java.api.metadata.Metadata;
 import org.apache.plc4x.java.api.model.PlcTag;
 import org.apache.plc4x.java.api.types.PlcResponseCode;
 
@@ -38,4 +39,9 @@ public interface PlcTagResponse extends PlcResponse {
 
     PlcResponseCode getResponseCode(String name);
 
+    /**
+     * Returns tag level metadata information.
+     */
+    Metadata getTagMetadata(String name);
+
 }
diff --git 
a/plc4j/api/src/main/java/org/apache/plc4x/java/api/metadata/Metadata.java 
b/plc4j/api/src/main/java/org/apache/plc4x/java/api/metadata/Metadata.java
new file mode 100644
index 0000000000..484fad7148
--- /dev/null
+++ b/plc4j/api/src/main/java/org/apache/plc4x/java/api/metadata/Metadata.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.plc4x.java.api.metadata;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+public interface Metadata {
+
+    Metadata EMPTY = new Metadata() {
+        @Override
+        public Set<Key<?>> keys() {
+            return Set.of();
+        }
+
+        @Override
+        public Map<Key<?>, Object> entries() {
+            return Map.of();
+        }
+
+        @Override
+        public Object getValue(Key<?> key) {
+            return null;
+        }
+
+    };
+
+    class Key<T> {
+
+        private final String key;
+        private final Class<T> type;
+
+        protected Key(String key, Class<T> type) {
+            this.key = key;
+            this.type = type;
+        }
+
+        public String getKey() {
+            return key;
+        }
+
+        public boolean validate(Object value) {
+            return type.isInstance(value);
+        }
+
+        public static <T> Key<T> of(String key, Class<T> type) {
+            return new Key<>(key, type);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (!(o instanceof Key)) {
+                return false;
+            }
+            Key<?> key1 = (Key<?>) o;
+            return Objects.equals(getKey(), key1.getKey()) && 
Objects.equals(type, key1.type);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(getKey(), type);
+        }
+    }
+
+    Set<Key<?>> keys();
+    Map<Key<?>, Object> entries();
+    Object getValue(Key<?> key);
+}
diff --git 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7UserEvent.java
 
b/plc4j/api/src/main/java/org/apache/plc4x/java/api/metadata/time/TimeSource.java
similarity index 63%
copy from 
plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7UserEvent.java
copy to 
plc4j/api/src/main/java/org/apache/plc4x/java/api/metadata/time/TimeSource.java
index d9f01d0dbd..84e5843635 100644
--- 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7UserEvent.java
+++ 
b/plc4j/api/src/main/java/org/apache/plc4x/java/api/metadata/time/TimeSource.java
@@ -7,7 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   https://www.apache.org/licenses/LICENSE-2.0
+ *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
@@ -16,14 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.plc4x.java.s7.events;
 
-import org.apache.plc4x.java.s7.readwrite.S7PayloadDiagnosticMessage;
+package org.apache.plc4x.java.api.metadata.time;
 
-public class S7UserEvent extends S7SysEvent {
+public enum TimeSource {
+
+    // Time information is assumed by PLC4X itself
+    ASSUMPTION,
+    // Time comes from software layer, kernel driver and similar
+    SOFTWARE,
+    // Time can is confronted through hardware i.e. microcontroller
+    HARDWARE,
+    // Other source of time which fall into separate truthiness category
+    OTHER
 
-    public S7UserEvent(S7PayloadDiagnosticMessage payload) {
-        super(payload);
-        map.put(Fields.TYPE.name(), "USER");
-    }
 }
diff --git 
a/plc4j/drivers/ads/src/main/java/org/apache/plc4x/java/ads/protocol/AdsProtocolLogic.java
 
b/plc4j/drivers/ads/src/main/java/org/apache/plc4x/java/ads/protocol/AdsProtocolLogic.java
index 08eaa141d0..7a58931ae7 100644
--- 
a/plc4j/drivers/ads/src/main/java/org/apache/plc4x/java/ads/protocol/AdsProtocolLogic.java
+++ 
b/plc4j/drivers/ads/src/main/java/org/apache/plc4x/java/ads/protocol/AdsProtocolLogic.java
@@ -34,6 +34,9 @@ import org.apache.plc4x.java.api.exceptions.PlcException;
 import org.apache.plc4x.java.api.exceptions.PlcInvalidTagException;
 import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
 import org.apache.plc4x.java.api.messages.*;
+import org.apache.plc4x.java.api.metadata.Metadata;
+import org.apache.plc4x.java.spi.metadata.DefaultMetadata;
+import org.apache.plc4x.java.api.metadata.time.TimeSource;
 import org.apache.plc4x.java.api.model.*;
 import org.apache.plc4x.java.api.types.PlcResponseCode;
 import org.apache.plc4x.java.api.types.PlcSubscriptionType;
@@ -718,8 +721,13 @@ public class AdsProtocolLogic extends 
Plc4xProtocolBase<AmsTCPPacket> implements
             .check(userdata -> userdata.getInvokeId() == 
amsPacket.getInvokeId())
             .only(AdsReadResponse.class)
             .handle(response -> {
+                // result metadata
+                Metadata metadata = new DefaultMetadata.Builder()
+                    .put(PlcMetadataKeys.RECEIVE_TIMESTAMP, 
System.currentTimeMillis())
+                    .put(PlcMetadataKeys.TIMESTAMP_SOURCE, 
TimeSource.ASSUMPTION)
+                    .build();
                 if (response.getResult() == ReturnCode.OK) {
-                    final PlcReadResponse plcReadResponse = 
convertToPlc4xReadResponse(readRequest, Map.of((AdsTag) 
readRequest.getTags().get(0), directAdsTag), response);
+                    final PlcReadResponse plcReadResponse = 
convertToPlc4xReadResponse(readRequest, Map.of((AdsTag) 
readRequest.getTags().get(0), directAdsTag), response, metadata);
                     // Convert the response from the PLC into a PLC4X Response 
...
                     future.complete(plcReadResponse);
                 } else {
@@ -791,8 +799,12 @@ public class AdsProtocolLogic extends 
Plc4xProtocolBase<AmsTCPPacket> implements
             .check(userdata -> userdata.getInvokeId() == 
amsPacket.getInvokeId())
             .only(AdsReadWriteResponse.class)
             .handle(response -> {
+                Metadata metadata = new DefaultMetadata.Builder()
+                    .put(PlcMetadataKeys.RECEIVE_TIMESTAMP, 
System.currentTimeMillis())
+                    .put(PlcMetadataKeys.TIMESTAMP_SOURCE, 
TimeSource.ASSUMPTION)
+                    .build();
                 if (response.getResult() == ReturnCode.OK) {
-                    final PlcReadResponse plcReadResponse = 
convertToPlc4xReadResponse(readRequest, resolvedTags, response);
+                    final PlcReadResponse plcReadResponse = 
convertToPlc4xReadResponse(readRequest, resolvedTags, response, metadata);
                     // Convert the response from the PLC into a PLC4X Response 
...
                     future.complete(plcReadResponse);
                 } else if (response.getResult() == 
ReturnCode.ADSERR_DEVICE_INVALIDSIZE) {
@@ -807,8 +819,9 @@ public class AdsProtocolLogic extends 
Plc4xProtocolBase<AmsTCPPacket> implements
         return future;
     }
 
-    protected PlcReadResponse convertToPlc4xReadResponse(PlcReadRequest 
readRequest, Map<AdsTag, DirectAdsTag> resolvedTags, AmsPacket adsData) {
+    protected PlcReadResponse convertToPlc4xReadResponse(PlcReadRequest 
readRequest, Map<AdsTag, DirectAdsTag> resolvedTags, AmsPacket adsData, 
Metadata responseMetadata) {
         ReadBuffer readBuffer = null;
+        Map<String, Metadata> metadata = new HashMap<>();
         Map<String, PlcResponseCode> responseCodes = new HashMap<>();
 
         // Read the response codes first
@@ -841,6 +854,7 @@ public class AdsProtocolLogic extends 
Plc4xProtocolBase<AmsTCPPacket> implements
         if (readBuffer != null) {
             Map<String, PlcResponseItem<PlcValue>> values = new HashMap<>();
             for (String tagName : readRequest.getTagNames()) {
+                metadata.put(tagName, new 
DefaultMetadata.Builder(responseMetadata).build());
                 // If the response-code was anything but OK, we don't need to 
parse the payload.
                 if(responseCodes.get(tagName) != PlcResponseCode.OK) {
                     values.put(tagName, new 
DefaultPlcResponseItem<>(responseCodes.get(tagName), null));
@@ -851,7 +865,7 @@ public class AdsProtocolLogic extends 
Plc4xProtocolBase<AmsTCPPacket> implements
                     values.put(tagName, parseResponseItem(directAdsTag, 
readBuffer));
                 }
             }
-            return new DefaultPlcReadResponse(readRequest, values);
+            return new DefaultPlcReadResponse(readRequest, values, metadata);
         }
         return null;
     }
@@ -1071,8 +1085,13 @@ public class AdsProtocolLogic extends 
Plc4xProtocolBase<AmsTCPPacket> implements
             .check(userdata -> userdata.getInvokeId() == 
amsPacket.getInvokeId())
             .only(AdsWriteResponse.class)
             .handle(response -> {
+                // result metadata
+                Metadata eventMetadata = new DefaultMetadata.Builder()
+                    .put(PlcMetadataKeys.RECEIVE_TIMESTAMP, 
System.currentTimeMillis())
+                    .put(PlcMetadataKeys.TIMESTAMP_SOURCE, 
TimeSource.ASSUMPTION)
+                    .build();
                 if (response.getResult() == ReturnCode.OK) {
-                    final PlcWriteResponse plcWriteResponse = 
convertToPlc4xWriteResponse(writeRequest, Collections.singletonMap((AdsTag) 
writeRequest.getTag(tagName), directAdsTag), response);
+                    final PlcWriteResponse plcWriteResponse = 
convertToPlc4xWriteResponse(writeRequest, Collections.singletonMap((AdsTag) 
writeRequest.getTag(tagName), directAdsTag), response, eventMetadata);
                     // Convert the response from the PLC into a PLC4X Response 
...
                     future.complete(plcWriteResponse);
                 } else {
@@ -1149,8 +1168,14 @@ public class AdsProtocolLogic extends 
Plc4xProtocolBase<AmsTCPPacket> implements
             .check(userdata -> userdata.getInvokeId() == 
amsPacket.getInvokeId())
             .only(AdsReadWriteResponse.class)
             .handle(response -> {
+                // result metadata
+                Metadata eventMetadata = new DefaultMetadata.Builder()
+                    .put(PlcMetadataKeys.RECEIVE_TIMESTAMP, 
System.currentTimeMillis())
+                    .put(PlcMetadataKeys.TIMESTAMP_SOURCE, 
TimeSource.ASSUMPTION)
+                    .build();
+
                 if (response.getResult() == ReturnCode.OK) {
-                    final PlcWriteResponse plcWriteResponse = 
convertToPlc4xWriteResponse(writeRequest, resolvedTags, response);
+                    final PlcWriteResponse plcWriteResponse = 
convertToPlc4xWriteResponse(writeRequest, resolvedTags, response, 
eventMetadata);
                     // Convert the response from the PLC into a PLC4X Response 
...
                     future.complete(plcWriteResponse);
                 } else {
@@ -1244,8 +1269,9 @@ public class AdsProtocolLogic extends 
Plc4xProtocolBase<AmsTCPPacket> implements
         }
     }
 
-    protected PlcWriteResponse convertToPlc4xWriteResponse(PlcWriteRequest 
writeRequest, Map<AdsTag, DirectAdsTag> resolvedTags, AmsPacket adsData) {
+    protected PlcWriteResponse convertToPlc4xWriteResponse(PlcWriteRequest 
writeRequest, Map<AdsTag, DirectAdsTag> resolvedTags, AmsPacket adsData, 
Metadata eventMtadata) {
         Map<String, PlcResponseCode> responseCodes = new HashMap<>();
+        Map<String, Metadata> metadata = new HashMap<>();
         if (adsData instanceof AdsWriteResponse) {
             AdsWriteResponse adsWriteResponse = (AdsWriteResponse) adsData;
             
responseCodes.put(writeRequest.getTagNames().stream().findFirst().orElse(""),
@@ -1256,6 +1282,9 @@ public class AdsProtocolLogic extends 
Plc4xProtocolBase<AmsTCPPacket> implements
             // When parsing a multi-item response, the error codes of each 
items come
             // in sequence and then come the values.
             for (String tagName : writeRequest.getTagNames()) {
+                // result metadata
+                metadata.put(tagName, eventMtadata);
+
                 AdsTag adsTag = (AdsTag) writeRequest.getTag(tagName);
                 // Skip invalid addresses.
                 if(resolvedTags.get(adsTag) == null) {
@@ -1271,7 +1300,7 @@ public class AdsProtocolLogic extends 
Plc4xProtocolBase<AmsTCPPacket> implements
             }
         }
 
-        return new DefaultPlcWriteResponse(writeRequest, responseCodes);
+        return new DefaultPlcWriteResponse(writeRequest, responseCodes, 
metadata);
     }
 
     @Override
@@ -1493,9 +1522,16 @@ public class AdsProtocolLogic extends 
Plc4xProtocolBase<AmsTCPPacket> implements
         if (msg.getUserdata() instanceof AdsDeviceNotificationRequest) {
             AdsDeviceNotificationRequest notificationData = 
(AdsDeviceNotificationRequest) msg.getUserdata();
             List<AdsStampHeader> stamps = 
notificationData.getAdsStampHeaders();
+            long receiveTs = System.currentTimeMillis();
             for (AdsStampHeader stamp : stamps) {
                 // convert Windows FILETIME format to unix epoch
                 long unixEpochTimestamp = 
stamp.getTimestamp().divide(BigInteger.valueOf(10000L)).longValue() - 
11644473600000L;
+                // result metadata
+                Metadata eventMetadata = new DefaultMetadata.Builder()
+                    .put(PlcMetadataKeys.RECEIVE_TIMESTAMP, receiveTs)
+                    .put(PlcMetadataKeys.TIMESTAMP, unixEpochTimestamp)
+                    .put(PlcMetadataKeys.TIMESTAMP_SOURCE, TimeSource.SOFTWARE)
+                    .build();
                 List<AdsNotificationSample> samples = 
stamp.getAdsNotificationSamples();
                 for (AdsNotificationSample sample : samples) {
                     long handle = sample.getNotificationHandle();
@@ -1503,10 +1539,12 @@ public class AdsProtocolLogic extends 
Plc4xProtocolBase<AmsTCPPacket> implements
                         for (PlcSubscriptionHandle subscriptionHandle : 
registration.getSubscriptionHandles()) {
                             if (subscriptionHandle instanceof 
AdsSubscriptionHandle) {
                                 AdsSubscriptionHandle adsHandle = 
(AdsSubscriptionHandle) subscriptionHandle;
-                                if (adsHandle.getNotificationHandle() == 
handle)
-                                    consumers.get(registration).accept(
-                                        new 
DefaultPlcSubscriptionEvent(Instant.ofEpochMilli(unixEpochTimestamp),
-                                            
convertSampleToPlc4XResult(adsHandle, sample.getData())));
+                                if (adsHandle.getNotificationHandle() == 
handle) {
+                                    Map<String, Metadata> metadata = new 
HashMap<>();
+                                    Instant timestamp = 
Instant.ofEpochMilli(unixEpochTimestamp);
+                                    DefaultPlcSubscriptionEvent event = new 
DefaultPlcSubscriptionEvent(timestamp, convertSampleToPlc4XResult(adsHandle, 
sample.getData(), metadata, eventMetadata));
+                                    consumers.get(registration).accept(event);
+                                }
                             }
                         }
                     }
@@ -1515,12 +1553,13 @@ public class AdsProtocolLogic extends 
Plc4xProtocolBase<AmsTCPPacket> implements
         }
     }
 
-    private Map<String, PlcResponseItem<PlcValue>> 
convertSampleToPlc4XResult(AdsSubscriptionHandle subscriptionHandle, byte[] 
data) throws
+    private Map<String, PlcResponseItem<PlcValue>> 
convertSampleToPlc4XResult(AdsSubscriptionHandle subscriptionHandle, byte[] 
data, Map<String, Metadata> tagMetadata, Metadata metadata) throws
         ParseException {
         Map<String, PlcResponseItem<PlcValue>> values = new HashMap<>();
         ReadBufferByteBased readBuffer = new ReadBufferByteBased(data, 
ByteOrder.LITTLE_ENDIAN);
         values.put(subscriptionHandle.getTagName(), new 
DefaultPlcResponseItem<>(PlcResponseCode.OK,
             DataItem.staticParse(readBuffer, 
getPlcValueTypeForAdsDataType(subscriptionHandle.getAdsDataType()), 
data.length)));
+        tagMetadata.put(subscriptionHandle.getTagName(), new 
DefaultMetadata.Builder(metadata).build());
         return values;
     }
 
diff --git 
a/plc4j/api/src/main/java/org/apache/plc4x/java/api/messages/PlcTagResponse.java
 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcMetadataKeys.java
similarity index 56%
copy from 
plc4j/api/src/main/java/org/apache/plc4x/java/api/messages/PlcTagResponse.java
copy to 
plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcMetadataKeys.java
index 3aa2d642de..713081159c 100644
--- 
a/plc4j/api/src/main/java/org/apache/plc4x/java/api/messages/PlcTagResponse.java
+++ 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcMetadataKeys.java
@@ -7,7 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   https://www.apache.org/licenses/LICENSE-2.0
+ *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
@@ -16,26 +16,21 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.plc4x.java.api.messages;
 
-import org.apache.plc4x.java.api.model.PlcTag;
-import org.apache.plc4x.java.api.types.PlcResponseCode;
+package org.apache.plc4x.java.opcua;
 
-import java.util.Collection;
+import org.apache.plc4x.java.api.metadata.Metadata;
+import org.apache.plc4x.java.api.metadata.Metadata.Key;
+import org.apache.plc4x.java.opcua.tag.OpcuaQualityStatus;
 
 /**
- * Base type for all response messages sent as response for a prior request
- * from a plc to the plc4x system.
+ * OPC UA level metadata keys.
  */
-public interface PlcTagResponse extends PlcResponse {
+public interface OpcMetadataKeys {
 
-    @Override
-    PlcTagRequest getRequest();
+    Key<OpcuaQualityStatus> QUALITY = Metadata.Key.of("opcua_quality", 
OpcuaQualityStatus.class);
 
-    Collection<String> getTagNames();
-
-    PlcTag getTag(String name);
-
-    PlcResponseCode getResponseCode(String name);
+    Key<Long> SERVER_TIMESTAMP = Metadata.Key.of("opcua_server_timestamp", 
Long.class);
+    Key<Long> SOURCE_TIMESTAMP = Metadata.Key.of("opcua_source_timestamp", 
Long.class);
 
 }
diff --git 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaProtocolLogic.java
 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaProtocolLogic.java
index 91112aef60..0ccf130a5b 100644
--- 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaProtocolLogic.java
+++ 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaProtocolLogic.java
@@ -26,18 +26,23 @@ import 
org.apache.plc4x.java.api.authentication.PlcAuthentication;
 import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
 import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
 import org.apache.plc4x.java.api.messages.*;
+import org.apache.plc4x.java.api.metadata.Metadata;
+import org.apache.plc4x.java.spi.metadata.DefaultMetadata;
+import org.apache.plc4x.java.api.metadata.time.TimeSource;
 import org.apache.plc4x.java.api.model.PlcConsumerRegistration;
 import org.apache.plc4x.java.api.model.PlcSubscriptionHandle;
 import org.apache.plc4x.java.api.model.PlcTag;
 import org.apache.plc4x.java.api.types.PlcResponseCode;
 import org.apache.plc4x.java.api.types.PlcValueType;
 import org.apache.plc4x.java.api.value.PlcValue;
+import org.apache.plc4x.java.opcua.OpcMetadataKeys;
 import org.apache.plc4x.java.opcua.config.OpcuaConfiguration;
 import org.apache.plc4x.java.opcua.context.Conversation;
 import org.apache.plc4x.java.opcua.context.OpcuaDriverContext;
 import org.apache.plc4x.java.opcua.context.SecureChannel;
 import org.apache.plc4x.java.opcua.readwrite.*;
 import org.apache.plc4x.java.opcua.tag.OpcuaPlcTagHandler;
+import org.apache.plc4x.java.opcua.tag.OpcuaQualityStatus;
 import org.apache.plc4x.java.opcua.tag.OpcuaTag;
 import org.apache.plc4x.java.spi.ConversationContext;
 import org.apache.plc4x.java.spi.Plc4xProtocolBase;
@@ -210,7 +215,7 @@ public class OpcuaProtocolLogic extends 
Plc4xProtocolBase<OpcuaAPU> implements H
 
         List<ExtensionObjectDefinition> readValueArray = new 
ArrayList<>(request.getTagNames().size());
         Iterator<String> iterator = request.getTagNames().iterator();
-        Map<String, PlcTag> tagMap = new HashMap<>();
+        Map<String, PlcTag> tagMap = new LinkedHashMap<>();
         for (int i = 0; i < request.getTagNames().size(); i++) {
             String tagName = iterator.next();
             // TODO: We need to check that the tag-return-code is OK as it 
could also be INVALID_TAG
@@ -237,7 +242,13 @@ public class OpcuaProtocolLogic extends 
Plc4xProtocolBase<OpcuaAPU> implements H
         transaction.submit(() -> {
             conversation.submit(opcuaReadRequest, 
ReadResponse.class).whenComplete((response, error) -> bridge(transaction, 
future, response, error));
         });
-        return future.thenApply(response -> new 
DefaultPlcReadResponse(request, readResponse(request.getTagNames(), tagMap, 
response.getResults())));
+        return future.thenApply(response -> {
+            Metadata responseMetadata = new DefaultMetadata.Builder()
+                .put(PlcMetadataKeys.RECEIVE_TIMESTAMP, 
System.currentTimeMillis())
+                .build();
+            Map<String, Metadata> metadata = new LinkedHashMap<>();
+            return new DefaultPlcReadResponse(request, readResponse(tagMap, 
response.getResults(), metadata, responseMetadata), metadata);
+        });
     }
 
     static NodeId generateNodeId(OpcuaTag tag) {
@@ -262,15 +273,16 @@ public class OpcuaProtocolLogic extends 
Plc4xProtocolBase<OpcuaAPU> implements H
         return nodeId;
     }
 
-    public Map<String, PlcResponseItem<PlcValue>> 
readResponse(LinkedHashSet<String> tagNames, Map<String, PlcTag> tagMap, 
List<DataValue> results) {
+    public Map<String, PlcResponseItem<PlcValue>> readResponse(Map<String, 
PlcTag> tagMap, List<DataValue> results, Map<String, Metadata> metadata, 
Metadata responseMetadata) {
         PlcResponseCode responseCode = null; // initialize variable
         Map<String, PlcResponseItem<PlcValue>> response = new HashMap<>();
-        int count = 0;
-        for (String tagName : tagNames) {
+        int index = 0;
+        for (String tagName : tagMap.keySet()) {
             PlcTag tag = tagMap.get(tagName);
             PlcValue value = null;
-            if (results.get(count).getValueSpecified()) {
-                Variant variant = results.get(count).getValue();
+            DataValue dataValue = results.get(index++);
+            if (dataValue.getValueSpecified()) {
+                Variant variant = dataValue.getValue();
                 LOGGER.trace("Response of type {}", 
variant.getClass().toString());
                 if (variant instanceof VariantBoolean) {
                     byte[] array = ((VariantBoolean) variant).getValue();
@@ -423,12 +435,20 @@ public class OpcuaProtocolLogic extends 
Plc4xProtocolBase<OpcuaAPU> implements H
                     responseCode = PlcResponseCode.OK;
                 }
             } else {
-                StatusCode statusCode = results.get(count).getStatusCode();
+                StatusCode statusCode = dataValue.getStatusCode();
                 responseCode = mapOpcStatusCode(statusCode.getStatusCode(), 
PlcResponseCode.UNSUPPORTED);
-                LOGGER.error("Error while reading value from OPC UA server 
error code: {}", results.get(count).getStatusCode().toString());
+                LOGGER.error("Error while reading value from OPC UA server 
error code:- " + dataValue.getStatusCode().toString());
             }
-            count++;
+
+            Metadata tagMetadata = new 
DefaultMetadata.Builder(responseMetadata)
+                .put(OpcMetadataKeys.QUALITY, new 
OpcuaQualityStatus(dataValue.getStatusCode()))
+                .put(OpcMetadataKeys.SERVER_TIMESTAMP, 
dataValue.getServerTimestamp())
+                .put(OpcMetadataKeys.SOURCE_TIMESTAMP, 
dataValue.getSourceTimestamp())
+                .put(PlcMetadataKeys.TIMESTAMP, dataValue.getServerTimestamp())
+                .put(PlcMetadataKeys.TIMESTAMP_SOURCE, TimeSource.SOFTWARE)
+                .build();
             response.put(tagName, new DefaultPlcResponseItem<>(responseCode, 
value));
+            metadata.put(tagName, tagMetadata);
         }
         return response;
     }
@@ -880,4 +900,5 @@ public class OpcuaProtocolLogic extends 
Plc4xProtocolBase<OpcuaAPU> implements H
             transaction.endRequest();
         }
     }
+
 }
diff --git 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java
 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java
index ab241b0411..4889480f3c 100644
--- 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java
+++ 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java
@@ -24,8 +24,11 @@ import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
+import org.apache.plc4x.java.api.messages.PlcMetadataKeys;
 import org.apache.plc4x.java.api.messages.PlcSubscriptionEvent;
 import org.apache.plc4x.java.api.messages.PlcSubscriptionRequest;
+import org.apache.plc4x.java.api.metadata.Metadata;
+import org.apache.plc4x.java.spi.metadata.DefaultMetadata;
 import org.apache.plc4x.java.api.model.PlcConsumerRegistration;
 import org.apache.plc4x.java.api.model.PlcTag;
 import org.apache.plc4x.java.api.value.PlcValue;
@@ -34,7 +37,6 @@ import org.apache.plc4x.java.opcua.tag.OpcuaTag;
 import org.apache.plc4x.java.opcua.readwrite.*;
 import org.apache.plc4x.java.spi.messages.DefaultPlcSubscriptionEvent;
 import org.apache.plc4x.java.spi.messages.utils.PlcResponseItem;
-import org.apache.plc4x.java.spi.messages.utils.PlcTagItem;
 import org.apache.plc4x.java.spi.model.DefaultPlcConsumerRegistration;
 import org.apache.plc4x.java.spi.model.DefaultPlcSubscriptionTag;
 import org.apache.plc4x.java.spi.model.DefaultPlcSubscriptionHandle;
@@ -260,17 +262,22 @@ public class OpcuaSubscriptionHandle extends 
DefaultPlcSubscriptionHandle {
      * @param values - array of data values to be sent to the client.
      */
     private void onSubscriptionValue(MonitoredItemNotification[] values) {
-        LinkedHashSet<String> tagNameList = new LinkedHashSet<>();
+        long receiveTs = System.currentTimeMillis();
+        Metadata responseMetadata = new DefaultMetadata.Builder()
+            .put(PlcMetadataKeys.RECEIVE_TIMESTAMP, receiveTs)
+            .build();
+
         List<DataValue> dataValues = new ArrayList<>(values.length);
         Map<String, PlcTag> tagMap = new LinkedHashMap<>();
         for (MonitoredItemNotification value : values) {
             String tagName = tagNames.get((int) value.getClientHandle() - 1);
-            tagNameList.add(tagName);
             tagMap.put(tagName, subscriptionRequest.getTag(tagName).getTag());
             dataValues.add(value.getValue());
         }
-        Map<String, PlcResponseItem<PlcValue>> tags = 
plcSubscriber.readResponse(tagNameList, tagMap, dataValues);
-        final PlcSubscriptionEvent event = new 
DefaultPlcSubscriptionEvent(Instant.now(), tags);
+
+        Map<String, Metadata> metadata = new HashMap<>();
+        Map<String, PlcResponseItem<PlcValue>> tags = 
plcSubscriber.readResponse(tagMap, dataValues, metadata, responseMetadata);
+        final PlcSubscriptionEvent event = new 
DefaultPlcSubscriptionEvent(Instant.now(), tags, metadata);
 
         consumers.forEach(plcSubscriptionEventConsumer -> 
plcSubscriptionEventConsumer.accept(event));
     }
diff --git 
a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaQualityStatus.java
 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaQualityStatus.java
new file mode 100644
index 0000000000..1500e21099
--- /dev/null
+++ 
b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/tag/OpcuaQualityStatus.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.plc4x.java.opcua.tag;
+
+import org.apache.plc4x.java.opcua.readwrite.StatusCode;
+
+public final class OpcuaQualityStatus {
+
+    private static final long STATUS_MASK = 0xC0000000L;
+    private static final long STATUS_GOOD = 0x00000000L;
+    private static final long STATUS_UNCERTAIN = 0x40000000L;
+    private static final long STATUS_BAD = 0x80000000L;
+
+    private final StatusCode statusCode;
+
+    public OpcuaQualityStatus(StatusCode statusCode) {
+        this.statusCode = statusCode;
+    }
+
+    public boolean isGood() {
+        return (statusCode.getStatusCode() & STATUS_MASK) == STATUS_GOOD;
+    }
+
+    public boolean isBad() {
+        return (statusCode.getStatusCode() & STATUS_MASK) == STATUS_BAD;
+    }
+
+    public boolean isUncertain() {
+        return (statusCode.getStatusCode() & STATUS_MASK) == STATUS_UNCERTAIN;
+    }
+
+    @Override
+    public String toString() {
+        if (isGood()) {
+            return "good";
+        } else if (isBad()) {
+            return "bad";
+        }
+        return "uncertain";
+    }
+}
diff --git 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7AlarmEvent.java
 
b/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7AlarmEvent.java
index 931bfab0a8..5d08cc6104 100644
--- 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7AlarmEvent.java
+++ 
b/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7AlarmEvent.java
@@ -18,6 +18,7 @@
  */
 package org.apache.plc4x.java.s7.events;
 
+import java.util.Collections;
 import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
 import org.apache.plc4x.java.api.messages.PlcReadRequest;
 import org.apache.plc4x.java.api.model.PlcTag;
@@ -33,7 +34,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-public class S7AlarmEvent implements S7Event {
+public class S7AlarmEvent extends S7EventBase {
 
 
     public enum Fields {
@@ -130,172 +131,19 @@ public class S7AlarmEvent implements S7Event {
 
     }
 
-    private final Instant timeStamp;
     private final Map<String, Object> map;
 
-    public S7AlarmEvent(Object obj) {
-        this.map = new HashMap<>();
-        if (obj instanceof S7PayloadAlarmAckInd) {
-            AlarmMessageAckPushType msg = ((S7PayloadAlarmAckInd) 
obj).getAlarmMessage();
-            DateAndTime dt = msg.getTimeStamp();
-            int year = (dt.getYear() >= 90) ? dt.getYear() + 1900 : 
dt.getYear() + 2000;
-            LocalDateTime ldt = LocalDateTime.of(year,
-                dt.getMonth(),
-                dt.getDay(),
-                dt.getHour(),
-                dt.getMinutes(),
-                dt.getSeconds(),
-                dt.getMsec() * 1000000);
-            this.timeStamp = ldt.toInstant(ZoneOffset.UTC);
-            map.put(S7SysEvent.Fields.TIMESTAMP.name(), this.timeStamp);
-
-            List<AlarmMessageAckObjectPushType> items = 
msg.getMessageObjects();
-            for (AlarmMessageAckObjectPushType item : items) {
-                map.put(Fields.EVENT_ID.name(), item.getEventId());
-                map.put(Fields.TYPE.name(), "ALARMACK_IND");
-                map.put(Fields.ASSOCIATED_VALUES.name(), 
item.getNumberOfValues());
-
-                map.put(Fields.SIG_1_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_1());
-                map.put(Fields.SIG_2_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_2());
-                map.put(Fields.SIG_3_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_3());
-                map.put(Fields.SIG_4_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_4());
-                map.put(Fields.SIG_5_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_5());
-                map.put(Fields.SIG_6_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_6());
-                map.put(Fields.SIG_7_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_7());
-                map.put(Fields.SIG_8_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_8());
-
-                map.put(Fields.SIG_1_DATA_COMING.name(), 
item.getAckStateComing().getSIG_1());
-                map.put(Fields.SIG_2_DATA_COMING.name(), 
item.getAckStateComing().getSIG_2());
-                map.put(Fields.SIG_3_DATA_COMING.name(), 
item.getAckStateComing().getSIG_3());
-                map.put(Fields.SIG_4_DATA_COMING.name(), 
item.getAckStateComing().getSIG_4());
-                map.put(Fields.SIG_5_DATA_COMING.name(), 
item.getAckStateComing().getSIG_5());
-                map.put(Fields.SIG_6_DATA_COMING.name(), 
item.getAckStateComing().getSIG_6());
-                map.put(Fields.SIG_7_DATA_COMING.name(), 
item.getAckStateComing().getSIG_7());
-                map.put(Fields.SIG_8_DATA_COMING.name(), 
item.getAckStateComing().getSIG_8());
-            }
-
-        } else {
-
-            AlarmMessagePushType msg = null;
-
-            if (obj instanceof S7PayloadAlarm8) {
-                msg = ((S7PayloadAlarm8) obj).getAlarmMessage();
-            } else if (obj instanceof S7PayloadNotify) {
-                msg = ((S7PayloadNotify) obj).getAlarmMessage();
-            } else if (obj instanceof S7PayloadAlarmSQ) {
-                msg = ((S7PayloadAlarmSQ) obj).getAlarmMessage();
-            } else if (obj instanceof S7PayloadAlarmS) {
-                msg = ((S7PayloadAlarmS) obj).getAlarmMessage();
-            } else if (obj instanceof S7PayloadNotify8) {
-                msg = ((S7PayloadNotify8) obj).getAlarmMessage();
-            } else {
-                throw new PlcRuntimeException("Unsupported type: " + 
obj.getClass().getName());
-            }
-
-            DateAndTime dt = msg.getTimeStamp();
-            int year = (dt.getYear() >= 90) ? dt.getYear() + 1900 : 
dt.getYear() + 2000;
-            LocalDateTime ldt = LocalDateTime.of(year,
-                dt.getMonth(),
-                dt.getDay(),
-                dt.getHour(),
-                dt.getMinutes(),
-                dt.getSeconds(),
-                dt.getMsec() * 1000000);
-            this.timeStamp = ldt.toInstant(ZoneOffset.UTC);
-            map.put(S7SysEvent.Fields.TIMESTAMP.name(), this.timeStamp);
-
-            List<AlarmMessageObjectPushType> items = msg.getMessageObjects();
-            for (AlarmMessageObjectPushType item : items) {
-                map.put(Fields.EVENT_ID.name(), item.getEventId());
-
-                if (obj instanceof S7PayloadAlarm8)
-                    map.put(Fields.TYPE.name(), "ALARM8");
-                if (obj instanceof S7PayloadNotify)
-                    map.put(Fields.TYPE.name(), "NOTIFY");
-                if (obj instanceof S7PayloadAlarmSQ)
-                    map.put(Fields.TYPE.name(), "ALARMSQ");
-                if (obj instanceof S7PayloadAlarmS)
-                    map.put(Fields.TYPE.name(), "ALARMS");
-                if (obj instanceof S7PayloadNotify8)
-                    map.put(Fields.TYPE.name(), "NOTIFY8");
-
-
-                map.put(Fields.ASSOCIATED_VALUES.name(), 
item.getNumberOfValues());
-
-
-                map.put(Fields.SIG_1.name(), item.getEventState().getSIG_1());
-                map.put(Fields.SIG_2.name(), item.getEventState().getSIG_2());
-                map.put(Fields.SIG_3.name(), item.getEventState().getSIG_3());
-                map.put(Fields.SIG_4.name(), item.getEventState().getSIG_4());
-                map.put(Fields.SIG_5.name(), item.getEventState().getSIG_5());
-                map.put(Fields.SIG_6.name(), item.getEventState().getSIG_6());
-                map.put(Fields.SIG_7.name(), item.getEventState().getSIG_7());
-                map.put(Fields.SIG_8.name(), item.getEventState().getSIG_8());
-
-
-                map.put(Fields.SIG_1_STATE.name(), 
item.getLocalState().getSIG_1());
-                map.put(Fields.SIG_2_STATE.name(), 
item.getLocalState().getSIG_2());
-                map.put(Fields.SIG_3_STATE.name(), 
item.getLocalState().getSIG_3());
-                map.put(Fields.SIG_4_STATE.name(), 
item.getLocalState().getSIG_4());
-                map.put(Fields.SIG_5_STATE.name(), 
item.getLocalState().getSIG_5());
-                map.put(Fields.SIG_6_STATE.name(), 
item.getLocalState().getSIG_6());
-                map.put(Fields.SIG_7_STATE.name(), 
item.getLocalState().getSIG_7());
-                map.put(Fields.SIG_8_STATE.name(), 
item.getLocalState().getSIG_8());
-
-                map.put(Fields.SIG_1_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_1());
-                map.put(Fields.SIG_2_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_2());
-                map.put(Fields.SIG_3_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_3());
-                map.put(Fields.SIG_4_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_4());
-                map.put(Fields.SIG_5_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_5());
-                map.put(Fields.SIG_6_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_6());
-                map.put(Fields.SIG_7_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_7());
-                map.put(Fields.SIG_8_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_8());
-
-                map.put(Fields.SIG_1_DATA_COMING.name(), 
item.getAckStateComing().getSIG_1());
-                map.put(Fields.SIG_2_DATA_COMING.name(), 
item.getAckStateComing().getSIG_2());
-                map.put(Fields.SIG_3_DATA_COMING.name(), 
item.getAckStateComing().getSIG_3());
-                map.put(Fields.SIG_4_DATA_COMING.name(), 
item.getAckStateComing().getSIG_4());
-                map.put(Fields.SIG_5_DATA_COMING.name(), 
item.getAckStateComing().getSIG_5());
-                map.put(Fields.SIG_6_DATA_COMING.name(), 
item.getAckStateComing().getSIG_6());
-                map.put(Fields.SIG_7_DATA_COMING.name(), 
item.getAckStateComing().getSIG_7());
-                map.put(Fields.SIG_8_DATA_COMING.name(), 
item.getAckStateComing().getSIG_8());
-
-                List<AssociatedValueType> values = item.getAssociatedValues();
-                int i = 1;
-                int j = 0;
-                for (AssociatedValueType value : values) {
-                    map.put("SIG_" + i + "_DATA_STATUS", 
value.getReturnCode().getValue());
-                    map.put("SIG_" + i + "_DATA_SIZE", 
value.getTransportSize().getValue());
-                    map.put("SIG_" + i + "_DATA_LENGTH", 
value.getValueLength());
-                    byte[] data = new byte[value.getData().size()];
-                    j = 0;
-                    for (short s : value.getData()) {
-                        data[j] = (byte) s;
-                        j++;
-                    }
-                    map.put("SIG_" + i + "_DATA", data);
-                    i++;
-                }
-
-            }
-
-        }
-
+    S7AlarmEvent(Instant timestamp, Map<String, Object> obj) {
+        super(timestamp);
+        this.map = obj;
     }
 
-    ;
-
 
     @Override
     public Map<String, Object> getMap() {
         return map;
     }
 
-    @Override
-    public Instant getTimestamp() {
-        return timeStamp;
-    }
-
     @Override
     public PlcReadRequest getRequest() {
         throw new UnsupportedOperationException("Not supported yet.");
@@ -671,5 +519,160 @@ public class S7AlarmEvent implements S7Event {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
+    public static S7AlarmEvent of(Object obj) {
+        if (obj instanceof S7PayloadAlarmAckInd) {
+            AlarmMessageAckPushType msg = ((S7PayloadAlarmAckInd) 
obj).getAlarmMessage();
+            DateAndTime dt = msg.getTimeStamp();
+            int year = (dt.getYear() >= 90) ? dt.getYear() + 1900 : 
dt.getYear() + 2000;
+            LocalDateTime ldt = LocalDateTime.of(year,
+                dt.getMonth(),
+                dt.getDay(),
+                dt.getHour(),
+                dt.getMinutes(),
+                dt.getSeconds(),
+                dt.getMsec() * 1000000);
+            Instant timeStamp = ldt.toInstant(ZoneOffset.UTC);
+
+            Map<String, Object> map = new HashMap<>();
+            map.put(S7SysEvent.Fields.TIMESTAMP.name(), timeStamp);
+
+            List<AlarmMessageAckObjectPushType> items = 
msg.getMessageObjects();
+            for (AlarmMessageAckObjectPushType item : items) {
+                map.put(Fields.EVENT_ID.name(), item.getEventId());
+                map.put(Fields.TYPE.name(), "ALARMACK_IND");
+                map.put(Fields.ASSOCIATED_VALUES.name(), 
item.getNumberOfValues());
+
+                map.put(Fields.SIG_1_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_1());
+                map.put(Fields.SIG_2_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_2());
+                map.put(Fields.SIG_3_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_3());
+                map.put(Fields.SIG_4_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_4());
+                map.put(Fields.SIG_5_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_5());
+                map.put(Fields.SIG_6_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_6());
+                map.put(Fields.SIG_7_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_7());
+                map.put(Fields.SIG_8_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_8());
+
+                map.put(Fields.SIG_1_DATA_COMING.name(), 
item.getAckStateComing().getSIG_1());
+                map.put(Fields.SIG_2_DATA_COMING.name(), 
item.getAckStateComing().getSIG_2());
+                map.put(Fields.SIG_3_DATA_COMING.name(), 
item.getAckStateComing().getSIG_3());
+                map.put(Fields.SIG_4_DATA_COMING.name(), 
item.getAckStateComing().getSIG_4());
+                map.put(Fields.SIG_5_DATA_COMING.name(), 
item.getAckStateComing().getSIG_5());
+                map.put(Fields.SIG_6_DATA_COMING.name(), 
item.getAckStateComing().getSIG_6());
+                map.put(Fields.SIG_7_DATA_COMING.name(), 
item.getAckStateComing().getSIG_7());
+                map.put(Fields.SIG_8_DATA_COMING.name(), 
item.getAckStateComing().getSIG_8());
+            }
+            return new S7AlarmEvent(timeStamp, map);
+        } else {
+
+            AlarmMessagePushType msg = null;
+
+            if (obj instanceof S7PayloadAlarm8) {
+                msg = ((S7PayloadAlarm8) obj).getAlarmMessage();
+            } else if (obj instanceof S7PayloadNotify) {
+                msg = ((S7PayloadNotify) obj).getAlarmMessage();
+            } else if (obj instanceof S7PayloadAlarmSQ) {
+                msg = ((S7PayloadAlarmSQ) obj).getAlarmMessage();
+            } else if (obj instanceof S7PayloadAlarmS) {
+                msg = ((S7PayloadAlarmS) obj).getAlarmMessage();
+            } else if (obj instanceof S7PayloadNotify8) {
+                msg = ((S7PayloadNotify8) obj).getAlarmMessage();
+            } else {
+                throw new PlcRuntimeException("Unsupported type: " + 
obj.getClass().getName());
+            }
+
+            DateAndTime dt = msg.getTimeStamp();
+            int year = (dt.getYear() >= 90) ? dt.getYear() + 1900 : 
dt.getYear() + 2000;
+            LocalDateTime ldt = LocalDateTime.of(year,
+                dt.getMonth(),
+                dt.getDay(),
+                dt.getHour(),
+                dt.getMinutes(),
+                dt.getSeconds(),
+                dt.getMsec() * 1000000);
+            Instant timeStamp = ldt.toInstant(ZoneOffset.UTC);
+
+            Map<String, Object> map = new HashMap<>();
+            map.put(S7SysEvent.Fields.TIMESTAMP.name(), timeStamp);
+
+            List<AlarmMessageObjectPushType> items = msg.getMessageObjects();
+            for (AlarmMessageObjectPushType item : items) {
+                map.put(Fields.EVENT_ID.name(), item.getEventId());
+
+                if (obj instanceof S7PayloadAlarm8) {
+                    map.put(Fields.TYPE.name(), "ALARM8");
+                }
+                if (obj instanceof S7PayloadNotify) {
+                    map.put(Fields.TYPE.name(), "NOTIFY");
+                }
+                if (obj instanceof S7PayloadAlarmSQ) {
+                    map.put(Fields.TYPE.name(), "ALARMSQ");
+                }
+                if (obj instanceof S7PayloadAlarmS) {
+                    map.put(Fields.TYPE.name(), "ALARMS");
+                }
+                if (obj instanceof S7PayloadNotify8) {
+                    map.put(Fields.TYPE.name(), "NOTIFY8");
+                }
+
+                map.put(Fields.ASSOCIATED_VALUES.name(), 
item.getNumberOfValues());
+
+                map.put(Fields.SIG_1.name(), item.getEventState().getSIG_1());
+                map.put(Fields.SIG_2.name(), item.getEventState().getSIG_2());
+                map.put(Fields.SIG_3.name(), item.getEventState().getSIG_3());
+                map.put(Fields.SIG_4.name(), item.getEventState().getSIG_4());
+                map.put(Fields.SIG_5.name(), item.getEventState().getSIG_5());
+                map.put(Fields.SIG_6.name(), item.getEventState().getSIG_6());
+                map.put(Fields.SIG_7.name(), item.getEventState().getSIG_7());
+                map.put(Fields.SIG_8.name(), item.getEventState().getSIG_8());
+
+
+                map.put(Fields.SIG_1_STATE.name(), 
item.getLocalState().getSIG_1());
+                map.put(Fields.SIG_2_STATE.name(), 
item.getLocalState().getSIG_2());
+                map.put(Fields.SIG_3_STATE.name(), 
item.getLocalState().getSIG_3());
+                map.put(Fields.SIG_4_STATE.name(), 
item.getLocalState().getSIG_4());
+                map.put(Fields.SIG_5_STATE.name(), 
item.getLocalState().getSIG_5());
+                map.put(Fields.SIG_6_STATE.name(), 
item.getLocalState().getSIG_6());
+                map.put(Fields.SIG_7_STATE.name(), 
item.getLocalState().getSIG_7());
+                map.put(Fields.SIG_8_STATE.name(), 
item.getLocalState().getSIG_8());
+
+                map.put(Fields.SIG_1_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_1());
+                map.put(Fields.SIG_2_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_2());
+                map.put(Fields.SIG_3_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_3());
+                map.put(Fields.SIG_4_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_4());
+                map.put(Fields.SIG_5_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_5());
+                map.put(Fields.SIG_6_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_6());
+                map.put(Fields.SIG_7_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_7());
+                map.put(Fields.SIG_8_DATA_GOING.name(), 
item.getAckStateGoing().getSIG_8());
+
+                map.put(Fields.SIG_1_DATA_COMING.name(), 
item.getAckStateComing().getSIG_1());
+                map.put(Fields.SIG_2_DATA_COMING.name(), 
item.getAckStateComing().getSIG_2());
+                map.put(Fields.SIG_3_DATA_COMING.name(), 
item.getAckStateComing().getSIG_3());
+                map.put(Fields.SIG_4_DATA_COMING.name(), 
item.getAckStateComing().getSIG_4());
+                map.put(Fields.SIG_5_DATA_COMING.name(), 
item.getAckStateComing().getSIG_5());
+                map.put(Fields.SIG_6_DATA_COMING.name(), 
item.getAckStateComing().getSIG_6());
+                map.put(Fields.SIG_7_DATA_COMING.name(), 
item.getAckStateComing().getSIG_7());
+                map.put(Fields.SIG_8_DATA_COMING.name(), 
item.getAckStateComing().getSIG_8());
+
+                List<AssociatedValueType> values = item.getAssociatedValues();
+                int i = 1;
+                int j = 0;
+                for (AssociatedValueType value : values) {
+                    map.put("SIG_" + i + "_DATA_STATUS", 
value.getReturnCode().getValue());
+                    map.put("SIG_" + i + "_DATA_SIZE", 
value.getTransportSize().getValue());
+                    map.put("SIG_" + i + "_DATA_LENGTH", 
value.getValueLength());
+                    byte[] data = new byte[value.getData().size()];
+                    j = 0;
+                    for (short s : value.getData()) {
+                        data[j] = (byte) s;
+                        j++;
+                    }
+                    map.put("SIG_" + i + "_DATA", data);
+                    i++;
+                }
+            }
+
+            return new S7AlarmEvent(timeStamp, map);
+        }
+
+    }
 
 }
diff --git 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7CyclicEvent.java
 
b/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7CyclicEvent.java
index 6b7c83d3bf..9c0d0fcd83 100644
--- 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7CyclicEvent.java
+++ 
b/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7CyclicEvent.java
@@ -17,12 +17,14 @@
  * under the License.
  */
 package org.apache.plc4x.java.s7.events;
-
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import static io.netty.buffer.Unpooled.wrappedBuffer;
+import org.apache.plc4x.java.api.messages.PlcMetadataKeys;
 import org.apache.plc4x.java.api.messages.PlcReadRequest;
 import org.apache.plc4x.java.api.messages.PlcSubscriptionRequest;
+import org.apache.plc4x.java.spi.metadata.DefaultMetadata;
+import org.apache.plc4x.java.api.metadata.time.TimeSource;
 import org.apache.plc4x.java.api.model.PlcTag;
 import org.apache.plc4x.java.api.types.PlcResponseCode;
 import org.apache.plc4x.java.api.value.PlcValue;
@@ -31,7 +33,6 @@ import 
org.apache.plc4x.java.s7.readwrite.S7PayloadUserDataItemCyclicServicesCha
 import 
org.apache.plc4x.java.s7.readwrite.S7PayloadUserDataItemCyclicServicesPush;
 import 
org.apache.plc4x.java.s7.readwrite.S7PayloadUserDataItemCyclicServicesSubscribeResponse;
 import org.apache.plc4x.java.s7.readwrite.utils.StaticHelper;
-
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.nio.charset.Charset;
@@ -43,8 +44,7 @@ import 
org.apache.plc4x.java.s7.readwrite.tag.S7SubscriptionTag;
 import org.apache.plc4x.java.s7.readwrite.tag.S7Tag;
 import org.apache.plc4x.java.spi.model.DefaultPlcSubscriptionTag;
 import org.apache.plc4x.java.spi.values.DefaultPlcValueHandler;
-
-public class S7CyclicEvent implements S7Event {
+public class S7CyclicEvent extends S7EventBase implements S7Event {
 
     public enum Fields {
         TYPE,
@@ -57,110 +57,104 @@ public class S7CyclicEvent implements S7Event {
         TRANSPORTSIZE_,
         DATA_
     }
-
     private final PlcSubscriptionRequest request;
-
-    private final Instant timeStamp;
     private final Map<String, Object> map;
-
     public S7CyclicEvent(PlcSubscriptionRequest request, short jobid, 
S7PayloadUserDataItemCyclicServicesPush event) {
+        super(Instant.now(), new DefaultMetadata.Builder()
+            .put(PlcMetadataKeys.TIMESTAMP_SOURCE, TimeSource.ASSUMPTION)
+            .build()
+        );
         this.map = new HashMap<>();
-        this.timeStamp = Instant.now();
         this.request = request;
         map.put(Fields.TYPE.name(), "CYCEVENT");
-        map.put(Fields.TIMESTAMP.name(), this.timeStamp);
+        map.put(Fields.TIMESTAMP.name(), getTimestamp());
         map.put(Fields.JOBID.name(), jobid);
         map.put(Fields.ITEMSCOUNT.name(), event.getItemsCount());
-        int[] n = new int[1];        
+        int[] n = new int[1];
 
         request.getTagNames().forEach(tagname -> {
             int i = n[0];
             map.put(Fields.RETURNCODE_.name() + i, 
event.getItems().get(i).getReturnCode().getValue());
             map.put(Fields.TRANSPORTSIZE_.name() + i, 
event.getItems().get(i).getTransportSize().getValue());
             map.put(tagname, dataToPlcValue(tagname, request, 
event.getItems().get(i).getData()));
-            n[0]++;                                    
+            n[0]++;
         });
-
     }
-
     public S7CyclicEvent(PlcSubscriptionRequest request, short jobid, 
S7PayloadUserDataItemCyclicServicesChangeDrivenPush event) {
+        super(Instant.now(), new DefaultMetadata.Builder()
+            .put(PlcMetadataKeys.TIMESTAMP_SOURCE, TimeSource.ASSUMPTION)
+            .build()
+        );
         this.map = new HashMap<>();
-        this.timeStamp = Instant.now();
         this.request = request;
         map.put(Fields.TYPE.name(), "CYCEVENT");
-        map.put(Fields.TIMESTAMP.name(), this.timeStamp);
+        map.put(Fields.TIMESTAMP.name(), getTimestamp());
         map.put(Fields.JOBID.name(), jobid);
         map.put(Fields.ITEMSCOUNT.name(), event.getItemsCount());
         int[] n = new int[1];
-        
+       
         request.getTagNames().forEach(tagname -> {
             int i = n[0];
             map.put(Fields.RETURNCODE_.name() + i, 
event.getItems().get(i).getReturnCode().getValue());
             map.put(Fields.TRANSPORTSIZE_.name() + i, 
event.getItems().get(i).getTransportSize().getValue());
             map.put(tagname, dataToPlcValue(tagname, request, 
event.getItems().get(i).getData()));
-            n[0]++;                                    
+            n[0]++;
         });
-        
+       
     }
-
     public S7CyclicEvent(PlcSubscriptionRequest request, short jobid, 
S7PayloadUserDataItemCyclicServicesSubscribeResponse event) {
+        super(Instant.now(), new DefaultMetadata.Builder()
+            .put(PlcMetadataKeys.TIMESTAMP_SOURCE, TimeSource.ASSUMPTION)
+            .build()
+        );
         this.map = new HashMap<>();
-        this.timeStamp = Instant.now();
         this.request = request;
         map.put(Fields.TYPE.name(), "CYCEVENT");
-        map.put(Fields.TIMESTAMP.name(), this.timeStamp);
+        map.put(Fields.TIMESTAMP.name(), getTimestamp());
         map.put(Fields.JOBID.name(), jobid);
         map.put(Fields.ITEMSCOUNT.name(), event.getItemsCount());
         int[] n = new int[1];
-
         request.getTagNames().forEach(tagname -> {
             int i = n[0];
             map.put(Fields.RETURNCODE_.name() + i, 
event.getItems().get(i).getReturnCode().getValue());
             map.put(Fields.TRANSPORTSIZE_.name() + i, 
event.getItems().get(i).getTransportSize().getValue());
             map.put(tagname, dataToPlcValue(tagname, request, 
event.getItems().get(i).getData()));
-            n[0]++;                                    
-        });       
+            n[0]++;
+        });
     }
-
     public S7CyclicEvent(PlcSubscriptionRequest request, short jobid, 
S7PayloadUserDataItemCyclicServicesChangeDrivenSubscribeResponse event) {
+        super(Instant.now(), new DefaultMetadata.Builder()
+            .put(PlcMetadataKeys.TIMESTAMP_SOURCE, TimeSource.ASSUMPTION)
+            .build()
+        );
         this.map = new HashMap<>();
-        this.timeStamp = Instant.now();
         this.request = request;
         map.put(Fields.TYPE.name(), "CYCEVENT");
-        map.put(Fields.TIMESTAMP.name(), this.timeStamp);
+        map.put(Fields.TIMESTAMP.name(), getTimestamp());
         map.put(Fields.JOBID.name(), jobid);
         map.put(Fields.ITEMSCOUNT.name(), event.getItemsCount());
         int[] n = new int[1];
-        
+       
         request.getTagNames().forEach(tagname -> {
             int i = n[0];
             map.put(Fields.RETURNCODE_.name() + i, 
event.getItems().get(i).getReturnCode().getValue());
             map.put(Fields.TRANSPORTSIZE_.name() + i, 
event.getItems().get(i).getTransportSize().getValue());
             map.put(tagname, dataToPlcValue(tagname, request, 
event.getItems().get(i).getData()));
-            n[0]++;                                    
-        });       
+            n[0]++;
+        });
     }
-
     @Override
     public Map<String, Object> getMap() {
         return this.map;
     }
-
-    @Override
-    public Instant getTimestamp() {
-        return this.timeStamp;
-    }
-
     @Override
     public PlcReadRequest getRequest() {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public PlcValue getAsPlcValue() {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public PlcValue getPlcValue(String name) {
         if (request.getTagNames().contains(name)) {
@@ -170,33 +164,27 @@ public class S7CyclicEvent implements S7Event {
         }
         return null;
     }
-
     @Override
     public int getNumberOfValues(String name) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public Object getObject(String name) {
         if ("REQUEST".equals(name)) return request;
         return null;
     }
-
     @Override
     public Object getObject(String name, int index) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public Collection<Object> getAllObjects(String name) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public boolean isValidBoolean(String name) {
         return isValidBoolean(name, 0);
     }
-
     @Override
     public boolean isValidBoolean(String name, int index) {
         try {
@@ -206,12 +194,10 @@ public class S7CyclicEvent implements S7Event {
             return false;
         }
     }
-
     @Override
     public Boolean getBoolean(String name) {
         return getBoolean(name, 0);
     }
-
     @Override
     public Boolean getBoolean(String name, int index) {
         if (!(map.get(name) instanceof byte[])) {
@@ -220,17 +206,14 @@ public class S7CyclicEvent implements S7Event {
         ByteBuf byteBuf = Unpooled.wrappedBuffer((byte[]) map.get(name));
         return byteBuf.getBoolean(index);
     }
-
     @Override
     public Collection<Boolean> getAllBooleans(String name) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public boolean isValidByte(String name) {
         return isValidByte(name, 0);
     }
-
     @Override
     public boolean isValidByte(String name, int index) {
         try {
@@ -240,12 +223,10 @@ public class S7CyclicEvent implements S7Event {
             return false;
         }
     }
-
     @Override
     public Byte getByte(String name) {
         return getByte(name, 0);
     }
-
     @Override
     public Byte getByte(String name, int index) {
         if (!(map.get(name) instanceof byte[])) {
@@ -255,7 +236,6 @@ public class S7CyclicEvent implements S7Event {
         int pos = index * Byte.BYTES;
         return byteBuf.getByte(pos);
     }
-
     @Override
     public Collection<Byte> getAllBytes(String name) {
         if (!(map.get(name) instanceof byte[])) {
@@ -264,12 +244,10 @@ public class S7CyclicEvent implements S7Event {
         byte[] array = (byte[]) map.get(name);
         return IntStream.range(0, array.length).mapToObj(i -> 
array[i]).collect(Collectors.toList());
     }
-
     @Override
     public boolean isValidShort(String name) {
         return isValidShort(name, 0);
     }
-
     @Override
     public boolean isValidShort(String name, int index) {
         try {
@@ -279,12 +257,10 @@ public class S7CyclicEvent implements S7Event {
             return false;
         }
     }
-
     @Override
     public Short getShort(String name) {
         return getShort(name, 0);
     }
-
     @Override
     public Short getShort(String name, int index) {
         if (!(map.get(name) instanceof byte[])) {
@@ -294,7 +270,6 @@ public class S7CyclicEvent implements S7Event {
         int pos = index * Short.BYTES;
         return byteBuf.getShort(pos);
     }
-
     @Override
     public Collection<Short> getAllShorts(String name) {
         if (!(map.get(name) instanceof byte[])) {
@@ -307,12 +282,10 @@ public class S7CyclicEvent implements S7Event {
         }
         return list;
     }
-
     @Override
     public boolean isValidInteger(String name) {
         return isValidInteger(name, 0);
     }
-
     @Override
     public boolean isValidInteger(String name, int index) {
         try {
@@ -322,12 +295,10 @@ public class S7CyclicEvent implements S7Event {
             return false;
         }
     }
-
     @Override
     public Integer getInteger(String name) {
         return getInteger(name, 0);
     }
-
     @Override
     public Integer getInteger(String name, int index) {
         if (!(map.get(name) instanceof byte[])) {
@@ -337,7 +308,6 @@ public class S7CyclicEvent implements S7Event {
         int pos = index * Integer.BYTES;
         return byteBuf.getInt(pos);
     }
-
     @Override
     public Collection<Integer> getAllIntegers(String name) {
         if (!(map.get(name) instanceof byte[])) {
@@ -350,37 +320,30 @@ public class S7CyclicEvent implements S7Event {
         }
         return list;
     }
-
     @Override
     public boolean isValidBigInteger(String name) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public boolean isValidBigInteger(String name, int index) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public BigInteger getBigInteger(String name) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public BigInteger getBigInteger(String name, int index) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public Collection<BigInteger> getAllBigIntegers(String name) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public boolean isValidLong(String name) {
         return isValidLong(name, 0);
     }
-
     @Override
     public boolean isValidLong(String name, int index) {
         try {
@@ -390,12 +353,10 @@ public class S7CyclicEvent implements S7Event {
             return false;
         }
     }
-
     @Override
     public Long getLong(String name) {
         return getLong(name, 0);
     }
-
     @Override
     public Long getLong(String name, int index) {
         if (!(map.get(name) instanceof byte[])) {
@@ -405,7 +366,6 @@ public class S7CyclicEvent implements S7Event {
         int pos = index * Long.BYTES;
         return byteBuf.getLong(pos);
     }
-
     @Override
     public Collection<Long> getAllLongs(String name) {
         if (!(map.get(name) instanceof byte[])) {
@@ -418,12 +378,10 @@ public class S7CyclicEvent implements S7Event {
         }
         return list;
     }
-
     @Override
     public boolean isValidFloat(String name) {
         return isValidFloat(name, 0);
     }
-
     @Override
     public boolean isValidFloat(String name, int index) {
         try {
@@ -433,12 +391,10 @@ public class S7CyclicEvent implements S7Event {
             return false;
         }
     }
-
     @Override
     public Float getFloat(String name) {
         return getFloat(name, 0);
     }
-
     @Override
     public Float getFloat(String name, int index) {
         if (!(map.get(name) instanceof byte[])) {
@@ -448,7 +404,6 @@ public class S7CyclicEvent implements S7Event {
         int pos = index * Float.BYTES;
         return byteBuf.getFloat(pos);
     }
-
     @Override
     public Collection<Float> getAllFloats(String name) {
         if (!(map.get(name) instanceof byte[])) {
@@ -461,12 +416,10 @@ public class S7CyclicEvent implements S7Event {
         }
         return list;
     }
-
     @Override
     public boolean isValidDouble(String name) {
         return isValidDouble(name, 0);
     }
-
     @Override
     public boolean isValidDouble(String name, int index) {
         try {
@@ -476,12 +429,10 @@ public class S7CyclicEvent implements S7Event {
             return false;
         }
     }
-
     @Override
     public Double getDouble(String name) {
         return getDouble(name, 0);
     }
-
     @Override
     public Double getDouble(String name, int index) {
         if (!(map.get(name) instanceof byte[])) {
@@ -491,7 +442,6 @@ public class S7CyclicEvent implements S7Event {
         int pos = index * Double.BYTES;
         return byteBuf.getDouble(pos);
     }
-
     @Override
     public Collection<Double> getAllDoubles(String name) {
         if (!(map.get(name) instanceof byte[])) {
@@ -504,37 +454,30 @@ public class S7CyclicEvent implements S7Event {
         }
         return list;
     }
-
     @Override
     public boolean isValidBigDecimal(String name) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public boolean isValidBigDecimal(String name, int index) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public BigDecimal getBigDecimal(String name) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public BigDecimal getBigDecimal(String name, int index) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public Collection<BigDecimal> getAllBigDecimals(String name) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public boolean isValidString(String name) {
         return isValidString(name, 0);
     }
-
     @Override
     public boolean isValidString(String name, int index) {
         try {
@@ -544,7 +487,6 @@ public class S7CyclicEvent implements S7Event {
             return false;
         }
     }
-
     @Override
     public String getString(String name) {
         if (!(map.get(name) instanceof byte[])) {
@@ -553,22 +495,18 @@ public class S7CyclicEvent implements S7Event {
         ByteBuf byteBuf = Unpooled.wrappedBuffer((byte[]) map.get(name));
         return byteBuf.toString(Charset.defaultCharset());
     }
-
     @Override
     public String getString(String name, int index) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public Collection<String> getAllStrings(String name) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public boolean isValidTime(String name) {
         return isValidTime(name, 0);
     }
-
     @Override
     public boolean isValidTime(String name, int index) {
         try {
@@ -578,12 +516,10 @@ public class S7CyclicEvent implements S7Event {
             return false;
         }
     }
-
     @Override
     public LocalTime getTime(String name) {
         return getTime(name, 0);
     }
-
     /*
      * In S7, data type TIME occupies one double word.
      * The value is in milliseconds (ms).
@@ -599,7 +535,6 @@ public class S7CyclicEvent implements S7Event {
         Duration dr = StaticHelper.s7TimeToDuration(value);
         return LocalTime.of(dr.toHoursPart(), dr.toMinutesPart(), 
dr.toSecondsPart(), dr.toNanosPart());
     }
-
     @Override
     public Collection<LocalTime> getAllTimes(String name) {
         if (!(map.get(name) instanceof byte[])) {
@@ -613,12 +548,10 @@ public class S7CyclicEvent implements S7Event {
         }
         return items;
     }
-
     @Override
     public boolean isValidDate(String name) {
         return isValidDate(name, 0);
     }
-
     @Override
     public boolean isValidDate(String name, int index) {
         try {
@@ -628,12 +561,10 @@ public class S7CyclicEvent implements S7Event {
             return false;
         }
     }
-
     @Override
     public LocalDate getDate(String name) {
         return getDate(name, 0);
     }
-
     @Override
     public LocalDate getDate(String name, int index) {
         if (!(map.get(name) instanceof byte[])) {
@@ -644,7 +575,6 @@ public class S7CyclicEvent implements S7Event {
         short value = byteBuf.getShort(pos);
         return StaticHelper.s7DateToLocalDate(value);
     }
-
     @Override
     public Collection<LocalDate> getAllDates(String name) {
         if (!(map.get(name) instanceof byte[])) {
@@ -658,12 +588,10 @@ public class S7CyclicEvent implements S7Event {
         }
         return items;
     }
-
     @Override
     public boolean isValidDateTime(String name) {
         return isValidDateTime(name, 0);
     }
-
     @Override
     public boolean isValidDateTime(String name, int index) {
         try {
@@ -673,12 +601,10 @@ public class S7CyclicEvent implements S7Event {
             return false;
         }
     }
-
     @Override
     public LocalDateTime getDateTime(String name) {
         return getDateTime(name, 0);
     }
-
     @Override
     public LocalDateTime getDateTime(String name, int index) {
         if (!(map.get(name) instanceof byte[])) {
@@ -688,7 +614,6 @@ public class S7CyclicEvent implements S7Event {
         int pos = index * Long.BYTES;
         return StaticHelper.s7DateTimeToLocalDateTime(byteBuf.slice(pos, 
Long.BYTES));
     }
-
     @Override
     public Collection<LocalDateTime> getAllDateTimes(String name) {
         if (!(map.get(name) instanceof byte[])) {
@@ -702,22 +627,18 @@ public class S7CyclicEvent implements S7Event {
         }
         return items;
     }
-
     @Override
     public Collection<String> getTagNames() {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public PlcTag getTag(String name) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public PlcResponseCode getResponseCode(String name) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
-
     @Override
     public boolean equals(Object obj) {
         if (this == obj) {
@@ -730,7 +651,7 @@ public class S7CyclicEvent implements S7Event {
             return false;
         }
         final S7CyclicEvent other = (S7CyclicEvent) obj;
-        
+       
         for (String tag:request.getTagNames()) {
             final PlcValue othervalue = other.getPlcValue(tag);
             if (othervalue == null) return false;
@@ -739,7 +660,7 @@ public class S7CyclicEvent implements S7Event {
                 return false;
             }
         };
-        
+       
         return true;
     }
     
@@ -747,23 +668,22 @@ public class S7CyclicEvent implements S7Event {
     private static PlcValue dataToPlcValue(String tagname, 
PlcSubscriptionRequest request, List<Short> data){
         
         int[] i = new int[1];
-        
+       
         final byte[] buffer = new byte[data.size()];
-
         data.forEach( b -> {
-            buffer[i[0]] = b.byteValue(); 
+            buffer[i[0]] = b.byteValue();
             i[0]++;
         });
-                
+               
         ByteBuf bb = wrappedBuffer(buffer);
-        
-        
+       
+       
         final DefaultPlcSubscriptionTag  dpst = (DefaultPlcSubscriptionTag) 
request.getTag(tagname);
         final S7SubscriptionTag subTag = (S7SubscriptionTag) dpst.getTag();
         final S7Tag[] s7Tags = subTag.getS7Tags();
-        
+       
         PlcValue plcValue = null;
-        
+       
         switch(s7Tags[0].getDataType()){
             case BOOL:
                 Boolean[] bools = new Boolean[s7Tags[0].getNumberOfElements()];
@@ -781,11 +701,11 @@ public class S7CyclicEvent implements S7Event {
                 plcValue = DefaultPlcValueHandler.of(s7Tags[0], bytes);
                 break;
             case WORD:
-                break;  
+                break;
             case DWORD:
-                break; 
+                break;
             case LWORD:
-                break;                  
+                break;
             case INT:
                 Short[] shorts = new Short[s7Tags[0].getNumberOfElements()];
                 for (int iter = 0; iter < s7Tags[0].getNumberOfElements(); 
iter ++) {
@@ -794,11 +714,11 @@ public class S7CyclicEvent implements S7Event {
                 plcValue = DefaultPlcValueHandler.of(s7Tags[0], shorts);
                 break;
             case UINT:
-                break;    
+                break;
             case SINT:
-                break;   
+                break;
             case USINT:
-                break;  
+                break;
             case DINT:
                 // TODO: This looks suspicious
                 Integer[] integers = new Integer[bb.capacity() / Integer.SIZE];
@@ -808,7 +728,7 @@ public class S7CyclicEvent implements S7Event {
                 plcValue = DefaultPlcValueHandler.of(s7Tags[0], integers);
                 break;
             case UDINT:
-                break;    
+                break;
             case LINT:
                 // TODO: This looks suspicious
                 Long[] longs = new Long[bb.capacity() / Long.SIZE];
@@ -818,7 +738,7 @@ public class S7CyclicEvent implements S7Event {
                 plcValue = DefaultPlcValueHandler.of(s7Tags[0], longs);
                 break;
             case ULINT:
-                break;  
+                break;
             case REAL:
                 // TODO: This looks suspicious
                 Float[] floats = new Float[bb.capacity() / Float.SIZE];
@@ -836,42 +756,41 @@ public class S7CyclicEvent implements S7Event {
                 plcValue = DefaultPlcValueHandler.of(s7Tags[0], doubles);
                 break;
             case CHAR:
-                break;   
+                break;
             case WCHAR:
-                break;   
+                break;
             case STRING:
-                break; 
+                break;
             case WSTRING:
-                break;      
+                break;
             case S5TIME:
                 break;
             case TIME:
-                break; 
+                break;
             case LTIME:
-                break;    
+                break;
             case DATE:
-                break; 
+                break;
             case TIME_OF_DAY:
-                break;      
+                break;
             case TOD:
-                break;                 
+                break;
             case LTIME_OF_DAY:
-                break;    
+                break;
             case LTOD:
-                break;   
+                break;
             case DATE_AND_TIME:
-                break; 
+                break;
             case DT:
-                break; 
+                break;
             case DATE_AND_LTIME:
-                break;                 
+                break;
             case LDT:
-                break;  
+                break;
             case DTL:
-                break;                  
+                break;
         }
-        
+       
         return plcValue;
     }
-
 }
diff --git 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7EventBase.java
 
b/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7EventBase.java
new file mode 100644
index 0000000000..7580b72923
--- /dev/null
+++ 
b/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7EventBase.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.plc4x.java.s7.events;
+
+import java.time.Instant;
+import org.apache.plc4x.java.api.messages.PlcMetadataKeys;
+import org.apache.plc4x.java.api.metadata.Metadata;
+import org.apache.plc4x.java.spi.metadata.DefaultMetadata;
+import org.apache.plc4x.java.api.metadata.time.TimeSource;
+
+public abstract class S7EventBase implements S7Event {
+
+    private final Instant timestamp;
+    private final Metadata metadata;
+
+    S7EventBase() {
+        this(Instant.now());
+    }
+
+    S7EventBase(Instant timestamp) {
+        this(timestamp, new DefaultMetadata.Builder()
+            .put(PlcMetadataKeys.TIMESTAMP, timestamp.getEpochSecond())
+            .put(PlcMetadataKeys.TIMESTAMP_SOURCE, TimeSource.HARDWARE) // 
event triggered by PLC itself
+            .build()
+        );
+    }
+
+    S7EventBase(Instant timestamp, Metadata metadata) {
+        this.timestamp = timestamp;
+        this.metadata = metadata;
+    }
+
+    @Override
+    public Metadata getTagMetadata(String name) {
+        return metadata;
+    }
+
+    @Override
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+}
diff --git 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7ModeEvent.java
 
b/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7ModeEvent.java
index b144ad8510..4a9d53f204 100644
--- 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7ModeEvent.java
+++ 
b/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7ModeEvent.java
@@ -18,7 +18,10 @@
  */
 package org.apache.plc4x.java.s7.events;
 
+import org.apache.plc4x.java.api.messages.PlcMetadataKeys;
 import org.apache.plc4x.java.api.messages.PlcReadRequest;
+import org.apache.plc4x.java.spi.metadata.DefaultMetadata;
+import org.apache.plc4x.java.api.metadata.time.TimeSource;
 import org.apache.plc4x.java.api.model.PlcTag;
 import org.apache.plc4x.java.api.types.PlcResponseCode;
 import org.apache.plc4x.java.api.value.PlcValue;
@@ -34,7 +37,7 @@ import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 
-public class S7ModeEvent implements S7Event {
+public class S7ModeEvent extends S7EventBase implements S7Event {
 
 
     public enum Fields {
@@ -46,17 +49,19 @@ public class S7ModeEvent implements S7Event {
         CURRENT_MODE
     }
 
-    private final Instant timeStamp;
     private final Map<String, Object> map;
 
     public S7ModeEvent(S7ParameterModeTransition parameter) {
+        super(Instant.now(), new DefaultMetadata.Builder()
+            .put(PlcMetadataKeys.TIMESTAMP_SOURCE, TimeSource.ASSUMPTION)
+            .build()
+        );
         this.map = new HashMap<>();
         map.put(Fields.TYPE.name(), "MODE");
         map.put(Fields.METHOD.name(), parameter.getMethod());
         map.put(Fields.FUNCTION.name(), parameter.getCpuFunctionType());
         map.put(Fields.CURRENT_MODE.name(), parameter.getCurrentMode());
-        this.timeStamp = Instant.now();
-        map.put(Fields.TIMESTAMP.name(), this.timeStamp);
+        map.put(Fields.TIMESTAMP.name(), getTimestamp());
         // TODO: Is this really correct, to put the map itself in itself?
         map.put(Fields.MAP.name(), map);
     }
@@ -67,11 +72,6 @@ public class S7ModeEvent implements S7Event {
         return map;
     }
 
-    @Override
-    public Instant getTimestamp() {
-        return timeStamp;
-    }
-
     @Override
     public PlcReadRequest getRequest() {
         throw new UnsupportedOperationException("Not supported yet.");
diff --git 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7SysEvent.java
 
b/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7SysEvent.java
index 4fab9b97ac..364adc443d 100644
--- 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7SysEvent.java
+++ 
b/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7SysEvent.java
@@ -32,7 +32,7 @@ import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 
-public class S7SysEvent implements S7Event {
+public class S7SysEvent extends S7EventBase implements S7Event {
 
     public enum Fields {
         TIMESTAMP,
@@ -45,30 +45,11 @@ public class S7SysEvent implements S7Event {
         INFO2
     }
 
-    private final Instant timeStamp;
     protected final Map<String, Object> map;
 
-    public S7SysEvent(S7PayloadDiagnosticMessage payload) {
-        this.map = new HashMap();
-        map.put(Fields.TYPE.name(), "SYS");
-        map.put(Fields.EVENT_ID.name(), payload.getEventId());
-        map.put(Fields.PRIORITY_CLASS.name(), payload.getPriorityClass());
-        map.put(Fields.OB_NUMBER.name(), payload.getObNumber());
-        map.put(Fields.DAT_ID.name(), payload.getDatId());
-        map.put(Fields.INFO1.name(), payload.getInfo1());
-        map.put(Fields.INFO2.name(), payload.getInfo2());
-
-        DateAndTime dt = payload.getTimeStamp();
-        int year = (dt.getYear() >= 90) ? dt.getYear() + 1900 : dt.getYear() + 
2000;
-        LocalDateTime ldt = LocalDateTime.of(year,
-            dt.getMonth(),
-            dt.getDay(),
-            dt.getHour(),
-            dt.getMinutes(),
-            dt.getSeconds(),
-            dt.getMsec() * 1000000);
-        this.timeStamp = ldt.toInstant(ZoneOffset.UTC);
-        map.put(Fields.TIMESTAMP.name(), this.timeStamp);
+    S7SysEvent(Instant instant, Map<String, Object> map) {
+        super(instant);
+        this.map = map;
     }
 
     @Override
@@ -76,11 +57,6 @@ public class S7SysEvent implements S7Event {
         return map;
     }
 
-    @Override
-    public Instant getTimestamp() {
-        return timeStamp;
-    }
-
     @Override
     public PlcReadRequest getRequest() {
         throw new UnsupportedOperationException("Not supported yet.");
@@ -456,4 +432,27 @@ public class S7SysEvent implements S7Event {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
+    public static S7SysEvent of(S7PayloadDiagnosticMessage payload) {
+        Map<String, Object> map = new HashMap<>();
+        map.put(Fields.TYPE.name(), "SYS");
+        map.put(Fields.EVENT_ID.name(), payload.getEventId());
+        map.put(Fields.PRIORITY_CLASS.name(), payload.getPriorityClass());
+        map.put(Fields.OB_NUMBER.name(), payload.getObNumber());
+        map.put(Fields.DAT_ID.name(), payload.getDatId());
+        map.put(Fields.INFO1.name(), payload.getInfo1());
+        map.put(Fields.INFO2.name(), payload.getInfo2());
+
+        DateAndTime dt = payload.getTimeStamp();
+        int year = (dt.getYear() >= 90) ? dt.getYear() + 1900 : dt.getYear() + 
2000;
+        LocalDateTime ldt = LocalDateTime.of(year,
+            dt.getMonth(),
+            dt.getDay(),
+            dt.getHour(),
+            dt.getMinutes(),
+            dt.getSeconds(),
+            dt.getMsec() * 1000000);
+        Instant timestamp = ldt.toInstant(ZoneOffset.UTC);
+        map.put(Fields.TIMESTAMP.name(), timestamp);
+        return new S7SysEvent(timestamp, map);
+    }
 }
diff --git 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7UserEvent.java
 
b/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7UserEvent.java
index d9f01d0dbd..d5805fc25e 100644
--- 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7UserEvent.java
+++ 
b/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7UserEvent.java
@@ -18,12 +18,21 @@
  */
 package org.apache.plc4x.java.s7.events;
 
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
 import org.apache.plc4x.java.s7.readwrite.S7PayloadDiagnosticMessage;
 
 public class S7UserEvent extends S7SysEvent {
 
-    public S7UserEvent(S7PayloadDiagnosticMessage payload) {
-        super(payload);
+    S7UserEvent(Instant instant, Map<String, Object> map) {
+        super(instant, map);
+    }
+
+    public static S7UserEvent of(S7PayloadDiagnosticMessage payload) {
+        S7SysEvent event = S7SysEvent.of(payload);
+        Map<String, Object> map = new HashMap<>(event.getMap());
         map.put(Fields.TYPE.name(), "USER");
+        return new S7UserEvent(event.getTimestamp(), map);
     }
 }
diff --git 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/readwrite/protocol/S7ProtocolLogic.java
 
b/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/readwrite/protocol/S7ProtocolLogic.java
index 8f4eee69c2..97aaa7835a 100644
--- 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/readwrite/protocol/S7ProtocolLogic.java
+++ 
b/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/readwrite/protocol/S7ProtocolLogic.java
@@ -1570,10 +1570,10 @@ public class S7ProtocolLogic extends 
Plc4xProtocolBase<TPKTPacket> {
                             if (item instanceof S7PayloadDiagnosticMessage) {
                                 final S7PayloadDiagnosticMessage pload = 
(S7PayloadDiagnosticMessage) item; 
                                 if ((pload.getEventId() >= 0x0A000) & 
(pload.getEventId() <= 0x0BFFF)) {
-                                    S7UserEvent userEvent = new 
S7UserEvent(pload);
+                                    S7UserEvent userEvent = 
S7UserEvent.of(pload);
                                     eventQueue.add(userEvent);                 
               
                                 } else {
-                                    S7SysEvent sysEvent = new 
S7SysEvent(pload);
+                                    S7SysEvent sysEvent = S7SysEvent.of(pload);
                                     eventQueue.add(sysEvent);                  
                      
                                 }
                             } 
@@ -1589,7 +1589,7 @@ public class S7ProtocolLogic extends 
Plc4xProtocolBase<TPKTPacket> {
                             (myParameter.getCpuSubfunction() == 0x16))) { 
//(04)
                         
                         payload.getItems().forEach(item ->{
-                            S7AlarmEvent alrmEvent = new S7AlarmEvent(item);
+                            S7AlarmEvent alrmEvent = S7AlarmEvent.of(item);
                             eventQueue.add(alrmEvent);                         
       
                         });
                         
diff --git 
a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/messages/DefaultPlcReadResponse.java
 
b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/messages/DefaultPlcReadResponse.java
index 5983e519f6..f0ee410501 100644
--- 
a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/messages/DefaultPlcReadResponse.java
+++ 
b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/messages/DefaultPlcReadResponse.java
@@ -18,10 +18,13 @@
  */
 package org.apache.plc4x.java.spi.messages;
 
+import java.util.Map.Entry;
 import org.apache.plc4x.java.api.exceptions.PlcInvalidTagException;
 import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
 import org.apache.plc4x.java.api.messages.PlcReadRequest;
 import org.apache.plc4x.java.api.messages.PlcReadResponse;
+import org.apache.plc4x.java.api.metadata.Metadata;
+import org.apache.plc4x.java.spi.metadata.DefaultMetadata;
 import org.apache.plc4x.java.api.model.PlcTag;
 import org.apache.plc4x.java.api.types.PlcResponseCode;
 import org.apache.plc4x.java.spi.generation.SerializationException;
@@ -46,11 +49,19 @@ public class DefaultPlcReadResponse implements 
PlcReadResponse, Serializable {
 
     private final PlcReadRequest request;
     private final Map<String, PlcResponseItem<PlcValue>> values;
+    private final Map<String, Metadata> metadata;
 
     public DefaultPlcReadResponse(PlcReadRequest request,
                                   Map<String, PlcResponseItem<PlcValue>> 
values) {
+        this(request, values, Collections.emptyMap());
+    }
+
+    public DefaultPlcReadResponse(PlcReadRequest request,
+                                  Map<String, PlcResponseItem<PlcValue>> 
values,
+                                  Map<String, Metadata> metadata) {
         this.request = request;
         this.values = values;
+        this.metadata = Collections.unmodifiableMap(metadata);
     }
 
     @Override
@@ -58,6 +69,11 @@ public class DefaultPlcReadResponse implements 
PlcReadResponse, Serializable {
         return request;
     }
 
+    @Override
+    public Metadata getTagMetadata(String tag) {
+        return metadata.getOrDefault(tag, DefaultMetadata.EMPTY);
+    }
+
     @Override
     public PlcValue getAsPlcValue() {
         Map<String, PlcValue> structMap = new HashMap<>();
@@ -669,6 +685,20 @@ public class DefaultPlcReadResponse implements 
PlcReadResponse, Serializable {
         }
         writeBuffer.popContext("values");
 
+        if (metadata != null && !metadata.isEmpty()) {
+            writeBuffer.pushContext("metadata", WithRenderAsList(true));
+
+            for (Entry<String, Metadata> entry : metadata.entrySet()) {
+                if (entry.getValue() instanceof Serializable) {
+                    writeBuffer.pushContext(entry.getKey());
+                    ((Serializable) entry.getValue()).serialize(writeBuffer);
+                    writeBuffer.popContext(entry.getKey());
+                }
+            }
+
+            writeBuffer.popContext("metadata");
+        }
+
         writeBuffer.popContext("PlcReadResponse");
     }
 
diff --git 
a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/messages/DefaultPlcSubscriptionEvent.java
 
b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/messages/DefaultPlcSubscriptionEvent.java
index 2035f76e00..e4ef1835aa 100644
--- 
a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/messages/DefaultPlcSubscriptionEvent.java
+++ 
b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/messages/DefaultPlcSubscriptionEvent.java
@@ -18,7 +18,10 @@
  */
 package org.apache.plc4x.java.spi.messages;
 
+import java.util.Collections;
 import org.apache.plc4x.java.api.messages.PlcSubscriptionEvent;
+import org.apache.plc4x.java.api.metadata.Metadata;
+import org.apache.plc4x.java.spi.metadata.DefaultMetadata;
 import org.apache.plc4x.java.api.model.PlcTag;
 import org.apache.plc4x.java.api.value.PlcValue;
 import org.apache.plc4x.java.spi.messages.utils.PlcResponseItem;
@@ -32,8 +35,14 @@ public class DefaultPlcSubscriptionEvent extends 
DefaultPlcReadResponse implemen
     public final Instant timestamp;
 
     public DefaultPlcSubscriptionEvent(Instant timestamp,
-                                       Map<String, PlcResponseItem<PlcValue>> 
tags) {
-        super(null, tags);
+        Map<String, PlcResponseItem<PlcValue>> tags) {
+        this(timestamp, tags, Collections.emptyMap());
+    }
+
+    public DefaultPlcSubscriptionEvent(Instant timestamp,
+                                       Map<String, PlcResponseItem<PlcValue>> 
tags,
+                                       Map<String, Metadata> metadata) {
+        super(null, tags, metadata);
         this.timestamp = timestamp;
     }
 
diff --git 
a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/messages/DefaultPlcWriteResponse.java
 
b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/messages/DefaultPlcWriteResponse.java
index 329792956d..96ff7ad066 100644
--- 
a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/messages/DefaultPlcWriteResponse.java
+++ 
b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/messages/DefaultPlcWriteResponse.java
@@ -18,8 +18,11 @@
  */
 package org.apache.plc4x.java.spi.messages;
 
+import java.util.Collections;
+import java.util.Map.Entry;
 import org.apache.plc4x.java.api.messages.PlcWriteRequest;
 import org.apache.plc4x.java.api.messages.PlcWriteResponse;
+import org.apache.plc4x.java.api.metadata.Metadata;
 import org.apache.plc4x.java.api.model.PlcTag;
 import org.apache.plc4x.java.api.types.PlcResponseCode;
 import org.apache.plc4x.java.spi.generation.SerializationException;
@@ -36,11 +39,19 @@ public class DefaultPlcWriteResponse implements 
PlcWriteResponse, Serializable {
 
     private final PlcWriteRequest request;
     private final Map<String, PlcResponseCode> responseCodes;
+    private final Map<String, Metadata> metadata;
 
     public DefaultPlcWriteResponse(PlcWriteRequest request,
                                    Map<String, PlcResponseCode> responseCodes) 
{
+        this(request, responseCodes, Collections.emptyMap());
+    }
+
+    public DefaultPlcWriteResponse(PlcWriteRequest request,
+                                   Map<String, PlcResponseCode> responseCodes,
+                                   Map<String, Metadata> metadata) {
         this.request = request;
         this.responseCodes = responseCodes;
+        this.metadata = metadata;
     }
 
     @Override
@@ -48,6 +59,11 @@ public class DefaultPlcWriteResponse implements 
PlcWriteResponse, Serializable {
         return request;
     }
 
+    @Override
+    public Metadata getTagMetadata(String tag) {
+        return metadata.getOrDefault(tag, Metadata.EMPTY);
+    }
+
     @Override
     public Collection<String> getTagNames() {
         return request.getTagNames();
@@ -83,6 +99,20 @@ public class DefaultPlcWriteResponse implements 
PlcWriteResponse, Serializable {
         }
         writeBuffer.popContext("responseCodes");
 
+        if (metadata != null && !metadata.isEmpty()) {
+            writeBuffer.pushContext("metadata", WithRenderAsList(true));
+
+            for (Entry<String, Metadata> entry : metadata.entrySet()) {
+                if (entry.getValue() instanceof Serializable) {
+                    writeBuffer.pushContext(entry.getKey());
+                    ((Serializable) entry.getValue()).serialize(writeBuffer);
+                    writeBuffer.popContext(entry.getKey());
+                }
+            }
+
+            writeBuffer.popContext("metadata");
+        }
+
         writeBuffer.popContext("PlcWriteResponse");
     }
 
diff --git 
a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/metadata/DefaultMetadata.java
 
b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/metadata/DefaultMetadata.java
new file mode 100644
index 0000000000..55bd78b422
--- /dev/null
+++ 
b/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/metadata/DefaultMetadata.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.plc4x.java.spi.metadata;
+
+import static 
org.apache.plc4x.java.spi.generation.WithReaderWriterArgs.WithRenderAsList;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.plc4x.java.api.metadata.Metadata;
+import org.apache.plc4x.java.spi.generation.SerializationException;
+import org.apache.plc4x.java.spi.generation.WriteBuffer;
+import org.apache.plc4x.java.spi.utils.Serializable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DefaultMetadata implements Metadata, Serializable {
+
+    private final Metadata parent;
+    private final Map<Key<?>, Object> values;
+
+    DefaultMetadata(Map<Key<?>, Object> values) {
+        this(values, Metadata.EMPTY);
+    }
+
+    public DefaultMetadata(Map<Key<?>, Object> values, Metadata parent) {
+        this.values = new LinkedHashMap<>(values);
+        this.parent = Objects.requireNonNull(parent, "Parent metadata must not 
be null");
+    }
+
+    @Override
+    public Set<Key<?>> keys() {
+        Set<Key<?>> keys = new LinkedHashSet<>(values.keySet());
+        keys.addAll(parent.keys());
+        return Collections.unmodifiableSet(keys);
+    }
+
+    @Override
+    public Map<Key<?>, Object> entries() {
+        Map<Key<?>, Object> copy = new LinkedHashMap<>(parent.entries());
+        copy.putAll(values);
+        return Map.copyOf(copy);
+    }
+
+    @Override
+    public Object getValue(Key<?> key) {
+        Object value = values.get(key);
+        if (value == null) {
+            return parent.getValue(key);
+        }
+        return value;
+    }
+
+    @Override
+    public void serialize(WriteBuffer writeBuffer) throws 
SerializationException {
+            for (Key<?> metadataKey : keys()) {
+            writeBuffer.pushContext("entry", WithRenderAsList(false));
+            writeBuffer.writeString("key", metadataKey.getKey().length(), 
metadataKey.getKey());
+            String value = "" + getValue(metadataKey);
+            writeBuffer.writeString("value", value.length(), value);
+            writeBuffer.popContext("entry");
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof Metadata)) {
+            return false;
+        }
+        Metadata that = (Metadata) o;
+        return Objects.equals(entries(), that.entries());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(entries());
+    }
+
+    public static class Builder {
+        private final Logger logger = LoggerFactory.getLogger(Builder.class);
+
+        private final Map<Key<?>, Object> values = new LinkedHashMap<>();
+        private final Metadata parent;
+
+        public Builder() {
+            this(DefaultMetadata.EMPTY);
+        }
+
+        public Builder(Metadata parent) {
+            this.parent = parent;
+        }
+
+        public <T> Builder put(Key<T> key, T value) {
+            if (!key.validate(value)) {
+                logger.debug("Ignore metadata value {}, it does not match 
constraints imposed by key {}", value, key);
+                return this;
+            }
+
+            values.put(key, value);
+            return this;
+        }
+
+        public Metadata build() {
+            return new DefaultMetadata(values, parent);
+        }
+    }
+
+}
diff --git 
a/plc4j/utils/test-utils/src/main/java/org/apache/plc4x/test/driver/internal/validator/ApiValidator.java
 
b/plc4j/utils/test-utils/src/main/java/org/apache/plc4x/test/driver/internal/validator/ApiValidator.java
index ddf6439c34..1d538223bd 100644
--- 
a/plc4j/utils/test-utils/src/main/java/org/apache/plc4x/test/driver/internal/validator/ApiValidator.java
+++ 
b/plc4j/utils/test-utils/src/main/java/org/apache/plc4x/test/driver/internal/validator/ApiValidator.java
@@ -19,6 +19,8 @@
 package org.apache.plc4x.test.driver.internal.validator;
 
 import org.apache.plc4x.test.driver.exceptions.DriverTestsuiteException;
+import org.apache.plc4x.test.driver.xmlunit.SkipAttributeFilter;
+import org.apache.plc4x.test.driver.xmlunit.SkipDifferenceEvaluator;
 import org.dom4j.Element;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -26,12 +28,15 @@ import org.xmlunit.builder.DiffBuilder;
 import org.xmlunit.diff.Diff;
 
 public class ApiValidator {
+
     private static final Logger LOGGER = 
LoggerFactory.getLogger(ApiValidator.class);
 
     public static void validateApiMessage(Element referenceXml, String 
apiMessage) throws DriverTestsuiteException {
         final String referenceXmlString = referenceXml.asXML();
         final Diff diff = DiffBuilder.compare(referenceXmlString)
             
.withTest(apiMessage).checkForSimilar().ignoreComments().ignoreWhitespace()
+            .withDifferenceEvaluator(new SkipDifferenceEvaluator())
+            .withAttributeFilter(new SkipAttributeFilter())
             .build();
         if (diff.hasDifferences()) {
             LOGGER.warn("got\n{}", apiMessage);
diff --git 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7UserEvent.java
 
b/plc4j/utils/test-utils/src/main/java/org/apache/plc4x/test/driver/xmlunit/SkipAttributeFilter.java
similarity index 60%
copy from 
plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7UserEvent.java
copy to 
plc4j/utils/test-utils/src/main/java/org/apache/plc4x/test/driver/xmlunit/SkipAttributeFilter.java
index d9f01d0dbd..d7f63abff1 100644
--- 
a/plc4j/drivers/s7/src/main/java/org/apache/plc4x/java/s7/events/S7UserEvent.java
+++ 
b/plc4j/utils/test-utils/src/main/java/org/apache/plc4x/test/driver/xmlunit/SkipAttributeFilter.java
@@ -7,7 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   https://www.apache.org/licenses/LICENSE-2.0
+ *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
@@ -16,14 +16,22 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.plc4x.java.s7.events;
 
-import org.apache.plc4x.java.s7.readwrite.S7PayloadDiagnosticMessage;
+package org.apache.plc4x.test.driver.xmlunit;
 
-public class S7UserEvent extends S7SysEvent {
+import org.w3c.dom.Attr;
+import org.xmlunit.util.Predicate;
 
-    public S7UserEvent(S7PayloadDiagnosticMessage payload) {
-        super(payload);
-        map.put(Fields.TYPE.name(), "USER");
+/**
+ * SPI element needed to exclude our custom attributes from comparison of XML 
results.
+ */
+public class SkipAttributeFilter implements Predicate<Attr> {
+
+    public static final String IGNORE_ATTRIBUTE_NAME = "plc4x-skip-comparison";
+
+    @Override
+    public boolean test(Attr attr) {
+        return !IGNORE_ATTRIBUTE_NAME.equals(attr.getName());
     }
+
 }
diff --git 
a/plc4j/utils/test-utils/src/main/java/org/apache/plc4x/test/driver/xmlunit/SkipDifferenceEvaluator.java
 
b/plc4j/utils/test-utils/src/main/java/org/apache/plc4x/test/driver/xmlunit/SkipDifferenceEvaluator.java
new file mode 100644
index 0000000000..8e5e0f7cab
--- /dev/null
+++ 
b/plc4j/utils/test-utils/src/main/java/org/apache/plc4x/test/driver/xmlunit/SkipDifferenceEvaluator.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.plc4x.test.driver.xmlunit;
+
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.xmlunit.diff.Comparison;
+import org.xmlunit.diff.ComparisonResult;
+import org.xmlunit.diff.DifferenceEvaluator;
+
+/**
+ * Evaluator of differences which allows to ignore differences for elements 
annotated with 'plc4x-skip-comparison' attribute.
+ */
+public class SkipDifferenceEvaluator implements DifferenceEvaluator {
+
+    @Override
+    public ComparisonResult evaluate(Comparison comparison, ComparisonResult 
comparisonResult) {
+        if (comparisonResult != ComparisonResult.EQUAL) {
+            Node target = comparison.getControlDetails().getTarget();
+
+            // root element
+            if (target == null || target.getParentNode() == null) {
+                return comparisonResult;
+            }
+
+            // verify parent element - help with text nodes
+            NamedNodeMap attributes = target.getParentNode().getAttributes();
+            if (attributes != null) {
+                Node attribute = 
attributes.getNamedItem(SkipAttributeFilter.IGNORE_ATTRIBUTE_NAME);
+                if (attribute != null) {
+                    String content = attribute.getTextContent();
+                    return Boolean.parseBoolean(content.trim()) ? 
ComparisonResult.EQUAL : comparisonResult;
+                }
+            }
+        }
+
+        return comparisonResult;
+    }
+}
diff --git a/protocols/ads/src/test/resources/protocols/ads/DriverTestsuite.xml 
b/protocols/ads/src/test/resources/protocols/ads/DriverTestsuite.xml
index 3770e9d267..332722e461 100644
--- a/protocols/ads/src/test/resources/protocols/ads/DriverTestsuite.xml
+++ b/protocols/ads/src/test/resources/protocols/ads/DriverTestsuite.xml
@@ -1239,6 +1239,18 @@
               </PlcResponseItem>
             </hurz>
           </values>
+          <metadata isList="true">
+            <hurz>
+              <entry>
+                <key dataType="string" bitLength="17" 
encoding="UTF-8">receive_timestamp</key>
+                <value dataType="string" bitLength="13" encoding="UTF-8" 
plc4x-skip-comparison="true">0</value>
+              </entry>
+              <entry>
+                <key dataType="string" bitLength="16" 
encoding="UTF-8">timestamp_source</key>
+                <value dataType="string" bitLength="10" 
encoding="UTF-8">ASSUMPTION</value>
+              </entry>
+            </hurz>
+          </metadata>
         </PlcReadResponse>
       </api-response>
     </steps>

Reply via email to