]> git.basschouten.com Git - openhab-addons.git/commitdiff
[folderwatcher] Initial contribution (#10045)
authorgoopilot <40123561+goopilot@users.noreply.github.com>
Wed, 10 Feb 2021 18:45:47 +0000 (12:45 -0600)
committerGitHub <noreply@github.com>
Wed, 10 Feb 2021 18:45:47 +0000 (19:45 +0100)
Signed-off-by: Alexandr Salamatov <wpgnetworks@gmail.com>
17 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.folderwatcher/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.folderwatcher/README.md [new file with mode: 0755]
bundles/org.openhab.binding.folderwatcher/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.folderwatcher/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherBindingConstants.java [new file with mode: 0755]
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherHandlerFactory.java [new file with mode: 0755]
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/SecureMode.java [new file with mode: 0644]
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/common/WatcherCommon.java [new file with mode: 0755]
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/FtpFolderWatcherConfiguration.java [new file with mode: 0755]
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/LocalFolderWatcherConfiguration.java [new file with mode: 0755]
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/FtpFolderWatcherHandler.java [new file with mode: 0755]
bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/LocalFolderWatcherHandler.java [new file with mode: 0755]
bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0755]
bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0755]
bundles/pom.xml

index 0eac39f7f3850233a042296920aa4c4c4da186af..e08a8885edf26e920beafbc07658f3e385fd82d3 100644 (file)
@@ -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
index 2190e4f6ad7c8087f68d85eca79d2e53c7eac8fa..37bf38789973805a4721a3758f6b9b23ccfe1dbc 100644 (file)
       <artifactId>org.openhab.binding.fmiweather</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.folderwatcher</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.folding</artifactId>
diff --git a/bundles/org.openhab.binding.folderwatcher/NOTICE b/bundles/org.openhab.binding.folderwatcher/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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 (executable)
index 0000000..03cb040
--- /dev/null
@@ -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 (file)
index 0000000..856523c
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  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.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.folderwatcher</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: FolderWatcher Binding</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>commons-net</groupId>
+      <artifactId>commons-net</artifactId>
+      <version>3.7.2</version>
+    </dependency>
+  </dependencies>
+
+</project>
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 (file)
index 0000000..b19d796
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.folderwatcher-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-folderwatcher" description="FolderWatcher Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.folderwatcher/${project.version}</bundle>
+       </feature>
+</features>
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 (executable)
index 0000000..174c328
--- /dev/null
@@ -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 (executable)
index 0000000..0fa785c
--- /dev/null
@@ -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<ThingTypeUID> 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 (file)
index 0000000..b65a788
--- /dev/null
@@ -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 (executable)
index 0000000..239e6da
--- /dev/null
@@ -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<String> initStorage(File file, String watchDir) throws IOException {
+        List<String> returnList = List.of();
+        List<String> 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<String> 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 (executable)
index 0000000..7a4448e
--- /dev/null
@@ -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 (executable)
index 0000000..18d3183
--- /dev/null
@@ -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 (executable)
index 0000000..c89fced
--- /dev/null
@@ -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<String> 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<String> 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<String> currentFtpListing = new ArrayList<>();
+                listDirectory(ftp, ftpRootDir, config.listRecursiveFtp, currentFtpListing);
+                List<String> 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 (executable)
index 0000000..14e55b9
--- /dev/null
@@ -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<String> 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<String> 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<String> 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 (executable)
index 0000000..71c6a06
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="folderwatcher" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>FolderWatcher Binding</name>
+       <description>This binding will monitor specified location for new files and trigger event channel with new file names.</description>
+
+</binding:binding>
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 (executable)
index 0000000..1a9dc7d
--- /dev/null
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="folderwatcher"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="ftpfolder">
+               <label>FTP Folder</label>
+               <description>FTP folder to be watched</description>
+
+               <channels>
+                       <channel id="newfile" typeId="newfile-channel"/>
+               </channels>
+
+               <config-description>
+                       <parameter name="ftpAddress" type="text" required="true">
+                               <label>FTP Server</label>
+                               <description>Address of FTP server</description>
+                               <context>network-address</context>
+                       </parameter>
+                       <parameter name="ftpPort" type="integer" min="1" max="65535">
+                               <label>FTP Port</label>
+                               <default>21</default>
+                               <description>FTP server's port</description>
+                       </parameter>
+                       <parameter name="secureMode" type="text">
+                               <label>FTP Security</label>
+                               <limitToOptions>true</limitToOptions>
+                               <options>
+                                       <option value="NONE">None</option>
+                                       <option value="IMPLICIT">TLS/SSL Implicit</option>
+                                       <option value="EXPLICIT">TLS/SSL Explicit</option>
+                               </options>
+                               <default>NONE</default>
+                               <description>FTP Security settings</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="ftpUsername" type="text" required="true">
+                               <label>Username</label>
+                               <description>User name</description>
+                       </parameter>
+                       <parameter name="ftpPassword" type="text" required="true">
+                               <label>Password</label>
+                               <description>FTP server password</description>
+                               <context>password</context>
+                       </parameter>
+                       <parameter name="ftpDir" type="text" required="true">
+                               <label>Root Directory</label>
+                               <description>Root directory to be watched</description>
+                       </parameter>
+                       <parameter name="listHidden" type="boolean">
+                               <label>List Hidden</label>
+                               <default>false</default>
+                               <description>Allow listing of hidden files</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="listRecursiveFtp" type="boolean">
+                               <label>List Sub Folders</label>
+                               <default>false</default>
+                               <description>Allow listing of sub folders</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="connectionTimeout" type="integer" min="1" unit="s">
+                               <label>Connection Timeout</label>
+                               <description>Connection timeout for FTP request, sec</description>
+                               <default>30</default>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="pollInterval" type="integer" min="1" unit="s">
+                               <label>Polling Interval</label>
+                               <description>Interval for polling folder changes, sec</description>
+                               <default>60</default>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="diffHours" type="integer" min="1" unit="h">
+                               <label>Timestamp Difference</label>
+                               <description>How many hours back to analyze</description>
+                               <default>24</default>
+                               <advanced>true</advanced>
+                       </parameter>
+               </config-description>
+
+       </thing-type>
+
+       <channel-type id="newfile-channel">
+               <kind>trigger</kind>
+               <label>New File Name(s)</label>
+               <description>A new file name</description>
+               <category>String</category>
+               <event/>
+       </channel-type>
+
+       <thing-type id="localfolder">
+               <label>Local Folder</label>
+               <description>Local folder to be watched</description>
+
+               <channels>
+                       <channel id="newfile" typeId="newfile-channel"/>
+               </channels>
+
+               <config-description>
+                       <parameter name="localDir" type="text" required="true">
+                               <label>Local Directory</label>
+                               <description>Local directory to be watched</description>
+                       </parameter>
+                       <parameter name="pollIntervalLocal" type="integer" min="1" unit="s">
+                               <label>Polling Interval</label>
+                               <description>Interval for polling folder changes, sec</description>
+                               <default>60</default>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="listHiddenLocal" type="boolean">
+                               <label>List Hidden</label>
+                               <default>false</default>
+                               <description>Allow listing of hidden files</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="listRecursiveLocal" type="boolean">
+                               <label>List Sub Folders</label>
+                               <default>false</default>
+                               <description>Allow listing of sub folders</description>
+                               <advanced>true</advanced>
+                       </parameter>
+               </config-description>
+       </thing-type>
+</thing:thing-descriptions>
index eda37e90ca5cbf207e979aacede1bb7400233540..7132c692ac7e002b6876c1110d2e55440d76c6e6 100644 (file)
     <module>org.openhab.binding.feed</module>
     <module>org.openhab.binding.feican</module>
     <module>org.openhab.binding.fmiweather</module>
+    <module>org.openhab.binding.folderwatcher</module>
     <module>org.openhab.binding.folding</module>
     <module>org.openhab.binding.foobot</module>
     <module>org.openhab.binding.freebox</module>