/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
<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>
--- /dev/null
+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
--- /dev/null
+# 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
+```
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+/**
+ * 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";
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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
+}
--- /dev/null
+/**
+ * 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();
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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.");
+ }
+ }
+}
--- /dev/null
+/**
+ * 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());
+ }
+ }
+}
--- /dev/null
+<?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>
--- /dev/null
+<?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>
<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>