diff --git a/CODEOWNERS b/CODEOWNERS
index 0eac39f7f38..e08a8885edf 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -77,6 +77,7 @@
/bundles/org.openhab.binding.feed/ @svilenvul
/bundles/org.openhab.binding.feican/ @Hilbrand
/bundles/org.openhab.binding.fmiweather/ @ssalonen
+/bundles/org.openhab.binding.folderwatcher/ @goopilot
/bundles/org.openhab.binding.folding/ @fa2k
/bundles/org.openhab.binding.foobot/ @airboxlab @Hilbrand
/bundles/org.openhab.binding.freebox/ @lolodomo
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 2190e4f6ad7..37bf3878997 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -371,6 +371,11 @@
org.openhab.binding.fmiweather
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.folderwatcher
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.folding
diff --git a/bundles/org.openhab.binding.folderwatcher/NOTICE b/bundles/org.openhab.binding.folderwatcher/NOTICE
new file mode 100644
index 00000000000..38d625e3492
--- /dev/null
+++ b/bundles/org.openhab.binding.folderwatcher/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.folderwatcher/README.md b/bundles/org.openhab.binding.folderwatcher/README.md
new file mode 100755
index 00000000000..03cb040ad7b
--- /dev/null
+++ b/bundles/org.openhab.binding.folderwatcher/README.md
@@ -0,0 +1,82 @@
+# FolderWatcher Binding
+
+This binding is intended to monitor FTP and local folder and its subfolders and notify of new files
+
+## Supported Things
+
+Currently the binding support two types of things: `ftpfolder` and `localfolder`.
+
+
+## Thing Configuration
+
+The `ftpfolder` thing has the following configuration options:
+
+| Parameter | Name | Description | Required | Default value |
+|-------------|--------------|------------------------------------------------------------------------------------------------------------------------|----------|---------------|
+| ftpAddress | FTP server | IP address of FTP server | yes | n/a |
+| ftpPort | FTP port | Port of FTP server | yes | 21 |
+| secureMode | FTP Security | FTP Security | yes | None |
+| ftpUsername | Username | FTP user name | yes | n/a |
+| ftpPassword | Password | FTP password | yes | n/a |
+| ftpDir | RootDir | Root directory to be watched | yes | n/a |
+| listRecursiveFtp | List Sub Folders | Allow listing of sub folders | yes | No |
+| listHidden | List Hidden | Allow listing of hidden files | yes | false |
+| connectionTimeout | Connection timeout, s | Connection timeout for FTP request | yes | 30 |
+| pollInterval | Polling interval, s | Interval for polling folder changes | yes | 60 |
+| diffHours | Time stamp difference, h | How many hours back to analyze | yes | 24 |
+
+The `localfolder` thing has the following configuration options:
+
+| Parameter | Name | Description | Required | Default value |
+|-------------|--------------|------------------------------------------------------------------------------------------------------------------------|----------|---------------|
+| localDir | Local Directory | Local directory to be watched | yes | n/a |
+| listHiddenLocal | List Hidden | Allow listing of hidden files | yes | No |
+| pollIntervalLocal | Polling interval, s | Interval for polling folder changes | yes | 60 |
+| listRecursiveLocal | List Sub Folders | Allow listing of sub folders | yes | No |
+
+## Events
+
+This binding currently supports the following events:
+
+| Channel Type ID | Item Type | Description |
+|-----------------|--------------|----------------------------------------------------------------------------------------|
+| newftpfile | String | A new file name discovered on FTP |
+| newlocalfile | String | A new file name discovered on in local folder |
+
+
+## Full Example
+
+Thing configuration:
+
+```java
+folderwatcher:localfolder:myLocalFolder [ localDir="/tmp/dumps", pollIntervalLocal=60, listHiddenLocal="false", listRecursiveLocal="false" ]
+folderwatcher:ftpfolder:myLocalFolder [ ftpAddress="192.168.0.222", ftpPort=21, secureMode="EXPLICIT", ftpUsername="ftpuser", ftpPassword="ftppass",ftpDir="/suvcams/192.168.0.209",listHidden="true",listRecursiveFtp="true",connectionTimeout=33,pollInterval=66,diffHours=25]
+```
+
+### Using in a rule:
+
+FTP example:
+
+```java
+rule "New FTP file"
+when
+ Channel 'folderwatcher:ftpfolder:XXXXX:newfile' triggered
+then
+
+ logInfo('NewFTPFile', receivedEvent.toString())
+
+end
+```
+
+Local folder example:
+
+```java
+rule "New Local file"
+when
+ Channel 'folderwatcher:localfolder:XXXXX:newfile' triggered
+then
+
+ logInfo('NewLocalFile', receivedEvent.toString())
+
+end
+```
diff --git a/bundles/org.openhab.binding.folderwatcher/pom.xml b/bundles/org.openhab.binding.folderwatcher/pom.xml
new file mode 100644
index 00000000000..856523c8706
--- /dev/null
+++ b/bundles/org.openhab.binding.folderwatcher/pom.xml
@@ -0,0 +1,25 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.1.0-SNAPSHOT
+
+
+ org.openhab.binding.folderwatcher
+
+ openHAB Add-ons :: Bundles :: FolderWatcher Binding
+
+
+
+ commons-net
+ commons-net
+ 3.7.2
+
+
+
+
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/feature/feature.xml b/bundles/org.openhab.binding.folderwatcher/src/main/feature/feature.xml
new file mode 100644
index 00000000000..b19d79626f1
--- /dev/null
+++ b/bundles/org.openhab.binding.folderwatcher/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.folderwatcher/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherBindingConstants.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherBindingConstants.java
new file mode 100755
index 00000000000..174c328ae47
--- /dev/null
+++ b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherBindingConstants.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.folderwatcher.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link FolderWatcherBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Alexandr Salamatov - Initial contribution
+ */
+@NonNullByDefault
+public class FolderWatcherBindingConstants {
+ private static final String BINDING_ID = "folderwatcher";
+ public static final ThingTypeUID THING_TYPE_FTPFOLDER = new ThingTypeUID(BINDING_ID, "ftpfolder");
+ public static final ThingTypeUID THING_TYPE_LOCALFOLDER = new ThingTypeUID(BINDING_ID, "localfolder");
+ public static final String CHANNEL_NEWFILE = "newfile";
+}
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherHandlerFactory.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherHandlerFactory.java
new file mode 100755
index 00000000000..0fa785c14e6
--- /dev/null
+++ b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherHandlerFactory.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.folderwatcher.internal;
+
+import static org.openhab.binding.folderwatcher.internal.FolderWatcherBindingConstants.*;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.folderwatcher.internal.handler.FtpFolderWatcherHandler;
+import org.openhab.binding.folderwatcher.internal.handler.LocalFolderWatcherHandler;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link FolderWatcherHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Alexandr Salamatov - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.folderwatcher", service = ThingHandlerFactory.class)
+public class FolderWatcherHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_FTPFOLDER,
+ THING_TYPE_LOCALFOLDER);
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (THING_TYPE_FTPFOLDER.equals(thingTypeUID)) {
+ return new FtpFolderWatcherHandler(thing);
+ } else if (THING_TYPE_LOCALFOLDER.equals(thingTypeUID)) {
+ return new LocalFolderWatcherHandler(thing);
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/SecureMode.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/SecureMode.java
new file mode 100644
index 00000000000..b65a78828af
--- /dev/null
+++ b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/SecureMode.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.folderwatcher.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link FolderWatcherBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Alexandr Salamatov - Initial contribution
+ */
+@NonNullByDefault
+public enum SecureMode {
+ NONE,
+ IMPLICIT,
+ EXPLICIT
+}
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/common/WatcherCommon.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/common/WatcherCommon.java
new file mode 100755
index 00000000000..239e6dafb13
--- /dev/null
+++ b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/common/WatcherCommon.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.folderwatcher.internal.common;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link WatcherCommon} class contains commonly used methods.
+ *
+ * @author Alexandr Salamatov - Initial contribution
+ */
+@NonNullByDefault
+public class WatcherCommon {
+
+ private static void initFile(File file, String watchDir) throws IOException {
+ try (BufferedWriter fileWriter = new BufferedWriter(new FileWriter(file))) {
+ fileWriter.write(watchDir);
+ fileWriter.newLine();
+ }
+ }
+
+ public static List initStorage(File file, String watchDir) throws IOException {
+ List returnList = List.of();
+ List currentFileListing = List.of();
+ if (!file.exists()) {
+ Files.createDirectories(file.toPath().getParent());
+ initFile(file, watchDir);
+ } else {
+ currentFileListing = Files.readAllLines(file.toPath().toAbsolutePath());
+ if (currentFileListing.get(0).equals(watchDir)) {
+ returnList = currentFileListing;
+ } else {
+ initFile(file, watchDir);
+ }
+ }
+ return returnList;
+ }
+
+ public static void saveNewListing(List newList, File listingFile) throws IOException {
+ try (BufferedWriter fileWriter = new BufferedWriter(new FileWriter(listingFile, true))) {
+ for (String newFile : newList) {
+ fileWriter.write(newFile);
+ fileWriter.newLine();
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/FtpFolderWatcherConfiguration.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/FtpFolderWatcherConfiguration.java
new file mode 100755
index 00000000000..7a4448ea3b8
--- /dev/null
+++ b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/FtpFolderWatcherConfiguration.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.folderwatcher.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.folderwatcher.internal.SecureMode;
+
+/**
+ * The {@link FtpFolderWatcherConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Alexandr Salamatov - Initial contribution
+ */
+@NonNullByDefault
+public class FtpFolderWatcherConfiguration {
+ public String ftpAddress = "";
+ public int ftpPort;
+ public String ftpUsername = "";
+ public String ftpPassword = "";
+ public String ftpDir = "";
+ public int pollInterval;
+ public int connectionTimeout;
+ public boolean listHidden;
+ public int diffHours;
+ public boolean listRecursiveFtp;
+ public SecureMode secureMode = SecureMode.NONE;
+}
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/LocalFolderWatcherConfiguration.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/LocalFolderWatcherConfiguration.java
new file mode 100755
index 00000000000..18d31831b5e
--- /dev/null
+++ b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/LocalFolderWatcherConfiguration.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.folderwatcher.internal.config;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link LocalFolderWatcherConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Alexandr Salamatov - Initial contribution
+ */
+@NonNullByDefault
+public class LocalFolderWatcherConfiguration {
+ public String localDir = "";
+ public boolean listHiddenLocal;
+ public int pollIntervalLocal;
+ public boolean listRecursiveLocal;
+}
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/FtpFolderWatcherHandler.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/FtpFolderWatcherHandler.java
new file mode 100755
index 00000000000..c89fced81da
--- /dev/null
+++ b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/FtpFolderWatcherHandler.java
@@ -0,0 +1,247 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.folderwatcher.internal.handler;
+
+import static org.openhab.binding.folderwatcher.internal.FolderWatcherBindingConstants.CHANNEL_NEWFILE;
+
+import java.io.File;
+import java.io.IOException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.net.ftp.FTPClient;
+import org.apache.commons.net.ftp.FTPFile;
+import org.apache.commons.net.ftp.FTPReply;
+import org.apache.commons.net.ftp.FTPSClient;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.folderwatcher.internal.common.WatcherCommon;
+import org.openhab.binding.folderwatcher.internal.config.FtpFolderWatcherConfiguration;
+import org.openhab.core.OpenHAB;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link FtpFolderWatcherHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Alexandr Salamatov - Initial contribution
+ */
+@NonNullByDefault
+public class FtpFolderWatcherHandler extends BaseThingHandler {
+ private final Logger logger = LoggerFactory.getLogger(FtpFolderWatcherHandler.class);
+ private FtpFolderWatcherConfiguration config = new FtpFolderWatcherConfiguration();
+ private @Nullable File currentFtpListingFile;
+ private @Nullable ScheduledFuture> executionJob, initJob;
+ private FTPClient ftp = new FTPClient();
+ private List previousFtpListing = new ArrayList<>();
+
+ public FtpFolderWatcherHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("Channel {} triggered with command {}", channelUID.getId(), command);
+ if (command instanceof RefreshType) {
+ refreshFTPFolderInformation();
+ }
+ }
+
+ @Override
+ public void initialize() {
+ File currentFtpListingFile;
+ config = getConfigAs(FtpFolderWatcherConfiguration.class);
+ updateStatus(ThingStatus.UNKNOWN);
+ if (config.connectionTimeout <= 0) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "Connection timeout can't be negative");
+ return;
+ }
+ if (config.ftpPort < 0) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "FTP port can't be negative");
+ return;
+ }
+ if (config.pollInterval <= 0) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "Polling interval can't be null or negative");
+ }
+
+ currentFtpListingFile = new File(OpenHAB.getUserDataFolder() + File.separator + "FolderWatcher" + File.separator
+ + thing.getUID().getAsString().replace(':', '_') + ".data");
+ try {
+ this.currentFtpListingFile = currentFtpListingFile;
+ previousFtpListing = WatcherCommon.initStorage(currentFtpListingFile, config.ftpAddress + config.ftpDir);
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+ logger.debug("Can't write file {}, error message {}", currentFtpListingFile, e.getMessage());
+ return;
+ }
+ this.initJob = scheduler.scheduleWithFixedDelay(this::connectionKeepAlive, 0, config.pollInterval,
+ TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void dispose() {
+ ScheduledFuture> executionJob = this.executionJob;
+ ScheduledFuture> initJob = this.initJob;
+ if (executionJob != null) {
+ executionJob.cancel(true);
+ }
+ if (initJob != null) {
+ initJob.cancel(true);
+ }
+ if (ftp.isConnected()) {
+ try {
+ ftp.logout();
+ ftp.disconnect();
+ } catch (IOException e) {
+ logger.debug("Error terminating FTP connection: ", e);
+ }
+ }
+ }
+
+ private void listDirectory(FTPClient ftpClient, String dirPath, boolean recursive, List dirFiles)
+ throws IOException {
+ Instant dateNow = Instant.now();
+ for (FTPFile file : ftpClient.listFiles(dirPath)) {
+ String currentFileName = file.getName();
+ if (currentFileName.equals(".") || currentFileName.equals("..")) {
+ continue;
+ }
+ String filePath = dirPath + "/" + currentFileName;
+ if (file.isDirectory()) {
+ if (recursive) {
+ try {
+ listDirectory(ftpClient, filePath, recursive, dirFiles);
+ } catch (IOException e) {
+ logger.debug("Can't read FTP directory: {}", filePath, e);
+ }
+ }
+ } else {
+ long diff = ChronoUnit.HOURS.between(file.getTimestamp().toInstant(), dateNow);
+ if (diff < config.diffHours) {
+ dirFiles.add("ftp:/" + ftpClient.getRemoteAddress() + filePath);
+ }
+ }
+ }
+ }
+
+ private void connectionKeepAlive() {
+ if (!ftp.isConnected()) {
+ switch (config.secureMode) {
+ case NONE:
+ ftp = new FTPClient();
+ break;
+ case IMPLICIT:
+ ftp = new FTPSClient(true);
+ break;
+ case EXPLICIT:
+ ftp = new FTPSClient(false);
+ break;
+ }
+
+ int reply = 0;
+ ftp.setListHiddenFiles(config.listHidden);
+ ftp.setConnectTimeout(config.connectionTimeout * 1000);
+
+ try {
+ ftp.connect(config.ftpAddress, config.ftpPort);
+ reply = ftp.getReplyCode();
+
+ if (!FTPReply.isPositiveCompletion(reply)) {
+ ftp.disconnect();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "FTP server refused connection.");
+ return;
+ }
+ } catch (IOException e) {
+ if (ftp.isConnected()) {
+ try {
+ ftp.disconnect();
+ } catch (IOException e2) {
+ logger.debug("Error disconneting, lost connection? : {}", e2.getMessage());
+ }
+ }
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ return;
+ }
+ try {
+ if (!ftp.login(config.ftpUsername, config.ftpPassword)) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ftp.getReplyString());
+ ftp.logout();
+ return;
+ }
+ updateStatus(ThingStatus.ONLINE);
+ ScheduledFuture> executionJob = this.executionJob;
+ if (executionJob != null) {
+ executionJob.cancel(true);
+ }
+ this.executionJob = scheduler.scheduleWithFixedDelay(this::refreshFTPFolderInformation, 0,
+ config.pollInterval, TimeUnit.SECONDS);
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ }
+ }
+ }
+
+ private void refreshFTPFolderInformation() {
+ String ftpRootDir = config.ftpDir;
+ final File currentFtpListingFile = this.currentFtpListingFile;
+ if (ftp.isConnected()) {
+ ftp.enterLocalPassiveMode();
+ try {
+ if (ftpRootDir.endsWith("/")) {
+ ftpRootDir = ftpRootDir.substring(0, ftpRootDir.length() - 1);
+ }
+ if (!ftpRootDir.startsWith("/")) {
+ ftpRootDir = "/" + ftpRootDir;
+ }
+ List currentFtpListing = new ArrayList<>();
+ listDirectory(ftp, ftpRootDir, config.listRecursiveFtp, currentFtpListing);
+ List diffFtpListing = new ArrayList<>(currentFtpListing);
+ diffFtpListing.removeAll(previousFtpListing);
+ diffFtpListing.forEach(file -> triggerChannel(CHANNEL_NEWFILE, file));
+ if (!diffFtpListing.isEmpty() && currentFtpListingFile != null) {
+ try {
+ WatcherCommon.saveNewListing(diffFtpListing, currentFtpListingFile);
+ } catch (IOException e2) {
+ logger.debug("Can't save new listing into file: {}", e2.getMessage());
+ }
+ }
+ previousFtpListing = new ArrayList<>(currentFtpListing);
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "FTP connection lost. " + e.getMessage());
+ try {
+ ftp.disconnect();
+ } catch (IOException e1) {
+ logger.debug("Error disconneting, lost connection? {}", e1.getMessage());
+ }
+ }
+ } else {
+ logger.debug("FTP connection lost.");
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/LocalFolderWatcherHandler.java b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/LocalFolderWatcherHandler.java
new file mode 100755
index 00000000000..14e55b91d3c
--- /dev/null
+++ b/bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/LocalFolderWatcherHandler.java
@@ -0,0 +1,162 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.folderwatcher.internal.handler;
+
+import static org.openhab.binding.folderwatcher.internal.FolderWatcherBindingConstants.CHANNEL_NEWFILE;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitor;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.folderwatcher.internal.common.WatcherCommon;
+import org.openhab.binding.folderwatcher.internal.config.LocalFolderWatcherConfiguration;
+import org.openhab.core.OpenHAB;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link LocalFolderWatcherHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Alexandr Salamatov - Initial contribution
+ */
+@NonNullByDefault
+public class LocalFolderWatcherHandler extends BaseThingHandler {
+ private final Logger logger = LoggerFactory.getLogger(LocalFolderWatcherHandler.class);
+ private LocalFolderWatcherConfiguration config = new LocalFolderWatcherConfiguration();
+ private File currentLocalListingFile = new File(OpenHAB.getUserDataFolder() + File.separator + "FolderWatcher"
+ + File.separator + thing.getUID().getAsString().replace(':', '_') + ".data");
+ private @Nullable ScheduledFuture> executionJob;
+ private List previousLocalListing = new ArrayList<>();
+
+ public LocalFolderWatcherHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("Channel {} triggered with command {}", channelUID.getId(), command);
+ if (command instanceof RefreshType) {
+ refreshFolderInformation();
+ }
+ }
+
+ @Override
+ public void initialize() {
+ config = getConfigAs(LocalFolderWatcherConfiguration.class);
+ updateStatus(ThingStatus.UNKNOWN);
+
+ if (!Files.isDirectory(Paths.get(config.localDir))) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Local directory is not valid");
+ return;
+ }
+ try {
+ previousLocalListing = WatcherCommon.initStorage(currentLocalListingFile, config.localDir);
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+ logger.debug("Can't write file {}: {}", currentLocalListingFile, e.getMessage());
+ return;
+ }
+
+ if (config.pollIntervalLocal > 0) {
+ updateStatus(ThingStatus.ONLINE);
+ executionJob = scheduler.scheduleWithFixedDelay(this::refreshFolderInformation, config.pollIntervalLocal,
+ config.pollIntervalLocal, TimeUnit.SECONDS);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "Polling interval can't be null or negative");
+ return;
+ }
+ }
+
+ @Override
+ public void dispose() {
+ ScheduledFuture> executionJob = this.executionJob;
+ if (executionJob != null) {
+ executionJob.cancel(true);
+ }
+ }
+
+ private void refreshFolderInformation() {
+ final String rootDir = config.localDir;
+ try {
+ List currentLocalListing = new ArrayList<>();
+
+ Files.walkFileTree(Paths.get(rootDir), new FileVisitor<@Nullable Path>() {
+ @Override
+ public FileVisitResult preVisitDirectory(@Nullable Path dir, @Nullable BasicFileAttributes attrs)
+ throws IOException {
+ if (dir != null) {
+ if (!dir.equals(Paths.get(rootDir)) && !config.listRecursiveLocal) {
+ return FileVisitResult.SKIP_SUBTREE;
+ }
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFile(@Nullable Path file, @Nullable BasicFileAttributes attrs)
+ throws IOException {
+ if (file != null) {
+ if (Files.isHidden(file) && !config.listHiddenLocal) {
+ return FileVisitResult.CONTINUE;
+ }
+ currentLocalListing.add(file.toAbsolutePath().toString());
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFileFailed(@Nullable Path file, @Nullable IOException exc)
+ throws IOException {
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(@Nullable Path dir, @Nullable IOException exc)
+ throws IOException {
+ return FileVisitResult.CONTINUE;
+ }
+ });
+
+ List diffLocalListing = new ArrayList<>(currentLocalListing);
+ diffLocalListing.removeAll(previousLocalListing);
+ diffLocalListing.forEach(file -> triggerChannel(CHANNEL_NEWFILE, file));
+
+ if (!diffLocalListing.isEmpty()) {
+ WatcherCommon.saveNewListing(diffLocalListing, currentLocalListingFile);
+ }
+ previousLocalListing = new ArrayList<>(currentLocalListing);
+ } catch (IOException e) {
+ logger.debug("File manipulation error: {}", e.getMessage());
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/binding/binding.xml
new file mode 100755
index 00000000000..71c6a0647c0
--- /dev/null
+++ b/bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,9 @@
+
+
+
+ FolderWatcher Binding
+ This binding will monitor specified location for new files and trigger event channel with new file names.
+
+
diff --git a/bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100755
index 00000000000..1a9dc7d18f1
--- /dev/null
+++ b/bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,126 @@
+
+
+
+
+
+ FTP folder to be watched
+
+
+
+
+
+
+
+
+ Address of FTP server
+ network-address
+
+
+
+ 21
+ FTP server's port
+
+
+
+ true
+
+
+
+
+
+ NONE
+ FTP Security settings
+ true
+
+
+
+ User name
+
+
+
+ FTP server password
+ password
+
+
+
+ Root directory to be watched
+
+
+
+ false
+ Allow listing of hidden files
+ true
+
+
+
+ false
+ Allow listing of sub folders
+ true
+
+
+
+ Connection timeout for FTP request, sec
+ 30
+ true
+
+
+
+ Interval for polling folder changes, sec
+ 60
+ true
+
+
+
+ How many hours back to analyze
+ 24
+ true
+
+
+
+
+
+
+ trigger
+
+ A new file name
+ String
+
+
+
+
+
+ Local folder to be watched
+
+
+
+
+
+
+
+
+ Local directory to be watched
+
+
+
+ Interval for polling folder changes, sec
+ 60
+ true
+
+
+
+ false
+ Allow listing of hidden files
+ true
+
+
+
+ false
+ Allow listing of sub folders
+ true
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index eda37e90ca5..7132c692ac7 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -108,6 +108,7 @@
org.openhab.binding.feed
org.openhab.binding.feican
org.openhab.binding.fmiweather
+ org.openhab.binding.folderwatcher
org.openhab.binding.folding
org.openhab.binding.foobot
org.openhab.binding.freebox