[folderwatcher] Initial contribution (#10045)

Signed-off-by: Alexandr Salamatov <wpgnetworks@gmail.com>
This commit is contained in:
goopilot 2021-02-10 12:45:47 -06:00 committed by GitHub
parent 95259b1095
commit 5a0a325344
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 925 additions and 0 deletions

View File

@ -77,6 +77,7 @@
/bundles/org.openhab.binding.feed/ @svilenvul /bundles/org.openhab.binding.feed/ @svilenvul
/bundles/org.openhab.binding.feican/ @Hilbrand /bundles/org.openhab.binding.feican/ @Hilbrand
/bundles/org.openhab.binding.fmiweather/ @ssalonen /bundles/org.openhab.binding.fmiweather/ @ssalonen
/bundles/org.openhab.binding.folderwatcher/ @goopilot
/bundles/org.openhab.binding.folding/ @fa2k /bundles/org.openhab.binding.folding/ @fa2k
/bundles/org.openhab.binding.foobot/ @airboxlab @Hilbrand /bundles/org.openhab.binding.foobot/ @airboxlab @Hilbrand
/bundles/org.openhab.binding.freebox/ @lolodomo /bundles/org.openhab.binding.freebox/ @lolodomo

View File

@ -371,6 +371,11 @@
<artifactId>org.openhab.binding.fmiweather</artifactId> <artifactId>org.openhab.binding.fmiweather</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.folderwatcher</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.openhab.addons.bundles</groupId> <groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.folding</artifactId> <artifactId>org.openhab.binding.folding</artifactId>

View File

@ -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

View File

@ -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
```

View File

@ -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>

View File

@ -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>

View File

@ -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";
}

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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();
}
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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.");
}
}
}

View File

@ -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());
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -108,6 +108,7 @@
<module>org.openhab.binding.feed</module> <module>org.openhab.binding.feed</module>
<module>org.openhab.binding.feican</module> <module>org.openhab.binding.feican</module>
<module>org.openhab.binding.fmiweather</module> <module>org.openhab.binding.fmiweather</module>
<module>org.openhab.binding.folderwatcher</module>
<module>org.openhab.binding.folding</module> <module>org.openhab.binding.folding</module>
<module>org.openhab.binding.foobot</module> <module>org.openhab.binding.foobot</module>
<module>org.openhab.binding.freebox</module> <module>org.openhab.binding.freebox</module>