]> git.basschouten.com Git - openhab-addons.git/blob
9d828965157855c7566f51b1a5f047bb4c344bd5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.feed.internal.handler;
14
15 import static org.openhab.binding.feed.internal.FeedBindingConstants.*;
16
17 import java.io.BufferedReader;
18 import java.io.IOException;
19 import java.io.InputStreamReader;
20 import java.math.BigDecimal;
21 import java.net.MalformedURLException;
22 import java.net.URL;
23 import java.net.URLConnection;
24 import java.time.ZoneId;
25 import java.time.ZonedDateTime;
26 import java.util.Date;
27 import java.util.List;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30 import java.util.zip.GZIPInputStream;
31
32 import org.openhab.core.config.core.Configuration;
33 import org.openhab.core.library.types.DateTimeType;
34 import org.openhab.core.library.types.DecimalType;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.thing.Channel;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.thing.binding.BaseThingHandler;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.RefreshType;
44 import org.openhab.core.types.State;
45 import org.openhab.core.types.UnDefType;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 import com.rometools.rome.feed.synd.SyndEntry;
50 import com.rometools.rome.feed.synd.SyndFeed;
51 import com.rometools.rome.io.FeedException;
52 import com.rometools.rome.io.SyndFeedInput;
53
54 /**
55  * The {@link FeedHandler } is responsible for handling commands, which are
56  * sent to one of the channels and for the regular updates of the feed data.
57  *
58  * @author Svilen Valkanov - Initial contribution
59  */
60 public class FeedHandler extends BaseThingHandler {
61
62     private Logger logger = LoggerFactory.getLogger(FeedHandler.class);
63
64     private String urlString;
65     private BigDecimal refreshTime;
66     private ScheduledFuture<?> refreshTask;
67     private SyndFeed currentFeedState;
68     private long lastRefreshTime;
69
70     public FeedHandler(Thing thing) {
71         super(thing);
72         currentFeedState = null;
73     }
74
75     @Override
76     public void initialize() {
77         checkConfiguration();
78         updateStatus(ThingStatus.UNKNOWN);
79         startAutomaticRefresh();
80     }
81
82     /**
83      * This method checks if the provided configuration is valid.
84      * When invalid parameter is found, default value is assigned.
85      */
86     private void checkConfiguration() {
87         logger.debug("Start reading Feed Thing configuration.");
88         Configuration configuration = getConfig();
89
90         // It is not necessary to check if the URL is valid, this will be done in fetchFeedData() method
91         urlString = (String) configuration.get(URL);
92
93         try {
94             refreshTime = (BigDecimal) configuration.get(REFRESH_TIME);
95             if (refreshTime.intValue() <= 0) {
96                 throw new IllegalArgumentException("Refresh time must be positive number!");
97             }
98         } catch (Exception e) {
99             logger.warn("Refresh time [{}] is not valid. Falling back to default value: {}. {}", refreshTime,
100                     DEFAULT_REFRESH_TIME, e.getMessage());
101             refreshTime = DEFAULT_REFRESH_TIME;
102         }
103     }
104
105     private void startAutomaticRefresh() {
106         refreshTask = scheduler.scheduleWithFixedDelay(this::refreshFeedState, 0, refreshTime.intValue(),
107                 TimeUnit.MINUTES);
108         logger.debug("Start automatic refresh at {} minutes", refreshTime.intValue());
109     }
110
111     private void refreshFeedState() {
112         SyndFeed feed = fetchFeedData(urlString);
113         boolean feedUpdated = updateFeedIfChanged(feed);
114
115         if (feedUpdated) {
116             List<Channel> channels = getThing().getChannels();
117             for (Channel channel : channels) {
118                 publishChannelIfLinked(channel.getUID());
119             }
120         }
121     }
122
123     private void publishChannelIfLinked(ChannelUID channelUID) {
124         String channelID = channelUID.getId();
125
126         if (currentFeedState == null) {
127             // This will happen if the binding could not download data from the server
128             logger.trace("Cannot update channel with ID {}; no data has been downloaded from the server!", channelID);
129             return;
130         }
131
132         if (!isLinked(channelUID)) {
133             logger.trace("Cannot update channel with ID {}; not linked!", channelID);
134             return;
135         }
136
137         State state = null;
138         SyndEntry latestEntry = getLatestEntry(currentFeedState);
139
140         switch (channelID) {
141             case CHANNEL_LATEST_TITLE:
142                 if (latestEntry == null || latestEntry.getTitle() == null) {
143                     state = UnDefType.UNDEF;
144                 } else {
145                     String title = latestEntry.getTitle();
146                     state = new StringType(getValueSafely(title));
147                 }
148                 break;
149             case CHANNEL_LATEST_DESCRIPTION:
150                 if (latestEntry == null || latestEntry.getDescription() == null) {
151                     state = UnDefType.UNDEF;
152                 } else {
153                     String description = latestEntry.getDescription().getValue();
154                     state = new StringType(getValueSafely(description));
155                 }
156                 break;
157             case CHANNEL_LATEST_PUBLISHED_DATE:
158             case CHANNEL_LAST_UPDATE:
159                 if (latestEntry == null || latestEntry.getPublishedDate() == null) {
160                     logger.debug("Cannot update date channel. No date found in feed.");
161                     return;
162                 } else {
163                     Date date = latestEntry.getPublishedDate();
164                     ZonedDateTime zdt = ZonedDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
165                     state = new DateTimeType(zdt);
166                 }
167                 break;
168             case CHANNEL_AUTHOR:
169                 String author = currentFeedState.getAuthor();
170                 state = new StringType(getValueSafely(author));
171                 break;
172             case CHANNEL_DESCRIPTION:
173                 String channelDescription = currentFeedState.getDescription();
174                 state = new StringType(getValueSafely(channelDescription));
175                 break;
176             case CHANNEL_TITLE:
177                 String channelTitle = currentFeedState.getTitle();
178                 state = new StringType(getValueSafely(channelTitle));
179                 break;
180             case CHANNEL_NUMBER_OF_ENTRIES:
181                 int numberOfEntries = currentFeedState.getEntries().size();
182                 state = new DecimalType(numberOfEntries);
183                 break;
184             default:
185                 logger.debug("Unrecognized channel: {}", channelID);
186         }
187
188         if (state != null) {
189             updateState(channelID, state);
190         } else {
191             logger.debug("Cannot update channel with ID {}; state not defined!", channelID);
192         }
193     }
194
195     /**
196      * This method updates the {@link #currentFeedState}, only if there are changes on the server, since the last check.
197      * It compares the content on the server with the local
198      * stored {@link #currentFeedState} in the {@link FeedHandler}.
199      *
200      * @return <code>true</code> if new content is available on the server since the last update or <code>false</code>
201      *         otherwise
202      */
203     private synchronized boolean updateFeedIfChanged(SyndFeed newFeedState) {
204         // SyndFeed class has implementation of equals ()
205         if (newFeedState != null && !newFeedState.equals(currentFeedState)) {
206             currentFeedState = newFeedState;
207             logger.debug("New content available!");
208             return true;
209         }
210         logger.debug("Feed content has not changed!");
211         return false;
212     }
213
214     /**
215      * This method tries to make connection with the server and fetch data from the feed.
216      * The status of the feed thing is set to {@link ThingStatus#ONLINE}, if the fetching was successful.
217      * Otherwise the status will be set to {@link ThingStatus#OFFLINE} with
218      * {@link ThingStatusDetail#CONFIGURATION_ERROR} or
219      * {@link ThingStatusDetail#COMMUNICATION_ERROR} and adequate message.
220      *
221      * @param urlString URL of the Feed
222      * @return {@link SyndFeed} instance with the feed data, if the connection attempt was successful and
223      *         <code>null</code> otherwise
224      */
225     private SyndFeed fetchFeedData(String urlString) {
226         SyndFeed feed = null;
227         try {
228             URL url = new URL(urlString);
229
230             URLConnection connection = url.openConnection();
231             connection.setRequestProperty("Accept-Encoding", "gzip");
232
233             BufferedReader in = null;
234             if ("gzip".equals(connection.getContentEncoding())) {
235                 in = new BufferedReader(new InputStreamReader(new GZIPInputStream(connection.getInputStream())));
236             } else {
237                 in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
238             }
239
240             SyndFeedInput input = new SyndFeedInput();
241             feed = input.build(in);
242             in.close();
243
244             if (this.thing.getStatus() != ThingStatus.ONLINE) {
245                 updateStatus(ThingStatus.ONLINE);
246             }
247         } catch (MalformedURLException e) {
248             logger.warn("Url '{}' is not valid: ", urlString, e);
249             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
250             return null;
251         } catch (IOException e) {
252             logger.warn("Error accessing feed: {}", urlString, e);
253             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
254             return null;
255         } catch (IllegalArgumentException e) {
256             logger.warn("Feed URL is null ", e);
257             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
258             return null;
259         } catch (FeedException e) {
260             logger.warn("Feed content is not valid: {} ", urlString, e);
261             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
262             return null;
263         }
264
265         return feed;
266     }
267
268     /**
269      * Returns the most recent entry or null, if no entries are found.
270      */
271     private SyndEntry getLatestEntry(SyndFeed feed) {
272         List<SyndEntry> allEntries = feed.getEntries();
273         SyndEntry lastEntry = null;
274         if (!allEntries.isEmpty()) {
275             /*
276              * The entries are stored in the SyndFeed object in the following order -
277              * the newest entry has index 0. The order is determined from the time the entry was posted, not the
278              * published time of the entry.
279              */
280             lastEntry = allEntries.get(0);
281         } else {
282             logger.debug("No entries found");
283         }
284         return lastEntry;
285     }
286
287     @Override
288     public void handleCommand(ChannelUID channelUID, Command command) {
289         if (command instanceof RefreshType) {
290             // safeguard for multiple REFRESH commands for different channels in a row
291             if (isMinimumRefreshTimeExceeded()) {
292                 SyndFeed feed = fetchFeedData(urlString);
293                 updateFeedIfChanged(feed);
294             }
295             publishChannelIfLinked(channelUID);
296         } else {
297             logger.debug("Command {} is not supported for channel: {}. Supported command: REFRESH", command,
298                     channelUID.getId());
299         }
300     }
301
302     @Override
303     public void dispose() {
304         if (refreshTask != null) {
305             refreshTask.cancel(true);
306         }
307         lastRefreshTime = 0;
308     }
309
310     private boolean isMinimumRefreshTimeExceeded() {
311         long currentTime = System.currentTimeMillis();
312         long timeSinceLastRefresh = currentTime - lastRefreshTime;
313         if (timeSinceLastRefresh < MINIMUM_REFRESH_TIME) {
314             return false;
315         }
316         lastRefreshTime = currentTime;
317         return true;
318     }
319
320     public String getValueSafely(String value) {
321         return value == null ? new String() : value;
322     }
323 }