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

dominikriemer pushed a commit to branch 
4442-add-adapter-to-import-wincc-archived-alarm-messages
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to 
refs/heads/4442-add-adapter-to-import-wincc-archived-alarm-messages by this 
push:
     new d8cc65aa30 feat(#4442): Add WinCC archived alarm adapter
d8cc65aa30 is described below

commit d8cc65aa30d52e30e6c2221e3b845d95f8f525cc
Author: Dominik Riemer <[email protected]>
AuthorDate: Wed May 6 23:36:16 2026 +0200

    feat(#4442): Add WinCC archived alarm adapter
---
 streampipes-extensions/pom.xml                     |   1 +
 .../streampipes-connectors-filewatcher/pom.xml     |  80 ++++++
 .../FileWatcherConnectorsModuleExport.java         |  46 +++
 .../adapter/WinCCAlarmArchiveAdapter.java          | 264 +++++++++++++++++
 .../filewatcher/adapter/WinCCAlarmEventMapper.java | 126 ++++++++
 .../adapter/WinCCTimestampConverter.java           | 121 ++++++++
 .../filewatcher/model/CsvParserSettings.java       |  46 +++
 .../filewatcher/model/FileFingerprint.java         |  83 ++++++
 .../filewatcher/model/FileWatcherCheckpoint.java   |  77 +++++
 .../filewatcher/model/FileWatcherConfig.java       |  30 ++
 .../filewatcher/runtime/CsvFileReader.java         | 145 ++++++++++
 .../filewatcher/runtime/EventDelayExecutor.java    |  25 ++
 .../filewatcher/runtime/EventMapper.java           |  31 ++
 .../filewatcher/runtime/FileSetWatcher.java        | 317 +++++++++++++++++++++
 .../connectors/filewatcher/runtime/FileSlot.java   |  26 ++
 .../filewatcher/runtime/FileWatchReadResult.java   |  24 ++
 .../runtime/FileWatcherCheckpointStore.java        |  70 +++++
 .../documentation.md                               |  45 +++
 .../strings.en                                     |  28 ++
 .../adapter/WinCCAlarmEventMapperTest.java         |  74 +++++
 .../adapter/WinCCTimestampConverterTest.java       |  50 ++++
 .../filewatcher/runtime/CsvFileReaderTest.java     |  79 +++++
 .../filewatcher/runtime/FileSetWatcherTest.java    | 180 ++++++++++++
 .../streampipes-extensions-all-iiot/pom.xml        |   5 +
 .../extensions/all/iiot/AllExtensionsIIoTInit.java |   2 +
 .../streampipes-extensions-all-jvm/pom.xml         |   5 +
 .../extensions/all/jvm/AllExtensionsInit.java      |   2 +
 27 files changed, 1982 insertions(+)

diff --git a/streampipes-extensions/pom.xml b/streampipes-extensions/pom.xml
index d540061ea2..749c3bbf24 100644
--- a/streampipes-extensions/pom.xml
+++ b/streampipes-extensions/pom.xml
@@ -35,6 +35,7 @@
         <module>streampipes-connect-adapters-iiot</module>
 
         <module>streampipes-connectors-camel</module>
+        <module>streampipes-connectors-filewatcher</module>
         <module>streampipes-connectors-influx</module>
         <module>streampipes-connectors-kafka</module>
         <module>streampipes-connectors-mqtt</module>
diff --git a/streampipes-extensions/streampipes-connectors-filewatcher/pom.xml 
b/streampipes-extensions/streampipes-connectors-filewatcher/pom.xml
new file mode 100644
index 0000000000..4627d76eba
--- /dev/null
+++ b/streampipes-extensions/streampipes-connectors-filewatcher/pom.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.streampipes</groupId>
+        <artifactId>streampipes-extensions</artifactId>
+        <version>0.99.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>streampipes-connectors-filewatcher</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.streampipes</groupId>
+            <artifactId>streampipes-extensions-api</artifactId>
+            <version>0.99.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.streampipes</groupId>
+            <artifactId>streampipes-extensions-management</artifactId>
+            <version>0.99.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.streampipes</groupId>
+            <artifactId>streampipes-sdk</artifactId>
+            <version>0.99.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.streampipes</groupId>
+            <artifactId>streampipes-serializers-json</artifactId>
+            <version>0.99.0-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.opencsv</groupId>
+            <artifactId>opencsv</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-checkstyle-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/FileWatcherConnectorsModuleExport.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/FileWatcherConnectorsModuleExport.java
new file mode 100644
index 0000000000..d87c1777d8
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/FileWatcherConnectorsModuleExport.java
@@ -0,0 +1,46 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher;
+
+import org.apache.streampipes.extensions.api.connect.StreamPipesAdapter;
+import org.apache.streampipes.extensions.api.declarer.IExtensionModuleExport;
+import org.apache.streampipes.extensions.api.migration.IModelMigrator;
+import org.apache.streampipes.extensions.api.pe.IStreamPipesPipelineElement;
+import 
org.apache.streampipes.extensions.connectors.filewatcher.adapter.WinCCAlarmArchiveAdapter;
+
+import java.util.Collections;
+import java.util.List;
+
+public class FileWatcherConnectorsModuleExport implements 
IExtensionModuleExport {
+
+  @Override
+  public List<StreamPipesAdapter> adapters() {
+    return List.of(new WinCCAlarmArchiveAdapter());
+  }
+
+  @Override
+  public List<IStreamPipesPipelineElement<?>> pipelineElements() {
+    return Collections.emptyList();
+  }
+
+  @Override
+  public List<IModelMigrator<?, ?>> migrators() {
+    return Collections.emptyList();
+  }
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/adapter/WinCCAlarmArchiveAdapter.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/adapter/WinCCAlarmArchiveAdapter.java
new file mode 100644
index 0000000000..21391d0218
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/adapter/WinCCAlarmArchiveAdapter.java
@@ -0,0 +1,264 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.adapter;
+
+import org.apache.streampipes.commons.exceptions.connect.AdapterException;
+import org.apache.streampipes.extensions.api.connect.IAdapterConfiguration;
+import org.apache.streampipes.extensions.api.connect.IEventCollector;
+import org.apache.streampipes.extensions.api.connect.IPullAdapter;
+import org.apache.streampipes.extensions.api.connect.StreamPipesAdapter;
+import 
org.apache.streampipes.extensions.api.connect.context.IAdapterGuessSchemaContext;
+import 
org.apache.streampipes.extensions.api.connect.context.IAdapterRuntimeContext;
+import 
org.apache.streampipes.extensions.api.extractor.IAdapterParameterExtractor;
+import 
org.apache.streampipes.extensions.connectors.filewatcher.model.CsvParserSettings;
+import 
org.apache.streampipes.extensions.connectors.filewatcher.model.FileWatcherConfig;
+import 
org.apache.streampipes.extensions.connectors.filewatcher.runtime.CsvFileReader;
+import 
org.apache.streampipes.extensions.connectors.filewatcher.runtime.FileSetWatcher;
+import 
org.apache.streampipes.extensions.connectors.filewatcher.runtime.FileWatcherCheckpointStore;
+import 
org.apache.streampipes.extensions.management.connect.PullAdapterScheduler;
+import 
org.apache.streampipes.extensions.management.connect.adapter.parser.CsvParser;
+import 
org.apache.streampipes.extensions.management.connect.adapter.util.PollingSettings;
+import org.apache.streampipes.model.connect.guess.SampleData;
+import org.apache.streampipes.model.extensions.ExtensionAssetType;
+import org.apache.streampipes.sdk.builder.adapter.AdapterConfigurationBuilder;
+import org.apache.streampipes.sdk.helpers.Labels;
+import org.apache.streampipes.sdk.helpers.Locales;
+import org.apache.streampipes.sdk.helpers.Options;
+import org.apache.streampipes.sdk.helpers.Tuple2;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+public class WinCCAlarmArchiveAdapter implements StreamPipesAdapter, 
IPullAdapter {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(WinCCAlarmArchiveAdapter.class);
+
+  public static final String ID = 
"org.apache.streampipes.connectors.wincc.alarm.archive";
+
+  private static final String DIRECTORY_PATH = "directory-path";
+  private static final String ARCHIVE_BASE_NAME = "archive-base-name";
+  private static final String SEGMENTED_CIRCULAR_LOG_ENABLED = 
"segmented-circular-log-enabled";
+  private static final String ARCHIVE_SEGMENT_COUNT = "archive-segment-count";
+  private static final String POLL_INTERVAL_SECONDS = "poll-interval-seconds";
+  private static final String INTER_EVENT_DELAY_MS = "inter-event-delay-ms";
+
+  private static final String SEGMENTED_CIRCULAR_LOG_ON = 
"segmented-circular-log-on";
+  private static final String SEGMENTED_CIRCULAR_LOG_OFF = 
"segmented-circular-log-off";
+  private static final CsvParserSettings WINCC_CSV_SETTINGS = new 
CsvParserSettings(true, ';');
+  private static final WinCCAlarmEventMapper EVENT_MAPPER = new 
WinCCAlarmEventMapper();
+
+  private PullAdapterScheduler scheduler;
+  private FileSetWatcher watcher;
+  private String adapterElementId;
+  private IEventCollector collector;
+  private PollingSettings pollingSettings;
+
+  @Override
+  public IAdapterConfiguration declareConfig() {
+    return AdapterConfigurationBuilder
+        .create(ID, 0, WinCCAlarmArchiveAdapter::new)
+        .withAssets(ExtensionAssetType.DOCUMENTATION)
+        .withLocales(Locales.EN)
+        .requiredTextParameter(Labels.from(
+            DIRECTORY_PATH,
+            "Archive directory",
+            "Absolute path to the WinCC archive directory that contains the 
exported alarm CSV files."
+        ))
+        .requiredTextParameter(Labels.from(
+            ARCHIVE_BASE_NAME,
+            "Archive base name",
+            "Base file name of the WinCC alarm archive, for example 
Meldungsarchiv."
+        ))
+        .requiredSingleValueSelection(
+            Labels.from(
+                SEGMENTED_CIRCULAR_LOG_ENABLED,
+                "Segmented circular log",
+                "Whether WinCC writes the alarm archive as a segmented 
circular log with rotating CSV segments."
+            ),
+            Options.from(
+                new Tuple2<>("Enabled", SEGMENTED_CIRCULAR_LOG_ON),
+                new Tuple2<>("Disabled", SEGMENTED_CIRCULAR_LOG_OFF)
+            )
+        )
+        .requiredIntegerParameter(Labels.from(
+            ARCHIVE_SEGMENT_COUNT,
+            "Segment count",
+            "Number of archive segments in segmented circular log mode. 
Ignored when the segmented circular log is disabled."
+        ))
+        .requiredIntegerParameter(Labels.from(
+            POLL_INTERVAL_SECONDS,
+            "Polling interval (seconds)",
+            "How often the WinCC archive files should be scanned for new alarm 
entries."
+        ))
+        .requiredIntegerParameter(Labels.from(
+            INTER_EVENT_DELAY_MS,
+            "Inter-event delay (ms)",
+            "Delay between replayed alarm events in milliseconds. Use 0 to 
disable throttling."
+        ), 0)
+        .buildConfiguration();
+  }
+
+  @Override
+  public void onAdapterStarted(IAdapterParameterExtractor extractor,
+                               IEventCollector collector,
+                               IAdapterRuntimeContext adapterRuntimeContext) 
throws AdapterException {
+    FileWatcherConfig config = toConfig(extractor);
+    this.adapterElementId = extractor.getAdapterDescription().getElementId();
+    this.collector = collector;
+    this.pollingSettings = PollingSettings.from(TimeUnit.SECONDS, 
config.pollIntervalSeconds());
+    this.watcher = new FileSetWatcher(
+        config,
+        new FileWatcherCheckpointStore(),
+        new CsvFileReader(),
+        EVENT_MAPPER
+    );
+    LOG.debug(
+        "Starting WinCC alarm archive adapter '{}': directory='{}', 
pattern='{}', pollIntervalSeconds={}, "
+            + "singleFileGrowthMode={}, interEventDelayMs={}.",
+        adapterElementId,
+        config.directory(),
+        config.filePattern().pattern(),
+        config.pollIntervalSeconds(),
+        config.singleFileGrowthMode(),
+        config.interEventDelayMs()
+    );
+    this.scheduler = new PullAdapterScheduler();
+    this.scheduler.schedule(this, adapterElementId);
+  }
+
+  @Override
+  public void onAdapterStopped(IAdapterParameterExtractor extractor,
+                               IAdapterRuntimeContext adapterRuntimeContext) {
+    if (scheduler != null) {
+      scheduler.shutdown();
+    }
+  }
+
+  @Override
+  public SampleData onSampleDataRequested(IAdapterParameterExtractor extractor,
+                                          IAdapterGuessSchemaContext 
adapterGuessSchemaContext) throws AdapterException {
+    FileWatcherConfig config = toConfig(extractor);
+    try (var files = Files.list(config.directory())) {
+      Path sampleFile = files
+          .filter(Files::isRegularFile)
+          .filter(path -> 
config.filePattern().matcher(path.getFileName().toString()).matches())
+          .sorted()
+          .findFirst()
+          .orElseThrow(() -> new AdapterException("No matching WinCC alarm 
archive files found in " + config.directory()));
+
+      try (var inputStream = Files.newInputStream(sampleFile)) {
+        Map<String, Object> rawSample = new CsvParser(true, 
';').getSampleData(inputStream).getSamples().get(0);
+        return 
org.apache.streampipes.sdk.builder.adapter.SampleDataBuilder.create()
+            .sample(EVENT_MAPPER.map(rawSample))
+            .build();
+      }
+    } catch (IOException e) {
+      throw new AdapterException("Could not load sample data from directory " 
+ config.directory(), e);
+    }
+  }
+
+  @Override
+  public void pullData() {
+    try {
+      watcher.poll(adapterElementId, collector);
+    } catch (IOException | RuntimeException e) {
+      throw new CompletionException("Could not poll files from WinCC alarm 
archive adapter", e);
+    }
+  }
+
+  @Override
+  public PollingSettings getPollingInterval() {
+    return pollingSettings;
+  }
+
+  private FileWatcherConfig toConfig(IAdapterParameterExtractor extractor) 
throws AdapterException {
+    var staticPropertyExtractor = extractor.getStaticPropertyExtractor();
+    var directory = 
Path.of(staticPropertyExtractor.singleValueParameter(DIRECTORY_PATH, 
String.class));
+    if (!Files.isDirectory(directory)) {
+      throw new AdapterException("Configured directory does not exist or is 
not a directory: " + directory);
+    }
+
+    var baseName = 
normalizeBaseName(staticPropertyExtractor.singleValueParameter(ARCHIVE_BASE_NAME,
 String.class));
+    var segmentedCircularLogMode =
+        
staticPropertyExtractor.selectedSingleValueInternalName(SEGMENTED_CIRCULAR_LOG_ENABLED,
 String.class);
+    var segmentCount = 
staticPropertyExtractor.singleValueParameter(ARCHIVE_SEGMENT_COUNT, 
Integer.class);
+    var interval = 
staticPropertyExtractor.singleValueParameter(POLL_INTERVAL_SECONDS, 
Integer.class);
+    var interEventDelayMs = 
staticPropertyExtractor.singleValueParameter(INTER_EVENT_DELAY_MS, 
Integer.class);
+    if (interEventDelayMs < 0) {
+      throw new AdapterException("Inter-event delay must be greater than or 
equal to 0.");
+    }
+
+    Pattern filePattern = 
SEGMENTED_CIRCULAR_LOG_ON.equals(segmentedCircularLogMode)
+        ? buildSegmentPattern(baseName, segmentCount)
+        : buildSingleSegmentPattern(baseName);
+
+    return new FileWatcherConfig(
+        directory,
+        filePattern,
+        WINCC_CSV_SETTINGS,
+        interval,
+        SEGMENTED_CIRCULAR_LOG_OFF.equals(segmentedCircularLogMode),
+        interEventDelayMs
+    );
+  }
+
+  private String normalizeBaseName(String configuredBaseName) {
+    String trimmedBaseName = configuredBaseName.trim();
+    if (trimmedBaseName.endsWith(".csv")) {
+      return trimmedBaseName.substring(0, trimmedBaseName.length() - 4);
+    }
+
+    return trimmedBaseName;
+  }
+
+  private Pattern buildSingleSegmentPattern(String baseName) {
+    return Pattern.compile(Pattern.quote(baseName) + "1\\.csv", 
Pattern.CASE_INSENSITIVE);
+  }
+
+  private Pattern buildSegmentPattern(String baseName, int segmentCount) 
throws AdapterException {
+    if (segmentCount < 1) {
+      throw new AdapterException("Segment count must be at least 1 when the 
segmented circular log is enabled.");
+    }
+
+    return Pattern.compile(
+        Pattern.quote(baseName) + "(" + buildAllowedSegments(segmentCount) + 
")\\.csv",
+        Pattern.CASE_INSENSITIVE
+    );
+  }
+
+  private String buildAllowedSegments(int segmentCount) {
+    StringBuilder builder = new StringBuilder();
+    for (int i = 1; i <= segmentCount; i++) {
+      if (i > 1) {
+        builder.append("|");
+      }
+      builder.append(i);
+    }
+
+    return builder.toString();
+  }
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/adapter/WinCCAlarmEventMapper.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/adapter/WinCCAlarmEventMapper.java
new file mode 100644
index 0000000000..ebca63feda
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/adapter/WinCCAlarmEventMapper.java
@@ -0,0 +1,126 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.adapter;
+
+import 
org.apache.streampipes.extensions.connectors.filewatcher.runtime.EventMapper;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class WinCCAlarmEventMapper implements EventMapper {
+
+  @Override
+  public Map<String, Object> map(Map<String, Object> rawEvent) {
+    Map<String, Object> normalizedEvent = new LinkedHashMap<>();
+
+    normalizedEvent.put("timestamp",
+        WinCCTimestampConverter.toUnixTimestampMillis(rawEvent.get("Time_ms"), 
rawEvent.get("TimeString")));
+    normalizedEvent.put("timestamp_ms", asString(rawEvent.get("Time_ms")));
+    normalizedEvent.put("timestamp_string", 
asString(rawEvent.get("TimeString")));
+    normalizedEvent.put("message_text", asString(rawEvent.get("MsgText")));
+    normalizedEvent.put("message_number", 
toInteger(rawEvent.get("MsgNumber")));
+    normalizedEvent.put("alarm_procedure_code", 
toInteger(rawEvent.get("MsgProc")));
+    normalizedEvent.put("alarm_procedure", 
alarmProcedure(rawEvent.get("MsgProc")));
+    normalizedEvent.put("state_after_code", 
toInteger(rawEvent.get("StateAfter")));
+    normalizedEvent.put("state_after", stateAfter(rawEvent.get("StateAfter")));
+    normalizedEvent.put("alarm_class_code", 
toInteger(rawEvent.get("MsgClass")));
+    normalizedEvent.put("alarm_class", alarmClass(rawEvent.get("MsgClass")));
+    normalizedEvent.put("plc", asString(rawEvent.get("PLC")));
+
+    for (int i = 1; i <= 8; i++) {
+      normalizedEvent.put("parameter_" + i, asString(rawEvent.get("Var" + i)));
+    }
+
+    // Keep only the original timestamp representations in addition to the 
normalized fields.
+    normalizedEvent.put("raw_Time_ms", asString(rawEvent.get("Time_ms")));
+    normalizedEvent.put("raw_TimeString", 
asString(rawEvent.get("TimeString")));
+    return normalizedEvent;
+  }
+
+  private String alarmProcedure(Object code) {
+    return switch (toInt(code)) {
+      case 1 -> "system_event";
+      case 2 -> "alarm_bit_operation";
+      case 3 -> "alarm_s";
+      case 4 -> "diagnostics_event";
+      case 7 -> "analog_alarm";
+      case 9 -> "program_alarm";
+      case 100 -> "alarm_bit_fault";
+      default -> "unknown";
+    };
+  }
+
+  private String stateAfter(Object code) {
+    return switch (toInt(code)) {
+      case 0 -> "incoming_outgoing";
+      case 1 -> "incoming";
+      case 2 -> "incoming_acknowledged_outgoing";
+      case 3 -> "incoming_acknowledged";
+      case 4 -> "pending_after_plc_reset";
+      case 6 -> "incoming_outgoing_acknowledged";
+      default -> "unknown";
+    };
+  }
+
+  private String alarmClass(Object code) {
+    int numericCode = toInt(code);
+    return switch (numericCode) {
+      case 0 -> "no_alarm_class";
+      case 1 -> "errors";
+      case 2 -> "warnings";
+      case 3 -> "system";
+      case 4 -> "diagnostic_events";
+      default -> numericCode >= 64 ? "user_defined" : "unknown";
+    };
+  }
+
+  private int toInt(Object code) {
+    Integer value = toInteger(code);
+    if (value != null) {
+      return value;
+    }
+
+    return -1;
+  }
+
+  private Integer toInteger(Object code) {
+    if (code instanceof Number number) {
+      return number.intValue();
+    }
+
+    if (code instanceof String stringValue) {
+      String trimmedValue = stringValue.trim();
+      if (trimmedValue.isEmpty()) {
+        return null;
+      }
+
+      try {
+        return Integer.parseInt(trimmedValue);
+      } catch (NumberFormatException e) {
+        return null;
+      }
+    }
+
+    return null;
+  }
+
+  private String asString(Object value) {
+    return value == null ? "" : String.valueOf(value);
+  }
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/adapter/WinCCTimestampConverter.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/adapter/WinCCTimestampConverter.java
new file mode 100644
index 0000000000..056fe03432
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/adapter/WinCCTimestampConverter.java
@@ -0,0 +1,121 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.adapter;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.List;
+
+public final class WinCCTimestampConverter {
+
+  private static final BigDecimal EXCEL_UNIX_EPOCH_OFFSET_DAYS = 
BigDecimal.valueOf(25569L);
+  private static final BigDecimal MILLIS_PER_DAY = 
BigDecimal.valueOf(86_400_000L);
+  private static final BigDecimal SCALED_DAY_DIVISOR = 
BigDecimal.valueOf(1_000_000L);
+  private static final List<DateTimeFormatter> TIME_STRING_FORMATS = List.of(
+      DateTimeFormatter.ofPattern("dd.MM.yy HH:mm:ss"),
+      DateTimeFormatter.ofPattern("dd.MM.yy HH:mm")
+  );
+
+  private WinCCTimestampConverter() {
+  }
+
+  public static Long toUnixTimestampMillis(Object winccTimestamp,
+                                           Object timeString) {
+    BigDecimal serialDays = normalizeSerialDays(winccTimestamp);
+    if (serialDays != null) {
+      return serialDays
+          .subtract(EXCEL_UNIX_EPOCH_OFFSET_DAYS)
+          .multiply(MILLIS_PER_DAY)
+          .setScale(0, RoundingMode.HALF_UP)
+          .longValue();
+    }
+
+    return parseTimeString(timeString);
+  }
+
+  private static BigDecimal normalizeSerialDays(Object winccTimestamp) {
+    BigDecimal rawValue = toBigDecimal(winccTimestamp);
+    if (rawValue == null) {
+      return null;
+    }
+
+    if (isScaledDayValue(rawValue, winccTimestamp)) {
+      return rawValue.divide(SCALED_DAY_DIVISOR, 10, RoundingMode.HALF_UP);
+    }
+
+    return rawValue;
+  }
+
+  private static boolean isScaledDayValue(BigDecimal rawValue,
+                                          Object originalValue) {
+    if (originalValue instanceof String stringValue) {
+      if (stringValue.contains(",") || stringValue.contains(".")) {
+        return false;
+      }
+    }
+
+    return rawValue.compareTo(BigDecimal.valueOf(1_000_000L)) > 0;
+  }
+
+  private static BigDecimal toBigDecimal(Object winccTimestamp) {
+    if (winccTimestamp instanceof Number number) {
+      return BigDecimal.valueOf(number.doubleValue());
+    }
+
+    if (winccTimestamp instanceof String stringValue) {
+      String normalizedValue = stringValue.trim().replace(',', '.');
+      if (normalizedValue.isEmpty()) {
+        return null;
+      }
+
+      try {
+        return new BigDecimal(normalizedValue);
+      } catch (NumberFormatException e) {
+        return null;
+      }
+    }
+
+    return null;
+  }
+
+  private static Long parseTimeString(Object timeString) {
+    if (!(timeString instanceof String stringValue)) {
+      return null;
+    }
+
+    String trimmedValue = stringValue.trim();
+    if (trimmedValue.isEmpty()) {
+      return null;
+    }
+
+    for (DateTimeFormatter formatter : TIME_STRING_FORMATS) {
+      try {
+        LocalDateTime dateTime = LocalDateTime.parse(trimmedValue, formatter);
+        return 
dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+      } catch (DateTimeParseException ignored) {
+      }
+    }
+
+    return null;
+  }
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/model/CsvParserSettings.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/model/CsvParserSettings.java
new file mode 100644
index 0000000000..89fcd9c22e
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/model/CsvParserSettings.java
@@ -0,0 +1,46 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.model;
+
+import org.apache.streampipes.commons.exceptions.connect.AdapterException;
+import org.apache.streampipes.extensions.api.connect.IParser;
+import 
org.apache.streampipes.extensions.management.connect.adapter.parser.CsvParser;
+
+import java.lang.reflect.Field;
+
+public record CsvParserSettings(boolean hasHeader, char delimiter) {
+
+  public static CsvParserSettings from(IParser parser) throws AdapterException 
{
+    if (!(parser instanceof CsvParser csvParser)) {
+      throw new AdapterException("The file watcher currently supports only the 
CSV parser.");
+    }
+
+    try {
+      Field headerField = CsvParser.class.getDeclaredField("header");
+      headerField.setAccessible(true);
+
+      Field delimiterField = CsvParser.class.getDeclaredField("delimiter");
+      delimiterField.setAccessible(true);
+
+      return new CsvParserSettings(headerField.getBoolean(csvParser), 
delimiterField.getChar(csvParser));
+    } catch (NoSuchFieldException | IllegalAccessException e) {
+      throw new AdapterException("Could not extract CSV parser settings.", e);
+    }
+  }
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/model/FileFingerprint.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/model/FileFingerprint.java
new file mode 100644
index 0000000000..16a3093f97
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/model/FileFingerprint.java
@@ -0,0 +1,83 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.model;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+public class FileFingerprint implements Serializable {
+
+  private long size;
+  private long lastModified;
+  private String contentHash;
+
+  public FileFingerprint() {
+  }
+
+  public FileFingerprint(long size, long lastModified, String contentHash) {
+    this.size = size;
+    this.lastModified = lastModified;
+    this.contentHash = contentHash;
+  }
+
+  public long getSize() {
+    return size;
+  }
+
+  public void setSize(long size) {
+    this.size = size;
+  }
+
+  public long getLastModified() {
+    return lastModified;
+  }
+
+  public void setLastModified(long lastModified) {
+    this.lastModified = lastModified;
+  }
+
+  public String getContentHash() {
+    return contentHash;
+  }
+
+  public void setContentHash(String contentHash) {
+    this.contentHash = contentHash;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof FileFingerprint other)) {
+      return false;
+    }
+    return size == other.size
+        && lastModified == other.lastModified
+        && Objects.equals(contentHash, other.contentHash);
+  }
+
+  @Override
+  public int hashCode() {
+    int result = Long.hashCode(size);
+    result = 31 * result + Long.hashCode(lastModified);
+    result = 31 * result + Objects.hashCode(contentHash);
+    return result;
+  }
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/model/FileWatcherCheckpoint.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/model/FileWatcherCheckpoint.java
new file mode 100644
index 0000000000..3b0cb0d7fb
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/model/FileWatcherCheckpoint.java
@@ -0,0 +1,77 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.model;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+public class FileWatcherCheckpoint implements Serializable {
+
+  private String currentFileName;
+  private long currentSequence;
+  private long lastProcessedRecord;
+  private FileFingerprint currentFingerprint;
+  private Map<String, FileFingerprint> processedGenerations;
+
+  public FileWatcherCheckpoint() {
+    this.lastProcessedRecord = -1L;
+    this.processedGenerations = new HashMap<>();
+  }
+
+  public String getCurrentFileName() {
+    return currentFileName;
+  }
+
+  public void setCurrentFileName(String currentFileName) {
+    this.currentFileName = currentFileName;
+  }
+
+  public long getCurrentSequence() {
+    return currentSequence;
+  }
+
+  public void setCurrentSequence(long currentSequence) {
+    this.currentSequence = currentSequence;
+  }
+
+  public long getLastProcessedRecord() {
+    return lastProcessedRecord;
+  }
+
+  public void setLastProcessedRecord(long lastProcessedRecord) {
+    this.lastProcessedRecord = lastProcessedRecord;
+  }
+
+  public FileFingerprint getCurrentFingerprint() {
+    return currentFingerprint;
+  }
+
+  public void setCurrentFingerprint(FileFingerprint currentFingerprint) {
+    this.currentFingerprint = currentFingerprint;
+  }
+
+  public Map<String, FileFingerprint> getProcessedGenerations() {
+    return processedGenerations;
+  }
+
+  public void setProcessedGenerations(Map<String, FileFingerprint> 
processedGenerations) {
+    this.processedGenerations = processedGenerations;
+  }
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/model/FileWatcherConfig.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/model/FileWatcherConfig.java
new file mode 100644
index 0000000000..9a2d534ebc
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/model/FileWatcherConfig.java
@@ -0,0 +1,30 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.model;
+
+import java.nio.file.Path;
+import java.util.regex.Pattern;
+
+public record FileWatcherConfig(Path directory,
+                                Pattern filePattern,
+                                CsvParserSettings parserSettings,
+                                int pollIntervalSeconds,
+                                boolean singleFileGrowthMode,
+                                int interEventDelayMs) {
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/CsvFileReader.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/CsvFileReader.java
new file mode 100644
index 0000000000..de59b5ad45
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/CsvFileReader.java
@@ -0,0 +1,145 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.runtime;
+
+import org.apache.streampipes.commons.exceptions.connect.ParseException;
+import 
org.apache.streampipes.extensions.connectors.filewatcher.model.CsvParserSettings;
+
+import com.opencsv.CSVParserBuilder;
+import com.opencsv.CSVReader;
+import com.opencsv.CSVReaderBuilder;
+import com.opencsv.exceptions.CsvValidationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.MalformedInputException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.stream.IntStream;
+
+public class CsvFileReader {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(CsvFileReader.class);
+
+  private static final List<Charset> FALLBACK_CHARSETS = List.of(
+      StandardCharsets.UTF_8,
+      Charset.forName("windows-1252"),
+      StandardCharsets.ISO_8859_1
+  );
+
+  public FileWatchReadResult readFrom(Path path,
+                                      CsvParserSettings parserSettings,
+                                      long startRecordIndex,
+                                      BiConsumer<Long, Map<String, Object>> 
eventConsumer) throws IOException {
+    IOException lastException = null;
+    for (Charset charset : FALLBACK_CHARSETS) {
+      try {
+        LOG.debug("Trying to read CSV file '{}' with charset '{}'.", 
path.getFileName(), charset.name());
+        return readFrom(path, parserSettings, startRecordIndex, eventConsumer, 
charset);
+      } catch (MalformedInputException e) {
+        LOG.debug("Could not decode CSV file '{}' with charset '{}': {}.",
+            path.getFileName(), charset.name(), e.getMessage());
+        lastException = e;
+      }
+    }
+
+    throw new IOException("Could not decode CSV file " + path.getFileName() + 
" using supported charsets.", lastException);
+  }
+
+  private FileWatchReadResult readFrom(Path path,
+                                       CsvParserSettings parserSettings,
+                                       long startRecordIndex,
+                                       BiConsumer<Long, Map<String, Object>> 
eventConsumer,
+                                       Charset charset) throws IOException {
+    try (var bufferedReader = Files.newBufferedReader(path, charset);
+         var csvReader = newCsvReader(bufferedReader, parserSettings)) {
+      LOG.debug("Reading CSV file '{}' using charset '{}'.", 
path.getFileName(), charset.name());
+
+      String[] header = parserSettings.hasHeader() ? csvReader.readNext() : 
null;
+      String[] row;
+      long currentRecordIndex = -1L;
+      long lastProcessedRecord = startRecordIndex - 1;
+
+      while ((row = csvReader.readNext()) != null) {
+        currentRecordIndex++;
+
+        if (header == null) {
+          header = IntStream.range(0, row.length)
+              .mapToObj(i -> "key_" + i)
+              .toArray(String[]::new);
+        }
+
+        if (currentRecordIndex < startRecordIndex) {
+          continue;
+        }
+
+        eventConsumer.accept(currentRecordIndex, toMap(header, row));
+        lastProcessedRecord = currentRecordIndex;
+      }
+
+      int emittedEvents = lastProcessedRecord < startRecordIndex
+          ? 0
+          : (int) (lastProcessedRecord - startRecordIndex + 1);
+
+      LOG.debug("Completed reading CSV file '{}'. Emitted {} records starting 
from record {}.",
+          path.getFileName(), emittedEvents, startRecordIndex);
+
+      return new FileWatchReadResult(lastProcessedRecord, emittedEvents, true);
+    } catch (CsvValidationException e) {
+      throw new ParseException("Could not parse CSV file " + 
path.getFileName(), e);
+    }
+  }
+
+  private CSVReader newCsvReader(BufferedReader reader, CsvParserSettings 
parserSettings) {
+    var csvParser = new CSVParserBuilder()
+        .withSeparator(parserSettings.delimiter())
+        .build();
+
+    return new CSVReaderBuilder(reader)
+        .withSkipLines(0)
+        .withCSVParser(csvParser)
+        .build();
+  }
+
+  private Map<String, Object> toMap(String[] header, String[] row) {
+    Map<String, Object> event = new LinkedHashMap<>();
+    for (int i = 0; i < header.length; i++) {
+      event.put(header[i], i < row.length ? row[i] : "");
+    }
+
+    if (row.length > header.length && header.length > 0) {
+      String lastHeader = header[header.length - 1];
+      String mergedValue = String.valueOf(event.get(lastHeader));
+      for (int i = header.length; i < row.length; i++) {
+        mergedValue = mergedValue + row[i];
+      }
+      event.put(lastHeader, mergedValue);
+    }
+
+    return event;
+  }
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/EventDelayExecutor.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/EventDelayExecutor.java
new file mode 100644
index 0000000000..515888105f
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/EventDelayExecutor.java
@@ -0,0 +1,25 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.runtime;
+
+@FunctionalInterface
+public interface EventDelayExecutor {
+
+  void sleep(long delayMs) throws InterruptedException;
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/EventMapper.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/EventMapper.java
new file mode 100644
index 0000000000..294e565b6f
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/EventMapper.java
@@ -0,0 +1,31 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.runtime;
+
+import java.util.Map;
+
+@FunctionalInterface
+public interface EventMapper {
+
+  Map<String, Object> map(Map<String, Object> rawEvent);
+
+  static EventMapper identity() {
+    return rawEvent -> rawEvent;
+  }
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/FileSetWatcher.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/FileSetWatcher.java
new file mode 100644
index 0000000000..549285de10
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/FileSetWatcher.java
@@ -0,0 +1,317 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.runtime;
+
+import org.apache.streampipes.extensions.api.connect.IEventCollector;
+import 
org.apache.streampipes.extensions.connectors.filewatcher.model.FileFingerprint;
+import 
org.apache.streampipes.extensions.connectors.filewatcher.model.FileWatcherCheckpoint;
+import 
org.apache.streampipes.extensions.connectors.filewatcher.model.FileWatcherConfig;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HexFormat;
+import java.util.List;
+import java.util.regex.Matcher;
+
+public class FileSetWatcher {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(FileSetWatcher.class);
+
+  private final FileWatcherConfig config;
+  private final FileWatcherCheckpointStore checkpointStore;
+  private final CsvFileReader csvFileReader;
+  private final EventMapper eventMapper;
+  private final EventDelayExecutor eventDelayExecutor;
+
+  public FileSetWatcher(FileWatcherConfig config,
+                        FileWatcherCheckpointStore checkpointStore,
+                        CsvFileReader csvFileReader) {
+    this(config, checkpointStore, csvFileReader, EventMapper.identity());
+  }
+
+  public FileSetWatcher(FileWatcherConfig config,
+                        FileWatcherCheckpointStore checkpointStore,
+                        CsvFileReader csvFileReader,
+                        EventMapper eventMapper) {
+    this(config, checkpointStore, csvFileReader, eventMapper, Thread::sleep);
+  }
+
+  public FileSetWatcher(FileWatcherConfig config,
+                        FileWatcherCheckpointStore checkpointStore,
+                        CsvFileReader csvFileReader,
+                        EventMapper eventMapper,
+                        EventDelayExecutor eventDelayExecutor) {
+    this.config = config;
+    this.checkpointStore = checkpointStore;
+    this.csvFileReader = csvFileReader;
+    this.eventMapper = eventMapper;
+    this.eventDelayExecutor = eventDelayExecutor;
+  }
+
+  public void poll(String adapterElementId, IEventCollector collector) throws 
IOException {
+    LOG.debug("Polling WinCC archive files in directory '{}' for adapter 
'{}'.",
+        config.directory(), adapterElementId);
+    var files = discoverFiles();
+    if (files.isEmpty()) {
+      LOG.debug("No matching WinCC archive files found in '{}'.", 
config.directory());
+      return;
+    }
+
+    var checkpoint = checkpointStore.load(adapterElementId);
+    LOG.debug(
+        "Loaded checkpoint for adapter '{}': currentFile='{}', sequence={}, 
lastProcessedRecord={}, knownGenerations={}.",
+        adapterElementId,
+        checkpoint.getCurrentFileName(),
+        checkpoint.getCurrentSequence(),
+        checkpoint.getLastProcessedRecord(),
+        checkpoint.getProcessedGenerations().size()
+    );
+    processCurrentFile(adapterElementId, collector, checkpoint, files);
+    processFollowingFiles(adapterElementId, collector, checkpoint, files);
+  }
+
+  private void processCurrentFile(String adapterElementId,
+                                  IEventCollector collector,
+                                  FileWatcherCheckpoint checkpoint,
+                                  List<FileSlot> files) throws IOException {
+    if (checkpoint.getCurrentFileName() == null || 
checkpoint.getCurrentFingerprint() == null) {
+      LOG.debug("No existing checkpoint state found. Starting from the first 
available matching file.");
+      return;
+    }
+
+    FileSlot currentSlot = files.stream()
+        .filter(file -> 
file.fileName().equals(checkpoint.getCurrentFileName()))
+        .findFirst()
+        .orElse(null);
+
+    if (currentSlot == null) {
+      LOG.debug("Checkpoint file '{}' is no longer present in '{}'. Continuing 
with following files.",
+          checkpoint.getCurrentFileName(), config.directory());
+      return;
+    }
+
+    long startRecord = determineStartRecord(currentSlot, checkpoint);
+    LOG.debug("Resuming file '{}' at record {}.", currentSlot.fileName(), 
startRecord);
+
+    readFile(adapterElementId, collector, checkpoint, currentSlot, 
startRecord);
+  }
+
+  private long determineStartRecord(FileSlot currentSlot, 
FileWatcherCheckpoint checkpoint) {
+    if (currentSlot.fingerprint().equals(checkpoint.getCurrentFingerprint())) {
+      LOG.debug("File '{}' fingerprint unchanged. Continuing after record {}.",
+          currentSlot.fileName(), checkpoint.getLastProcessedRecord());
+      return checkpoint.getLastProcessedRecord() + 1;
+    }
+
+    if (config.singleFileGrowthMode() && 
isAppendedInPlace(currentSlot.fingerprint(), 
checkpoint.getCurrentFingerprint())) {
+      LOG.debug("File '{}' grew in place. Continuing after record {}.",
+          currentSlot.fileName(), checkpoint.getLastProcessedRecord());
+      return checkpoint.getLastProcessedRecord() + 1;
+    }
+
+    LOG.debug("File '{}' fingerprint changed. Restarting from record 0.", 
currentSlot.fileName());
+    return 0;
+  }
+
+  private boolean isAppendedInPlace(FileFingerprint currentFingerprint, 
FileFingerprint previousFingerprint) {
+    return currentFingerprint.getSize() > previousFingerprint.getSize()
+        && currentFingerprint.getLastModified() >= 
previousFingerprint.getLastModified();
+  }
+
+  private void processFollowingFiles(String adapterElementId,
+                                     IEventCollector collector,
+                                     FileWatcherCheckpoint checkpoint,
+                                     List<FileSlot> files) throws IOException {
+    int startIndex = startIndex(files, checkpoint);
+    LOG.debug("Processing following files starting at index {}.", startIndex);
+    for (int offset = 0; offset < files.size(); offset++) {
+      FileSlot candidate = files.get((startIndex + offset) % files.size());
+      FileFingerprint processedFingerprint = 
checkpoint.getProcessedGenerations().get(candidate.fileName());
+      if (processedFingerprint != null && 
processedFingerprint.equals(candidate.fingerprint())) {
+        LOG.debug("Stopping at file '{}' because this generation was already 
processed.", candidate.fileName());
+        break;
+      }
+
+      LOG.debug("Reading new file generation '{}' from record 0.", 
candidate.fileName());
+      readFile(adapterElementId, collector, checkpoint, candidate, 0);
+    }
+  }
+
+  private void readFile(String adapterElementId,
+                        IEventCollector collector,
+                        FileWatcherCheckpoint checkpoint,
+                        FileSlot fileSlot,
+                        long startRecord) throws IOException {
+    LOG.debug(
+        "Reading file '{}' (sequence={}, size={}, lastModified={}) from record 
{} with interEventDelayMs={}.",
+        fileSlot.fileName(),
+        fileSlot.sequence(),
+        fileSlot.fingerprint().getSize(),
+        fileSlot.fingerprint().getLastModified(),
+        startRecord,
+        config.interEventDelayMs()
+    );
+    final boolean[] firstEvent = {true};
+    var result = csvFileReader.readFrom(fileSlot.path(), 
config.parserSettings(), startRecord, (recordIndex, event) -> {
+      delayBeforeNextEvent(firstEvent[0], fileSlot.fileName(), recordIndex);
+      firstEvent[0] = false;
+      if (LOG.isTraceEnabled()) {
+        LOG.trace("Collected record {} from file '{}'.", recordIndex, 
fileSlot.fileName());
+      }
+      collector.collect(eventMapper.map(event));
+      checkpoint.setCurrentFileName(fileSlot.fileName());
+      checkpoint.setCurrentSequence(fileSlot.sequence());
+      checkpoint.setCurrentFingerprint(fileSlot.fingerprint());
+      checkpoint.setLastProcessedRecord(recordIndex);
+      checkpoint.getProcessedGenerations().put(fileSlot.fileName(), 
fileSlot.fingerprint());
+
+      try {
+        checkpointStore.save(adapterElementId, checkpoint);
+        if (LOG.isTraceEnabled()) {
+          LOG.trace("Checkpoint saved for file '{}' at record {}.", 
fileSlot.fileName(), recordIndex);
+        }
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+    });
+
+    checkpoint.setCurrentFileName(fileSlot.fileName());
+    checkpoint.setCurrentSequence(fileSlot.sequence());
+    checkpoint.setCurrentFingerprint(fileSlot.fingerprint());
+    checkpoint.setLastProcessedRecord(result.lastProcessedRecord());
+    checkpoint.getProcessedGenerations().put(fileSlot.fileName(), 
fileSlot.fingerprint());
+    checkpointStore.save(adapterElementId, checkpoint);
+    LOG.debug(
+        "Finished file '{}'. Emitted {} events, lastProcessedRecord={}.",
+        fileSlot.fileName(),
+        result.emittedEvents(),
+        result.lastProcessedRecord()
+    );
+  }
+
+  private void delayBeforeNextEvent(boolean firstEvent,
+                                    String fileName,
+                                    long recordIndex) {
+    if (firstEvent || config.interEventDelayMs() <= 0) {
+      return;
+    }
+
+    try {
+      if (LOG.isTraceEnabled()) {
+        LOG.trace("Sleeping {} ms before record {} from file '{}'.",
+            config.interEventDelayMs(), recordIndex, fileName);
+      }
+      eventDelayExecutor.sleep(config.interEventDelayMs());
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new RuntimeException("Interrupted while delaying replay for file " 
+ fileName, e);
+    }
+  }
+
+  List<FileSlot> discoverFiles() throws IOException {
+    List<FileSlot> files = new ArrayList<>();
+    try (var stream = Files.list(config.directory())) {
+      stream.filter(Files::isRegularFile)
+          .filter(path -> 
config.filePattern().matcher(path.getFileName().toString()).matches())
+          .forEach(path -> files.add(toFileSlot(path)));
+    }
+
+    
files.sort(Comparator.comparingLong(FileSlot::sequence).thenComparing(FileSlot::fileName));
+    if (LOG.isDebugEnabled()) {
+      LOG.debug("Discovered {} matching WinCC archive files: {}",
+          files.size(),
+          files.stream().map(file -> file.fileName() + "@" + 
file.sequence()).toList());
+    }
+    return files;
+  }
+
+  private int startIndex(List<FileSlot> files, FileWatcherCheckpoint 
checkpoint) {
+    if (checkpoint.getCurrentFileName() == null) {
+      return 0;
+    }
+
+    for (int i = 0; i < files.size(); i++) {
+      FileSlot slot = files.get(i);
+      if (slot.sequence() > checkpoint.getCurrentSequence()) {
+        return i;
+      }
+      if (slot.sequence() == checkpoint.getCurrentSequence()
+          && slot.fileName().compareTo(checkpoint.getCurrentFileName()) > 0) {
+        return i;
+      }
+    }
+
+    return 0;
+  }
+
+  private FileSlot toFileSlot(Path path) {
+    String fileName = path.getFileName().toString();
+    return new FileSlot(fileName, path, sequence(fileName), fingerprint(path));
+  }
+
+  private long sequence(String fileName) {
+    Matcher matcher = config.filePattern().matcher(fileName);
+    if (matcher.matches() && matcher.groupCount() >= 1) {
+      try {
+        return Long.parseLong(matcher.group(1));
+      } catch (NumberFormatException ignored) {
+        return Long.MAX_VALUE;
+      }
+    }
+
+    return Long.MAX_VALUE;
+  }
+
+  private FileFingerprint fingerprint(Path path) {
+    try {
+      long size = Files.size(path);
+      long lastModified = Files.getLastModifiedTime(path).toMillis();
+      String hash = sha256(path);
+      return new FileFingerprint(size, lastModified, hash);
+    } catch (IOException e) {
+      throw new RuntimeException("Could not create fingerprint for " + 
path.getFileName(), e);
+    }
+  }
+
+  private String sha256(Path path) throws IOException {
+    try {
+      MessageDigest digest = MessageDigest.getInstance("SHA-256");
+      try (InputStream in = Files.newInputStream(path)) {
+        byte[] buffer = new byte[8192];
+        int read;
+        while ((read = in.read(buffer)) != -1) {
+          digest.update(buffer, 0, read);
+        }
+      }
+
+      return HexFormat.of().formatHex(digest.digest());
+    } catch (NoSuchAlgorithmException e) {
+      throw new IllegalStateException("SHA-256 is not available", e);
+    }
+  }
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/FileSlot.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/FileSlot.java
new file mode 100644
index 0000000000..9c75ed052d
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/FileSlot.java
@@ -0,0 +1,26 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.runtime;
+
+import 
org.apache.streampipes.extensions.connectors.filewatcher.model.FileFingerprint;
+
+import java.nio.file.Path;
+
+public record FileSlot(String fileName, Path path, long sequence, 
FileFingerprint fingerprint) {
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/FileWatchReadResult.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/FileWatchReadResult.java
new file mode 100644
index 0000000000..ff64ff4f12
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/FileWatchReadResult.java
@@ -0,0 +1,24 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.runtime;
+
+public record FileWatchReadResult(long lastProcessedRecord,
+                                  int emittedEvents,
+                                  boolean reachedEndOfFile) {
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/FileWatcherCheckpointStore.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/FileWatcherCheckpointStore.java
new file mode 100644
index 0000000000..15150c69bb
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/FileWatcherCheckpointStore.java
@@ -0,0 +1,70 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.runtime;
+
+import org.apache.streampipes.commons.environment.Environments;
+import 
org.apache.streampipes.extensions.connectors.filewatcher.model.FileWatcherCheckpoint;
+import org.apache.streampipes.serializers.json.JacksonSerializer;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class FileWatcherCheckpointStore {
+
+  private static final ObjectMapper MAPPER = 
JacksonSerializer.getObjectMapper();
+  private final Path baseDirectory;
+
+  public FileWatcherCheckpointStore() {
+    this(resolveDefaultDirectory());
+  }
+
+  public FileWatcherCheckpointStore(Path baseDirectory) {
+    this.baseDirectory = baseDirectory;
+  }
+
+  public FileWatcherCheckpoint load(String adapterElementId) throws 
IOException {
+    Path checkpointPath = checkpointPath(adapterElementId);
+    if (!Files.exists(checkpointPath)) {
+      return new FileWatcherCheckpoint();
+    }
+
+    return MAPPER.readValue(checkpointPath.toFile(), 
FileWatcherCheckpoint.class);
+  }
+
+  public void save(String adapterElementId, FileWatcherCheckpoint checkpoint) 
throws IOException {
+    Files.createDirectories(baseDirectory);
+    MAPPER.writeValue(checkpointPath(adapterElementId).toFile(), checkpoint);
+  }
+
+  private Path checkpointPath(String adapterElementId) {
+    return baseDirectory.resolve(adapterElementId + ".json");
+  }
+
+  private static Path resolveDefaultDirectory() {
+    String baseDir = Environments
+        .getEnvironment()
+        .getExtAssetBaseDir()
+        .getValueOrReturn(System.getProperty("user.home"));
+
+    return Path.of(baseDir, ".streampipes", "service", 
"filewatcher-checkpoints");
+  }
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/resources/org.apache.streampipes.connectors.wincc.alarm.archive/documentation.md
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/resources/org.apache.streampipes.connectors.wincc.alarm.archive/documentation.md
new file mode 100644
index 0000000000..80c5e7556d
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/resources/org.apache.streampipes.connectors.wincc.alarm.archive/documentation.md
@@ -0,0 +1,45 @@
+<!--
+  ~ 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.
+  ~
+  -->
+
+The WinCC Alarm Archive adapter reads Siemens WinCC alarm archives exported as 
CSV files.
+
+It is intended for the WinCC alarm CSV structure and for both archive modes:
+
+- single archive file
+- segmented circular log with multiple rotating CSV segments
+
+Configuration is Siemens-specific:
+
+- archive directory
+- archive base name
+- segmented circular log enabled or disabled
+- segment count in segmented circular log mode
+- polling interval
+- optional delay between replayed events in milliseconds
+
+The adapter assumes the WinCC alarm CSV layout with a header row and 
semicolon-separated columns.
+The configured base name should not include the segment number or file suffix. 
For example, use `Meldungsarchiv`
+to match `Meldungsarchiv1.csv` or `Meldungsarchiv1.csv` to 
`MeldungsarchivN.csv`.
+
+In segmented circular log mode the adapter treats reused file names as new 
generations by fingerprinting file content,
+so a segment like `Meldungsarchiv1.csv` can be consumed again after WinCC 
rotates the archive.
+
+Use the inter-event delay when consumers or brokers cannot handle large replay 
bursts. A value like `1` ms can reduce
+event loss on slower consumers, while `0` disables throttling.
+
+The configured directory must be reachable from the extension runtime process 
or container.
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/main/resources/org.apache.streampipes.connectors.wincc.alarm.archive/strings.en
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/resources/org.apache.streampipes.connectors.wincc.alarm.archive/strings.en
new file mode 100644
index 0000000000..0012cb3226
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/main/resources/org.apache.streampipes.connectors.wincc.alarm.archive/strings.en
@@ -0,0 +1,28 @@
+# 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.
+
+org.apache.streampipes.connectors.wincc.alarm.archive.title=WinCC Alarm Archive
+org.apache.streampipes.connectors.wincc.alarm.archive.description=Reads 
Siemens WinCC alarm archive CSV files with fixed WinCC CSV semantics and 
segmented circular log rotation
+directory-path.title=Archive directory
+directory-path.description=Absolute path to the WinCC alarm archive directory
+archive-base-name.title=Archive base name
+archive-base-name.description=Base name of the WinCC alarm archive files 
without segment number or .csv suffix, for example Meldungsarchiv
+segmented-circular-log-enabled.title=Segmented circular log
+segmented-circular-log-enabled.description=Whether the WinCC alarm archive 
uses rotating file segments
+archive-segment-count.title=Segment count
+archive-segment-count.description=Number of archive segments in segmented 
circular log mode
+poll-interval-seconds.title=Polling interval (seconds)
+poll-interval-seconds.description=Interval used to scan the alarm archive 
files for new entries
+inter-event-delay-ms.title=Inter-event delay (ms)
+inter-event-delay-ms.description=Delay between replayed alarm events in 
milliseconds. Use 0 to disable throttling
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/test/java/org/apache/streampipes/extensions/connectors/filewatcher/adapter/WinCCAlarmEventMapperTest.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/test/java/org/apache/streampipes/extensions/connectors/filewatcher/adapter/WinCCAlarmEventMapperTest.java
new file mode 100644
index 0000000000..911f3ddb7e
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/test/java/org/apache/streampipes/extensions/connectors/filewatcher/adapter/WinCCAlarmEventMapperTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.adapter;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class WinCCAlarmEventMapperTest {
+
+  @Test
+  void shouldMapRawWinCCAlarmToNormalizedEvent() {
+    var rawEvent = Map.<String, Object>ofEntries(
+        Map.entry("Time_ms", "46120366239"),
+        Map.entry("MsgProc", "1"),
+        Map.entry("StateAfter", "3"),
+        Map.entry("MsgClass", "2"),
+        Map.entry("MsgNumber", "110001"),
+        Map.entry("Var1", "A"),
+        Map.entry("Var2", "B"),
+        Map.entry("Var3", ""),
+        Map.entry("Var4", ""),
+        Map.entry("Var5", ""),
+        Map.entry("Var6", ""),
+        Map.entry("Var7", ""),
+        Map.entry("Var8", ""),
+        Map.entry("TimeString", "08.04.26 08:47"),
+        Map.entry("MsgText", "Alarm text"),
+        Map.entry("PLC", "PLC_1")
+    );
+
+    var mappedEvent = new WinCCAlarmEventMapper().map(rawEvent);
+
+    assertEquals(1775638043050L, mappedEvent.get("timestamp"));
+    assertEquals("46120366239", mappedEvent.get("timestamp_ms"));
+    assertEquals("08.04.26 08:47", mappedEvent.get("timestamp_string"));
+    assertEquals("Alarm text", mappedEvent.get("message_text"));
+    assertEquals(110001, mappedEvent.get("message_number"));
+    assertEquals(1, mappedEvent.get("alarm_procedure_code"));
+    assertEquals("system_event", mappedEvent.get("alarm_procedure"));
+    assertEquals(3, mappedEvent.get("state_after_code"));
+    assertEquals("incoming_acknowledged", mappedEvent.get("state_after"));
+    assertEquals(2, mappedEvent.get("alarm_class_code"));
+    assertEquals("warnings", mappedEvent.get("alarm_class"));
+    assertEquals("PLC_1", mappedEvent.get("plc"));
+    assertEquals("A", mappedEvent.get("parameter_1"));
+    assertEquals("B", mappedEvent.get("parameter_2"));
+    assertFalse(mappedEvent.containsKey("raw_Var1"));
+    assertNull(mappedEvent.get("raw_MsgText"));
+    assertNull(mappedEvent.get("raw_StateAfter"));
+    assertEquals("46120366239", mappedEvent.get("raw_Time_ms"));
+    assertEquals("08.04.26 08:47", mappedEvent.get("raw_TimeString"));
+  }
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/test/java/org/apache/streampipes/extensions/connectors/filewatcher/adapter/WinCCTimestampConverterTest.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/test/java/org/apache/streampipes/extensions/connectors/filewatcher/adapter/WinCCTimestampConverterTest.java
new file mode 100644
index 0000000000..5f1c05add2
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/test/java/org/apache/streampipes/extensions/connectors/filewatcher/adapter/WinCCTimestampConverterTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.adapter;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class WinCCTimestampConverterTest {
+
+  @Test
+  void shouldConvertWinCCTimestampToUnixMillis() {
+    assertEquals(1072876370999L,
+        WinCCTimestampConverter.toUnixTimestampMillis("37986.55059027", 
"30.12.03 13:12:51"));
+    assertEquals(1072876370999L,
+        WinCCTimestampConverter.toUnixTimestampMillis("37986,55059027", 
"30.12.03 13:12:51"));
+    assertEquals(1775638043050L,
+        WinCCTimestampConverter.toUnixTimestampMillis(46120366239L, "08.04.26 
08:47"));
+  }
+
+  @Test
+  void shouldReturnNullForInvalidInput() {
+    assertNull(WinCCTimestampConverter.toUnixTimestampMillis(null, null));
+    assertNull(WinCCTimestampConverter.toUnixTimestampMillis("", ""));
+    
assertNull(WinCCTimestampConverter.toUnixTimestampMillis("not-a-timestamp", 
"also-invalid"));
+  }
+
+  @Test
+  void shouldFallbackToTimeStringWhenNeeded() {
+    assertEquals(1775630820000L,
+        WinCCTimestampConverter.toUnixTimestampMillis(null, "08.04.26 08:47"));
+  }
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/test/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/CsvFileReaderTest.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/test/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/CsvFileReaderTest.java
new file mode 100644
index 0000000000..ecef4cf6b4
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/test/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/CsvFileReaderTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.runtime;
+
+import 
org.apache.streampipes.extensions.connectors.filewatcher.model.CsvParserSettings;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class CsvFileReaderTest {
+
+  @TempDir
+  Path tempDir;
+
+  @Test
+  void shouldKeepQuotedSeparatorsInsideOneField() throws IOException {
+    Path file = tempDir.resolve("Meldungsarchiv1.csv");
+    Files.writeString(file, "Time_ms;MsgText;PLC\n46120366239;\"Alarm; 
text\";PLC_1\n");
+
+    List<Map<String, Object>> events = new ArrayList<>();
+    new CsvFileReader().readFrom(file, new CsvParserSettings(true, ';'), 0, 
(recordIndex, event) -> events.add(event));
+
+    assertEquals(1, events.size());
+    assertEquals("Alarm; text", events.get(0).get("MsgText"));
+  }
+
+  @Test
+  void shouldPadMissingColumnsWithEmptyStrings() throws IOException {
+    Path file = tempDir.resolve("Meldungsarchiv1.csv");
+    Files.writeString(file, "Time_ms;MsgText;PLC\n46120366239;Alarm text\n");
+
+    List<Map<String, Object>> events = new ArrayList<>();
+    new CsvFileReader().readFrom(file, new CsvParserSettings(true, ';'), 0, 
(recordIndex, event) -> events.add(event));
+
+    assertEquals(1, events.size());
+    assertEquals("", events.get(0).get("PLC"));
+  }
+
+  @Test
+  void shouldFallbackToWindows1252ForWinCCExports() throws IOException {
+    Path file = tempDir.resolve("Meldungsarchiv1.csv");
+    Files.write(
+        file,
+        "Time_ms;MsgText;PLC\n46120366239;Störung 
Überfüllung;PLC_1\n".getBytes(Charset.forName("windows-1252"))
+    );
+
+    List<Map<String, Object>> events = new ArrayList<>();
+    new CsvFileReader().readFrom(file, new CsvParserSettings(true, ';'), 0, 
(recordIndex, event) -> events.add(event));
+
+    assertEquals(1, events.size());
+    assertEquals("Störung Überfüllung", events.get(0).get("MsgText"));
+  }
+}
diff --git 
a/streampipes-extensions/streampipes-connectors-filewatcher/src/test/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/FileSetWatcherTest.java
 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/test/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/FileSetWatcherTest.java
new file mode 100644
index 0000000000..5cc92c543f
--- /dev/null
+++ 
b/streampipes-extensions/streampipes-connectors-filewatcher/src/test/java/org/apache/streampipes/extensions/connectors/filewatcher/runtime/FileSetWatcherTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.streampipes.extensions.connectors.filewatcher.runtime;
+
+import 
org.apache.streampipes.extensions.connectors.filewatcher.model.CsvParserSettings;
+import 
org.apache.streampipes.extensions.connectors.filewatcher.model.FileWatcherCheckpoint;
+import 
org.apache.streampipes.extensions.connectors.filewatcher.model.FileWatcherConfig;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class FileSetWatcherTest {
+
+  @TempDir
+  Path tempDir;
+
+  @Test
+  void shouldReadInitialFilesAndOnlyNewGenerationAfterWrapAround() throws 
IOException {
+    write("Meldungsarchiv1", "id,value\n1,a\n2,b\n");
+    write("Meldungsarchiv2", "id,value\n3,c\n");
+
+    FileSetWatcher watcher = new FileSetWatcher(
+        new FileWatcherConfig(tempDir, 
Pattern.compile("Meldungsarchiv(\\d+)"), new CsvParserSettings(true, ','), 1, 
false, 0),
+        new FileWatcherCheckpointStore(tempDir.resolve("checkpoints")),
+        new CsvFileReader(),
+        EventMapper.identity()
+    );
+
+    List<Map<String, Object>> events = new ArrayList<>();
+    watcher.poll("adapter-1", events::add);
+
+    assertEquals(3, events.size());
+    assertEquals("1", events.get(0).get("id"));
+    assertEquals("3", events.get(2).get("id"));
+
+    events.clear();
+    watcher.poll("adapter-1", events::add);
+    assertEquals(0, events.size());
+
+    write("Meldungsarchiv1", "id,value\n4,d\n5,e\n");
+    watcher.poll("adapter-1", events::add);
+
+    assertEquals(2, events.size());
+    assertEquals("4", events.get(0).get("id"));
+    assertEquals("5", events.get(1).get("id"));
+  }
+
+  @Test
+  void shouldResumeWithinSameGenerationFromCheckpoint() throws IOException {
+    write("Meldungsarchiv1", "id,value\n1,a\n2,b\n3,c\n");
+    Path checkpointDir = tempDir.resolve("checkpoints");
+    FileWatcherCheckpointStore checkpointStore = new 
FileWatcherCheckpointStore(checkpointDir);
+
+    FileSetWatcher watcher = new FileSetWatcher(
+        new FileWatcherConfig(tempDir, 
Pattern.compile("Meldungsarchiv(\\d+)"), new CsvParserSettings(true, ','), 1, 
false, 0),
+        checkpointStore,
+        new CsvFileReader(),
+        EventMapper.identity()
+    );
+
+    FileSlot slot = watcher.discoverFiles().get(0);
+    FileWatcherCheckpoint checkpoint = new FileWatcherCheckpoint();
+    checkpoint.setCurrentFileName(slot.fileName());
+    checkpoint.setCurrentSequence(slot.sequence());
+    checkpoint.setCurrentFingerprint(slot.fingerprint());
+    checkpoint.setLastProcessedRecord(1L);
+    checkpoint.getProcessedGenerations().put(slot.fileName(), 
slot.fingerprint());
+    checkpointStore.save("adapter-2", checkpoint);
+
+    List<Map<String, Object>> events = new ArrayList<>();
+    watcher.poll("adapter-2", events::add);
+
+    assertEquals(1, events.size());
+    assertEquals("3", events.get(0).get("id"));
+  }
+
+  @Test
+  void shouldContinueFromLastLineWhenSingleFileGrows() throws IOException {
+    write("Meldungsarchiv.csv", "id,value\n1,a\n2,b\n");
+
+    FileSetWatcher watcher = new FileSetWatcher(
+        new FileWatcherConfig(tempDir, 
Pattern.compile("Meldungsarchiv\\.csv"), new CsvParserSettings(true, ','), 1, 
true, 0),
+        new FileWatcherCheckpointStore(tempDir.resolve("checkpoints-single")),
+        new CsvFileReader(),
+        EventMapper.identity()
+    );
+
+    List<Map<String, Object>> events = new ArrayList<>();
+    watcher.poll("adapter-single", events::add);
+
+    assertEquals(2, events.size());
+    events.clear();
+
+    write("Meldungsarchiv.csv", "id,value\n1,a\n2,b\n3,c\n4,d\n");
+    watcher.poll("adapter-single", events::add);
+
+    assertEquals(2, events.size());
+    assertEquals("3", events.get(0).get("id"));
+    assertEquals("4", events.get(1).get("id"));
+  }
+
+  @Test
+  void shouldRestartFromBeginningWhenSingleFileIsTruncatedOrReplaced() throws 
IOException {
+    write("Meldungsarchiv.csv", "id,value\n1,a\n2,b\n3,c\n");
+
+    FileSetWatcher watcher = new FileSetWatcher(
+        new FileWatcherConfig(tempDir, 
Pattern.compile("Meldungsarchiv\\.csv"), new CsvParserSettings(true, ','), 1, 
true, 0),
+        new FileWatcherCheckpointStore(tempDir.resolve("checkpoints-replace")),
+        new CsvFileReader(),
+        EventMapper.identity()
+    );
+
+    watcher.poll("adapter-replace", event -> {
+    });
+
+    List<Map<String, Object>> events = new ArrayList<>();
+    write("Meldungsarchiv.csv", "id,value\n10,x\n11,y\n");
+    watcher.poll("adapter-replace", events::add);
+
+    assertEquals(2, events.size());
+    assertEquals("10", events.get(0).get("id"));
+    assertEquals("11", events.get(1).get("id"));
+  }
+
+  @Test
+  void shouldDelayBetweenEventsWhenConfigured() throws IOException {
+    write("Meldungsarchiv1", "id,value\n1,a\n2,b\n3,c\n");
+    AtomicInteger delayCalls = new AtomicInteger();
+    List<Long> appliedDelays = new ArrayList<>();
+
+    FileSetWatcher watcher = new FileSetWatcher(
+        new FileWatcherConfig(tempDir, 
Pattern.compile("Meldungsarchiv(\\d+)"), new CsvParserSettings(true, ','), 1, 
false, 5),
+        new FileWatcherCheckpointStore(tempDir.resolve("checkpoints-delay")),
+        new CsvFileReader(),
+        EventMapper.identity(),
+        delayMs -> {
+          delayCalls.incrementAndGet();
+          appliedDelays.add(delayMs);
+        }
+    );
+
+    List<Map<String, Object>> events = new ArrayList<>();
+    watcher.poll("adapter-delay", events::add);
+
+    assertEquals(3, events.size());
+    assertEquals(2, delayCalls.get());
+    assertEquals(List.of(5L, 5L), appliedDelays);
+  }
+
+  private void write(String fileName, String content) throws IOException {
+    Files.writeString(tempDir.resolve(fileName), content);
+  }
+}
diff --git a/streampipes-extensions/streampipes-extensions-all-iiot/pom.xml 
b/streampipes-extensions/streampipes-extensions-all-iiot/pom.xml
index 4507aff55c..47e559cda0 100644
--- a/streampipes-extensions/streampipes-extensions-all-iiot/pom.xml
+++ b/streampipes-extensions/streampipes-extensions-all-iiot/pom.xml
@@ -56,6 +56,11 @@
                 </exclusion>
             </exclusions>
         </dependency>
+        <dependency>
+            <groupId>org.apache.streampipes</groupId>
+            <artifactId>streampipes-connectors-filewatcher</artifactId>
+            <version>0.99.0-SNAPSHOT</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.streampipes</groupId>
             <artifactId>streampipes-connectors-influx</artifactId>
diff --git 
a/streampipes-extensions/streampipes-extensions-all-iiot/src/main/java/org/apache/streampipes/extensions/all/iiot/AllExtensionsIIoTInit.java
 
b/streampipes-extensions/streampipes-extensions-all-iiot/src/main/java/org/apache/streampipes/extensions/all/iiot/AllExtensionsIIoTInit.java
index fb64c58b37..179f9b4ede 100644
--- 
a/streampipes-extensions/streampipes-extensions-all-iiot/src/main/java/org/apache/streampipes/extensions/all/iiot/AllExtensionsIIoTInit.java
+++ 
b/streampipes-extensions/streampipes-extensions-all-iiot/src/main/java/org/apache/streampipes/extensions/all/iiot/AllExtensionsIIoTInit.java
@@ -20,6 +20,7 @@ package org.apache.streampipes.extensions.all.iiot;
 
 import org.apache.streampipes.connect.iiot.IIoTAdaptersExtensionModuleExport;
 import org.apache.streampipes.connectors.ros.RosConnectorsModuleExport;
+import 
org.apache.streampipes.extensions.connectors.filewatcher.FileWatcherConnectorsModuleExport;
 import 
org.apache.streampipes.extensions.connectors.influx.InfluxConnectorsModuleExport;
 import 
org.apache.streampipes.extensions.connectors.kafka.KafkaConnectorsModuleExport;
 import 
org.apache.streampipes.extensions.connectors.mqtt.MqttConnectorsModuleExport;
@@ -65,6 +66,7 @@ public class AllExtensionsIIoTInit extends 
StreamPipesExtensionsServiceBase {
         .registerModules(
             new IIoTAdaptersExtensionModuleExport(),
 
+            new FileWatcherConnectorsModuleExport(),
             new InfluxConnectorsModuleExport(),
             new KafkaConnectorsModuleExport(),
             new MqttConnectorsModuleExport(),
diff --git a/streampipes-extensions/streampipes-extensions-all-jvm/pom.xml 
b/streampipes-extensions/streampipes-extensions-all-jvm/pom.xml
index f6da620463..a165ea8b03 100644
--- a/streampipes-extensions/streampipes-extensions-all-jvm/pom.xml
+++ b/streampipes-extensions/streampipes-extensions-all-jvm/pom.xml
@@ -74,6 +74,11 @@
             <artifactId>streampipes-connectors-camel-azure</artifactId>
             <version>0.99.0-SNAPSHOT</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.streampipes</groupId>
+            <artifactId>streampipes-connectors-filewatcher</artifactId>
+            <version>0.99.0-SNAPSHOT</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.streampipes</groupId>
             <artifactId>streampipes-connectors-influx</artifactId>
diff --git 
a/streampipes-extensions/streampipes-extensions-all-jvm/src/main/java/org/apache/streampipes/extensions/all/jvm/AllExtensionsInit.java
 
b/streampipes-extensions/streampipes-extensions-all-jvm/src/main/java/org/apache/streampipes/extensions/all/jvm/AllExtensionsInit.java
index e39dbdf0d8..ef031f35f3 100644
--- 
a/streampipes-extensions/streampipes-extensions-all-jvm/src/main/java/org/apache/streampipes/extensions/all/jvm/AllExtensionsInit.java
+++ 
b/streampipes-extensions/streampipes-extensions-all-jvm/src/main/java/org/apache/streampipes/extensions/all/jvm/AllExtensionsInit.java
@@ -21,6 +21,7 @@ import 
org.apache.streampipes.connect.GeneralAdaptersExtensionModuleExport;
 import org.apache.streampipes.connect.iiot.IIoTAdaptersExtensionModuleExport;
 import org.apache.streampipes.connectors.ros.RosConnectorsModuleExport;
 import 
org.apache.streampipes.extensions.connectors.camel.azure.CamelAzureConnectorsModuleExport;
+import 
org.apache.streampipes.extensions.connectors.filewatcher.FileWatcherConnectorsModuleExport;
 import 
org.apache.streampipes.extensions.connectors.influx.InfluxConnectorsModuleExport;
 import 
org.apache.streampipes.extensions.connectors.kafka.KafkaConnectorsModuleExport;
 import 
org.apache.streampipes.extensions.connectors.mqtt.MqttConnectorsModuleExport;
@@ -69,6 +70,7 @@ public class AllExtensionsInit extends 
StreamPipesExtensionsServiceBase {
             new IIoTAdaptersExtensionModuleExport(),
 
             new CamelAzureConnectorsModuleExport(),
+            new FileWatcherConnectorsModuleExport(),
             new InfluxConnectorsModuleExport(),
             new KafkaConnectorsModuleExport(),
             new MqttConnectorsModuleExport(),

Reply via email to