Skip to content

Commit

Permalink
[feed] Minor improvements for Feed Binding (openhab#8824)
Browse files Browse the repository at this point in the history
Signed-off-by: Christoph Weitkamp <[email protected]>
  • Loading branch information
cweitkamp authored Oct 21, 2020
1 parent 44e3f9c commit 333cae9
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 86 deletions.
20 changes: 9 additions & 11 deletions bundles/org.openhab.binding.feed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ This binding allows you to integrate feeds in the openHAB environment.
The Feed binding downloads the content, tracks for changes, and displays information like feed author, feed title and description, number of entries, last update date.

It can be used in combination with openHAB rules to trigger events on feed change.
It uses the [ROME library](https://rometools.github.io/rome/index.html) for parsing
and supports a wide range of popular feed formats - RSS 2.00, RSS 1.00, RSS 0.94, RSS 0.93, RSS 0.92, RSS 0.91 UserLand,
RSS 0.91 Netscape, RSS 0.90, Atom 1.0, Atom 0.3.
It uses the [ROME library](https://rometools.github.io/rome/index.html) for parsing and supports a wide range of popular feed formats - RSS 2.00, RSS 1.00, RSS 0.94, RSS 0.93, RSS 0.92, RSS 0.91 UserLand, RSS 0.91 Netscape, RSS 0.90, Atom 1.0, Atom 0.3.

## Supported Things

Expand All @@ -24,11 +22,11 @@ No binding configuration required.

Required configuration:

- **URL** - the URL of the feed (e.g <http://example.com/path/file>). The binding uses this URL to download data
- **URL** - the URL of the feed (e.g <http://example.com/path/file>). The binding uses this URL to download data.

Optional configuration:

- **refresh** - a refresh interval defines after how many minutes the binding will check, if new content is available. Default value is 20 minutes
- **refresh** - a refresh interval defines after how many minutes the binding will check, if new content is available. Default value is 20 minutes.

## Channels

Expand All @@ -39,18 +37,18 @@ The binding supports following channels
| latest-title | String | Contains the title of the last feed entry. |
| latest-description | String | Contains the description of last feed entry. |
| latest-date | DateTime | Contains the published date of the last feed entry. |
| author | String | The name of the feed author, if author is present |
| title | String | The title of the feed |
| description | String | Description of the feed |
| last-update | DateTime | The last update date of the feed |
| number-of-entries | Number | Number of entries in the feed |
| author | String | The name of the feed author, if author is present. |
| title | String | The title of the feed. |
| description | String | Description of the feed. |
| last-update | DateTime | The last update date of the feed. |
| number-of-entries | Number | Number of entries in the feed. |

## Example

Things:

```java
feed:feed:bbc [ URL="http://feeds.bbci.co.uk/news/video_and_audio/news_front_page/rss.xml?edition=uk"]
feed:feed:bbc [ URL="http://feeds.bbci.co.uk/news/video_and_audio/news_front_page/rss.xml?edition=uk"]
feed:feed:techCrunch [ URL="http://feeds.feedburner.com/TechCrunch/", refresh=60]
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
*/
package org.openhab.binding.feed.internal;

import java.math.BigDecimal;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;

Expand Down Expand Up @@ -86,11 +84,11 @@ public class FeedBindingConstants {
/**
* The default auto refresh time in minutes.
*/
public static final BigDecimal DEFAULT_REFRESH_TIME = new BigDecimal(20);
public static final long DEFAULT_REFRESH_TIME = 20;

/**
* The minimum refresh time in milliseconds. Any REFRESH command send to a Thing, before this time has expired, will
* not trigger an attempt to dowload new data form the server.
* not trigger an attempt to download new data from the server.
**/
public static final int MINIMUM_REFRESH_TIME = 3000;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@

import static org.openhab.binding.feed.internal.FeedBindingConstants.FEED_THING_TYPE_UID;

import java.util.Collections;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.feed.internal.handler.FeedHandler;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
Expand All @@ -32,17 +33,18 @@
* @author Svilen Valkanov - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.feed")
@NonNullByDefault
public class FeedHandlerFactory extends BaseThingHandlerFactory {

private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(FEED_THING_TYPE_UID);
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(FEED_THING_TYPE_UID);

@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}

@Override
protected ThingHandler createHandler(Thing thing) {
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();

if (thingTypeUID.equals(FEED_THING_TYPE_UID)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPInputStream;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
Expand All @@ -57,73 +58,80 @@
*
* @author Svilen Valkanov - Initial contribution
*/
@NonNullByDefault
public class FeedHandler extends BaseThingHandler {

private Logger logger = LoggerFactory.getLogger(FeedHandler.class);
private final Logger logger = LoggerFactory.getLogger(FeedHandler.class);

private String urlString;
private BigDecimal refreshTime;
private ScheduledFuture<?> refreshTask;
private SyndFeed currentFeedState;
private @Nullable URL url;
private long refreshTime;
private @Nullable ScheduledFuture<?> refreshTask;
private @Nullable SyndFeed currentFeedState;
private long lastRefreshTime;

public FeedHandler(Thing thing) {
super(thing);
currentFeedState = null;
}

@Override
public void initialize() {
checkConfiguration();
updateStatus(ThingStatus.UNKNOWN);
startAutomaticRefresh();
if (checkConfiguration()) {
updateStatus(ThingStatus.UNKNOWN);
startAutomaticRefresh();
}
}

/**
* This method checks if the provided configuration is valid.
* When invalid parameter is found, default value is assigned.
*/
private void checkConfiguration() {
private boolean checkConfiguration() {
logger.debug("Start reading Feed Thing configuration.");
Configuration configuration = getConfig();

// It is not necessary to check if the URL is valid, this will be done in fetchFeedData() method
urlString = (String) configuration.get(URL);
String urlString = (String) configuration.get(URL);
try {
url = new URL(urlString);
} catch (MalformedURLException e) {
logger.warn("Url '{}' is not valid: ", urlString, e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
return false;
}

BigDecimal localRefreshTime = null;
try {
refreshTime = (BigDecimal) configuration.get(REFRESH_TIME);
if (refreshTime.intValue() <= 0) {
localRefreshTime = (BigDecimal) configuration.get(REFRESH_TIME);
if (localRefreshTime.intValue() <= 0) {
throw new IllegalArgumentException("Refresh time must be positive number!");
}
refreshTime = localRefreshTime.longValue();
} catch (Exception e) {
logger.warn("Refresh time [{}] is not valid. Falling back to default value: {}. {}", refreshTime,
logger.warn("Refresh time [{}] is not valid. Falling back to default value: {}. {}", localRefreshTime,
DEFAULT_REFRESH_TIME, e.getMessage());
refreshTime = DEFAULT_REFRESH_TIME;
}
return true;
}

private void startAutomaticRefresh() {
refreshTask = scheduler.scheduleWithFixedDelay(this::refreshFeedState, 0, refreshTime.intValue(),
TimeUnit.MINUTES);
logger.debug("Start automatic refresh at {} minutes", refreshTime.intValue());
refreshTask = scheduler.scheduleWithFixedDelay(this::refreshFeedState, 0, refreshTime, TimeUnit.MINUTES);
logger.debug("Start automatic refresh at {} minutes!", refreshTime);
}

private void refreshFeedState() {
SyndFeed feed = fetchFeedData(urlString);
SyndFeed feed = fetchFeedData();
boolean feedUpdated = updateFeedIfChanged(feed);

if (feedUpdated) {
List<Channel> channels = getThing().getChannels();
for (Channel channel : channels) {
publishChannelIfLinked(channel.getUID());
}
getThing().getChannels().forEach(channel -> publishChannelIfLinked(channel.getUID()));
}
}

private void publishChannelIfLinked(ChannelUID channelUID) {
String channelID = channelUID.getId();

if (currentFeedState == null) {
SyndFeed feedState = currentFeedState;
if (feedState == null) {
// This will happen if the binding could not download data from the server
logger.trace("Cannot update channel with ID {}; no data has been downloaded from the server!", channelID);
return;
Expand All @@ -135,7 +143,7 @@ private void publishChannelIfLinked(ChannelUID channelUID) {
}

State state = null;
SyndEntry latestEntry = getLatestEntry(currentFeedState);
SyndEntry latestEntry = getLatestEntry(feedState);

switch (channelID) {
case CHANNEL_LATEST_TITLE:
Expand Down Expand Up @@ -166,19 +174,19 @@ private void publishChannelIfLinked(ChannelUID channelUID) {
}
break;
case CHANNEL_AUTHOR:
String author = currentFeedState.getAuthor();
String author = feedState.getAuthor();
state = new StringType(getValueSafely(author));
break;
case CHANNEL_DESCRIPTION:
String channelDescription = currentFeedState.getDescription();
String channelDescription = feedState.getDescription();
state = new StringType(getValueSafely(channelDescription));
break;
case CHANNEL_TITLE:
String channelTitle = currentFeedState.getTitle();
String channelTitle = feedState.getTitle();
state = new StringType(getValueSafely(channelTitle));
break;
case CHANNEL_NUMBER_OF_ENTRIES:
int numberOfEntries = currentFeedState.getEntries().size();
int numberOfEntries = feedState.getEntries().size();
state = new DecimalType(numberOfEntries);
break;
default:
Expand All @@ -200,7 +208,7 @@ private void publishChannelIfLinked(ChannelUID channelUID) {
* @return <code>true</code> if new content is available on the server since the last update or <code>false</code>
* otherwise
*/
private synchronized boolean updateFeedIfChanged(SyndFeed newFeedState) {
private synchronized boolean updateFeedIfChanged(@Nullable SyndFeed newFeedState) {
// SyndFeed class has implementation of equals ()
if (newFeedState != null && !newFeedState.equals(currentFeedState)) {
currentFeedState = newFeedState;
Expand All @@ -218,16 +226,18 @@ private synchronized boolean updateFeedIfChanged(SyndFeed newFeedState) {
* {@link ThingStatusDetail#CONFIGURATION_ERROR} or
* {@link ThingStatusDetail#COMMUNICATION_ERROR} and adequate message.
*
* @param urlString URL of the Feed
* @return {@link SyndFeed} instance with the feed data, if the connection attempt was successful and
* <code>null</code> otherwise
*/
private SyndFeed fetchFeedData(String urlString) {
SyndFeed feed = null;
try {
URL url = new URL(urlString);
private @Nullable SyndFeed fetchFeedData() {
URL localUrl = url;
if (localUrl == null) {
logger.trace("Url '{}' is not valid: ", localUrl);
return null;
}

URLConnection connection = url.openConnection();
try {
URLConnection connection = localUrl.openConnection();
connection.setRequestProperty("Accept-Encoding", "gzip");

BufferedReader in = null;
Expand All @@ -238,58 +248,53 @@ private SyndFeed fetchFeedData(String urlString) {
}

SyndFeedInput input = new SyndFeedInput();
feed = input.build(in);
SyndFeed feed = input.build(in);
in.close();

if (this.thing.getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
} catch (MalformedURLException e) {
logger.warn("Url '{}' is not valid: ", urlString, e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
return null;

return feed;
} catch (IOException e) {
logger.warn("Error accessing feed: {}", urlString, e);
logger.warn("Error accessing feed: {}", localUrl, e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
return null;
} catch (IllegalArgumentException e) {
logger.warn("Feed URL is null ", e);
logger.warn("Feed URL is null: {} ", localUrl, e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
return null;
} catch (FeedException e) {
logger.warn("Feed content is not valid: {} ", urlString, e);
logger.warn("Feed content is not valid: {} ", localUrl, e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
return null;
}

return feed;
}

/**
* Returns the most recent entry or null, if no entries are found.
*/
private SyndEntry getLatestEntry(SyndFeed feed) {
private @Nullable SyndEntry getLatestEntry(SyndFeed feed) {
List<SyndEntry> allEntries = feed.getEntries();
SyndEntry lastEntry = null;
if (!allEntries.isEmpty()) {
/*
* The entries are stored in the SyndFeed object in the following order -
* the newest entry has index 0. The order is determined from the time the entry was posted, not the
* published time of the entry.
*/
lastEntry = allEntries.get(0);
return allEntries.get(0);
} else {
logger.debug("No entries found");
}
return lastEntry;
return null;
}

@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
// safeguard for multiple REFRESH commands for different channels in a row
if (isMinimumRefreshTimeExceeded()) {
SyndFeed feed = fetchFeedData(urlString);
SyndFeed feed = fetchFeedData();
updateFeedIfChanged(feed);
}
publishChannelIfLinked(channelUID);
Expand Down Expand Up @@ -317,7 +322,7 @@ private boolean isMinimumRefreshTimeExceeded() {
return true;
}

public String getValueSafely(String value) {
return value == null ? new String() : value;
public String getValueSafely(@Nullable String value) {
return value == null ? "" : value;
}
}
Loading

0 comments on commit 333cae9

Please sign in to comment.