[feed] Minor improvements for Feed Binding (#8824)
Signed-off-by: Christoph Weitkamp <github@christophweitkamp.de>pull/8831/head
parent
44e3f9c90f
commit
333cae9e72
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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]
|
||||
```
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -14,9 +14,10 @@ package org.openhab.binding.feed.internal;
|
|||
|
||||
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;
|
||||
|
@ -32,9 +33,10 @@ import org.osgi.service.component.annotations.Component;
|
|||
* @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) {
|
||||
|
@ -42,7 +44,7 @@ public class FeedHandlerFactory extends BaseThingHandlerFactory {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected ThingHandler createHandler(Thing thing) {
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (thingTypeUID.equals(FEED_THING_TYPE_UID)) {
|
||||
|
|
|
@ -29,11 +29,12 @@ import java.util.concurrent.ScheduledFuture;
|
|||
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;
|
||||
|
@ -57,73 +58,80 @@ import com.rometools.rome.io.SyndFeedInput;
|
|||
*
|
||||
* @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 {
|
||||
refreshTime = (BigDecimal) configuration.get(REFRESH_TIME);
|
||||
if (refreshTime.intValue() <= 0) {
|
||||
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 {
|
||||
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;
|
||||
|
@ -135,7 +143,7 @@ public class FeedHandler extends BaseThingHandler {
|
|||
}
|
||||
|
||||
State state = null;
|
||||
SyndEntry latestEntry = getLatestEntry(currentFeedState);
|
||||
SyndEntry latestEntry = getLatestEntry(feedState);
|
||||
|
||||
switch (channelID) {
|
||||
case CHANNEL_LATEST_TITLE:
|
||||
|
@ -166,19 +174,19 @@ public class FeedHandler extends BaseThingHandler {
|
|||
}
|
||||
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:
|
||||
|
@ -200,7 +208,7 @@ public class FeedHandler extends BaseThingHandler {
|
|||
* @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;
|
||||
|
@ -218,16 +226,18 @@ public class FeedHandler extends BaseThingHandler {
|
|||
* {@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;
|
||||
|
@ -238,50 +248,45 @@ public class FeedHandler extends BaseThingHandler {
|
|||
}
|
||||
|
||||
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
|
||||
|
@ -289,7 +294,7 @@ public class FeedHandler extends BaseThingHandler {
|
|||
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);
|
||||
|
@ -317,7 +322,7 @@ public class FeedHandler extends BaseThingHandler {
|
|||
return true;
|
||||
}
|
||||
|
||||
public String getValueSafely(String value) {
|
||||
return value == null ? new String() : value;
|
||||
public String getValueSafely(@Nullable String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
|
||||
<!-- DEFINITIONS of terms used in the binding: Feed is a XML document used for providing users with frequently updated content.
|
||||
The most popular feed formats are RSS and Atom. Entry is a single element in the Feed, that contains reference (link),
|
||||
short description and other information like images, comments and etc. Entry in this binding is abstraction for RSS item
|
||||
element and Atom entry element. -->
|
||||
short
|
||||
description and other information like images, comments and etc. Entry in this binding is abstraction for RSS item element
|
||||
and Atom entry element. -->
|
||||
|
||||
<!-- Feed Thing Type -->
|
||||
<thing-type id="feed">
|
||||
|
@ -28,18 +29,16 @@
|
|||
<config-description>
|
||||
<parameter name="URL" type="text" required="true">
|
||||
<label>Feed URL</label>
|
||||
<description>The URL of the feed</description>
|
||||
<description>The URL of the feed.</description>
|
||||
</parameter>
|
||||
|
||||
<!--After the refresh time interval expires, the bindings checks for updates in the Feed, and if the information is not
|
||||
<!-- After the refresh time interval expires, the bindings checks for updates in the Feed, and if the information is not
|
||||
up to date, updates the feed content stored in the channel -->
|
||||
|
||||
<parameter name="refresh" type="integer">
|
||||
<label>Refresh Time Interval</label>
|
||||
<description>Refresh time interval in minutes.</description>
|
||||
<default>20</default>
|
||||
</parameter>
|
||||
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
|
@ -67,35 +66,35 @@
|
|||
<channel-type id="author" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Author</label>
|
||||
<description>The name of the feed author, if author is present</description>
|
||||
<description>The name of the feed author, if author is present.</description>
|
||||
<state readOnly="true" pattern="%s"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="title" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Title</label>
|
||||
<description>The title of the feed</description>
|
||||
<description>The title of the feed.</description>
|
||||
<state readOnly="true" pattern="%s"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="description" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Description</label>
|
||||
<description>Description of the feed</description>
|
||||
<description>Description of the feed.</description>
|
||||
<state readOnly="true" pattern="%s"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="last-update" advanced="true">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Last Update</label>
|
||||
<description>The last update date of the feed</description>
|
||||
<description>The last update date of the feed.</description>
|
||||
<state readOnly="true" pattern="%tc %n"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="number-of-entries" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Number of Entries</label>
|
||||
<description>Number of entries in the feed</description>
|
||||
<description>Number of entries in the feed.</description>
|
||||
<state readOnly="true" pattern="%d"/>
|
||||
</channel-type>
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ public class FeedHandlerTest extends JavaOSGiTest {
|
|||
/**
|
||||
* It is updated from mocked {@link StateChangeListener#stateUpdated() }
|
||||
*/
|
||||
private StringType currentItemState = null;
|
||||
private StringType currentItemState;
|
||||
|
||||
// Required services for the test
|
||||
private ManagedThingProvider managedThingProvider;
|
||||
|
|
|
@ -12,11 +12,13 @@
|
|||
*/
|
||||
package org.openhab.binding.feed.test;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* This interface is used to mark tests that take too much time
|
||||
*
|
||||
* @author Svilen Valkanov
|
||||
* @author Svilen Valkanov - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface SlowTests {
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue