From 5a0a325344fbb3977555b891c51f46ff45eb558e Mon Sep 17 00:00:00 2001 From: goopilot <40123561+goopilot@users.noreply.github.com> Date: Wed, 10 Feb 2021 12:45:47 -0600 Subject: [PATCH] [folderwatcher] Initial contribution (#10045) Signed-off-by: Alexandr Salamatov --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../org.openhab.binding.folderwatcher/NOTICE | 13 + .../README.md | 82 ++++++ .../org.openhab.binding.folderwatcher/pom.xml | 25 ++ .../src/main/feature/feature.xml | 9 + .../FolderWatcherBindingConstants.java | 30 +++ .../internal/FolderWatcherHandlerFactory.java | 59 +++++ .../folderwatcher/internal/SecureMode.java | 28 ++ .../internal/common/WatcherCommon.java | 64 +++++ .../config/FtpFolderWatcherConfiguration.java | 36 +++ .../LocalFolderWatcherConfiguration.java | 28 ++ .../handler/FtpFolderWatcherHandler.java | 247 ++++++++++++++++++ .../handler/LocalFolderWatcherHandler.java | 162 ++++++++++++ .../main/resources/OH-INF/binding/binding.xml | 9 + .../resources/OH-INF/thing/thing-types.xml | 126 +++++++++ bundles/pom.xml | 1 + 17 files changed, 925 insertions(+) create mode 100644 bundles/org.openhab.binding.folderwatcher/NOTICE create mode 100755 bundles/org.openhab.binding.folderwatcher/README.md create mode 100644 bundles/org.openhab.binding.folderwatcher/pom.xml create mode 100644 bundles/org.openhab.binding.folderwatcher/src/main/feature/feature.xml create mode 100755 bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherBindingConstants.java create mode 100755 bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/FolderWatcherHandlerFactory.java create mode 100644 bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/SecureMode.java create mode 100755 bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/common/WatcherCommon.java create mode 100755 bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/FtpFolderWatcherConfiguration.java create mode 100755 bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/config/LocalFolderWatcherConfiguration.java create mode 100755 bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/FtpFolderWatcherHandler.java create mode 100755 bundles/org.openhab.binding.folderwatcher/src/main/java/org/openhab/binding/folderwatcher/internal/handler/LocalFolderWatcherHandler.java create mode 100755 bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/binding/binding.xml create mode 100755 bundles/org.openhab.binding.folderwatcher/src/main/resources/OH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index 0eac39f7f..e08a8885e 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 2190e4f6a..37bf38789 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 000000000..38d625e34 --- /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 000000000..03cb040ad --- /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 000000000..856523c87 --- /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 000000000..b19d79626 --- /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 000000000..174c328ae --- /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 000000000..0fa785c14 --- /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 000000000..b65a78828 --- /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 000000000..239e6dafb --- /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 000000000..7a4448ea3 --- /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 000000000..18d31831b --- /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 000000000..c89fced81 --- /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 000000000..14e55b91d --- /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 000000000..71c6a0647 --- /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 000000000..1a9dc7d18 --- /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 eda37e90c..7132c692a 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