2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.folderwatcher.internal.handler;
15 import static org.openhab.binding.folderwatcher.internal.FolderWatcherBindingConstants.CHANNEL_NEWFILE;
18 import java.io.IOException;
19 import java.time.Instant;
20 import java.time.temporal.ChronoUnit;
21 import java.util.ArrayList;
22 import java.util.List;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import org.apache.commons.net.ftp.FTPClient;
27 import org.apache.commons.net.ftp.FTPFile;
28 import org.apache.commons.net.ftp.FTPReply;
29 import org.apache.commons.net.ftp.FTPSClient;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.folderwatcher.internal.common.WatcherCommon;
33 import org.openhab.binding.folderwatcher.internal.config.FtpFolderWatcherConfiguration;
34 import org.openhab.core.OpenHAB;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.openhab.core.thing.binding.BaseThingHandler;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
46 * The {@link FtpFolderWatcherHandler} is responsible for handling commands, which are
47 * sent to one of the channels.
49 * @author Alexandr Salamatov - Initial contribution
52 public class FtpFolderWatcherHandler extends BaseThingHandler {
53 private final Logger logger = LoggerFactory.getLogger(FtpFolderWatcherHandler.class);
54 private FtpFolderWatcherConfiguration config = new FtpFolderWatcherConfiguration();
55 private @Nullable File currentFtpListingFile;
56 private @Nullable ScheduledFuture<?> executionJob, initJob;
57 private FTPClient ftp = new FTPClient();
58 private List<String> previousFtpListing = new ArrayList<>();
60 public FtpFolderWatcherHandler(Thing thing) {
65 public void handleCommand(ChannelUID channelUID, Command command) {
66 logger.debug("Channel {} triggered with command {}", channelUID.getId(), command);
67 if (command instanceof RefreshType) {
68 refreshFTPFolderInformation();
73 public void initialize() {
74 File currentFtpListingFile;
75 config = getConfigAs(FtpFolderWatcherConfiguration.class);
76 updateStatus(ThingStatus.UNKNOWN);
77 if (config.connectionTimeout <= 0) {
78 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
79 "Connection timeout can't be negative");
82 if (config.ftpPort < 0) {
83 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "FTP port can't be negative");
86 if (config.pollInterval <= 0) {
87 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
88 "Polling interval can't be null or negative");
91 currentFtpListingFile = new File(OpenHAB.getUserDataFolder() + File.separator + "FolderWatcher" + File.separator
92 + thing.getUID().getAsString().replace(':', '_') + ".data");
94 this.currentFtpListingFile = currentFtpListingFile;
95 previousFtpListing = WatcherCommon.initStorage(currentFtpListingFile, config.ftpAddress + config.ftpDir);
96 } catch (IOException e) {
97 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
98 logger.debug("Can't write file {}, error message {}", currentFtpListingFile, e.getMessage());
101 this.initJob = scheduler.scheduleWithFixedDelay(this::connectionKeepAlive, 0, config.pollInterval,
106 public void dispose() {
107 ScheduledFuture<?> executionJob = this.executionJob;
108 ScheduledFuture<?> initJob = this.initJob;
109 if (executionJob != null) {
110 executionJob.cancel(true);
112 if (initJob != null) {
113 initJob.cancel(true);
115 if (ftp.isConnected()) {
119 } catch (IOException e) {
120 logger.debug("Error terminating FTP connection: ", e);
125 private void listDirectory(FTPClient ftpClient, String dirPath, boolean recursive, List<String> dirFiles)
127 Instant dateNow = Instant.now();
128 for (FTPFile file : ftpClient.listFiles(dirPath)) {
129 String currentFileName = file.getName();
130 if (currentFileName.equals(".") || currentFileName.equals("..")) {
133 String filePath = dirPath + "/" + currentFileName;
134 if (file.isDirectory()) {
137 listDirectory(ftpClient, filePath, recursive, dirFiles);
138 } catch (IOException e) {
139 logger.debug("Can't read FTP directory: {}", filePath, e);
143 long diff = ChronoUnit.HOURS.between(file.getTimestamp().toInstant(), dateNow);
144 if (diff < config.diffHours) {
145 dirFiles.add("ftp:/" + ftpClient.getRemoteAddress() + filePath);
151 private void connectionKeepAlive() {
152 if (!ftp.isConnected()) {
153 switch (config.secureMode) {
155 ftp = new FTPClient();
158 ftp = new FTPSClient(true);
161 ftp = new FTPSClient(false);
166 ftp.setListHiddenFiles(config.listHidden);
167 ftp.setConnectTimeout(config.connectionTimeout * 1000);
170 ftp.connect(config.ftpAddress, config.ftpPort);
171 reply = ftp.getReplyCode();
173 if (!FTPReply.isPositiveCompletion(reply)) {
175 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
176 "FTP server refused connection.");
179 } catch (IOException e) {
180 if (ftp.isConnected()) {
183 } catch (IOException e2) {
184 logger.debug("Error disconneting, lost connection? : {}", e2.getMessage());
187 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
191 if (!ftp.login(config.ftpUsername, config.ftpPassword)) {
192 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ftp.getReplyString());
196 updateStatus(ThingStatus.ONLINE);
197 ScheduledFuture<?> executionJob = this.executionJob;
198 if (executionJob != null) {
199 executionJob.cancel(true);
201 this.executionJob = scheduler.scheduleWithFixedDelay(this::refreshFTPFolderInformation, 0,
202 config.pollInterval, TimeUnit.SECONDS);
203 } catch (IOException e) {
204 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
209 private void refreshFTPFolderInformation() {
210 String ftpRootDir = config.ftpDir;
211 final File currentFtpListingFile = this.currentFtpListingFile;
212 if (ftp.isConnected()) {
213 ftp.enterLocalPassiveMode();
215 if (ftpRootDir.endsWith("/")) {
216 ftpRootDir = ftpRootDir.substring(0, ftpRootDir.length() - 1);
218 if (!ftpRootDir.startsWith("/")) {
219 ftpRootDir = "/" + ftpRootDir;
221 List<String> currentFtpListing = new ArrayList<>();
222 listDirectory(ftp, ftpRootDir, config.listRecursiveFtp, currentFtpListing);
223 List<String> diffFtpListing = new ArrayList<>(currentFtpListing);
224 diffFtpListing.removeAll(previousFtpListing);
225 diffFtpListing.forEach(file -> triggerChannel(CHANNEL_NEWFILE, file));
226 if (!diffFtpListing.isEmpty() && currentFtpListingFile != null) {
228 WatcherCommon.saveNewListing(diffFtpListing, currentFtpListingFile);
229 } catch (IOException e2) {
230 logger.debug("Can't save new listing into file: {}", e2.getMessage());
233 previousFtpListing = new ArrayList<>(currentFtpListing);
234 } catch (IOException e) {
235 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
236 "FTP connection lost. " + e.getMessage());
239 } catch (IOException e1) {
240 logger.debug("Error disconneting, lost connection? {}", e1.getMessage());
244 logger.debug("FTP connection lost.");