[icalendar] Add EventFilter for existing calendars (#8583)

This commit fixes #8022.

Signed-off-by: Michael Wodniok <michi@noorganization.org>
This commit is contained in:
Michael Wodniok
2020-10-24 22:35:07 +02:00
committed by GitHub
parent fa9e3db34b
commit 7312890d44
16 changed files with 1058 additions and 78 deletions

View File

@@ -14,6 +14,8 @@ package org.openhab.binding.icalendar.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.type.ChannelGroupTypeUID;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* The {@link ICalendarBindingConstants} class defines common constants, which are
@@ -28,6 +30,7 @@ public class ICalendarBindingConstants {
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_CALENDAR = new ThingTypeUID(BINDING_ID, "calendar");
public static final ThingTypeUID THING_TYPE_FILTERED_EVENTS = new ThingTypeUID(BINDING_ID, "eventfilter");
// List of all Channel ids
public static final String CHANNEL_CURRENT_EVENT_TITLE = "current_title";
@@ -40,4 +43,19 @@ public class ICalendarBindingConstants {
// additional constants
public static final int HTTP_TIMEOUT_SECS = 60;
public static final String DATETIME_UNIT_MINUTE = "minute";
public static final String DATETIME_UNIT_HOUR = "hour";
public static final String DATETIME_UNIT_DAY = "day";
public static final String DATETIME_UNIT_WEEK = "week";
// specials for EventFilter
public static final int DEFAULT_FILTER_REFRESH = 15;
public static final String RESULT_GROUP_ID_PREFIX = "result_";
public static final String RESULT_BEGIN_ID = "begin";
public static final String RESULT_END_ID = "end";
public static final String RESULT_TITLE_ID = "title";
public static final ChannelGroupTypeUID GROUP_TYPE_UID = new ChannelGroupTypeUID(BINDING_ID, "result");
public static final ChannelTypeUID BEGIN_TYPE_UID = new ChannelTypeUID(BINDING_ID, "result_start");
public static final ChannelTypeUID END_TYPE_UID = new ChannelTypeUID(BINDING_ID, "result_end");
public static final ChannelTypeUID TITLE_TYPE_UID = new ChannelTypeUID(BINDING_ID, "result_title");
}

View File

@@ -12,17 +12,22 @@
*/
package org.openhab.binding.icalendar.internal;
import static org.openhab.binding.icalendar.internal.ICalendarBindingConstants.THING_TYPE_CALENDAR;
import static org.openhab.binding.icalendar.internal.ICalendarBindingConstants.*;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.icalendar.internal.handler.EventFilterHandler;
import org.openhab.binding.icalendar.internal.handler.ICalendarHandler;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
@@ -31,6 +36,8 @@ import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ICalendarHandlerFactory} is responsible for creating things and thing
@@ -38,21 +45,27 @@ import org.osgi.service.component.annotations.Reference;
*
* @author Michael Wodniok - Initial contribution
* @author Andrew Fiddian-Green - EventPublisher code
* @author Michael Wodniok - Added FilteredEvent item type/handler
*/
@NonNullByDefault
@Component(configurationPid = "binding.icalendar", service = ThingHandlerFactory.class)
public class ICalendarHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_CALENDAR);
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(Collections.singleton(THING_TYPE_CALENDAR), Collections.singleton(THING_TYPE_FILTERED_EVENTS))
.flatMap(Set::stream).collect(Collectors.toSet());
private final Logger logger = LoggerFactory.getLogger(ICalendarHandlerFactory.class);
private final HttpClient sharedHttpClient;
private final EventPublisher eventPublisher;
private final TimeZoneProvider tzProvider;
@Activate
public ICalendarHandlerFactory(@Reference HttpClientFactory httpClientFactory,
@Reference EventPublisher eventPublisher) {
@Reference EventPublisher eventPublisher, @Reference TimeZoneProvider tzProvider) {
this.eventPublisher = eventPublisher;
sharedHttpClient = httpClientFactory.getCommonHttpClient();
this.tzProvider = tzProvider;
}
@Override
@@ -67,6 +80,16 @@ public class ICalendarHandlerFactory extends BaseThingHandlerFactory {
if (!supportsThingType(thingTypeUID)) {
return null;
}
return new ICalendarHandler(thing, sharedHttpClient, eventPublisher);
if (thingTypeUID.equals(THING_TYPE_CALENDAR)) {
if (thing instanceof Bridge) {
return new ICalendarHandler((Bridge) thing, sharedHttpClient, eventPublisher, tzProvider);
} else {
logger.warn(
"The API of iCalendar has changed. You have to recreate the calendar according to the docs.");
}
} else if (thingTypeUID.equals(THING_TYPE_FILTERED_EVENTS)) {
return new EventFilterHandler(thing, tzProvider);
}
return null;
}
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2020 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.icalendar.internal.config;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The EventFilterConfiguration holds configuration for the Event Filter Item Type.
*
* @author Michael Wodniok - Initial contribution
*/
@NonNullByDefault
public class EventFilterConfiguration {
@Nullable
public BigDecimal maxEvents;
@Nullable
public BigDecimal refreshTime;
@Nullable
public String datetimeUnit;
@Nullable
public BigDecimal datetimeStart;
@Nullable
public BigDecimal datetimeEnd;
@Nullable
public Boolean datetimeRound;
@Nullable
public String textEventField;
@Nullable
public String textEventValue;
@Nullable
public String textValueType;
}

View File

@@ -14,17 +14,28 @@ package org.openhab.binding.icalendar.internal.config;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link ICalendarConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Michael Wodniok - Initial contribution
* @author Andrew Fiddian-Green - Support for authorizationCode
* @author Michael Wodniok - Added Nullable annotations for conformity
*/
@NonNullByDefault
public class ICalendarConfiguration {
@Nullable
public String authorizationCode;
public Integer maxSize;
@Nullable
public BigDecimal maxSize;
@Nullable
public String password;
@Nullable
public BigDecimal refreshTime;
@Nullable
public String url;
@Nullable
public String username;
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 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.icalendar.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception or semantically describe configuration errors. Message is meant to be shown to the user.
*
* @author Michael Wodniok - Initial contribution
*/
@NonNullByDefault
public class ConfigBrokenException extends Exception {
private static final long serialVersionUID = -3805312008429711152L;
public ConfigBrokenException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,396 @@
/**
* Copyright (c) 2010-2020 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.icalendar.internal.handler;
import static org.openhab.binding.icalendar.internal.ICalendarBindingConstants.*;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
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.icalendar.internal.config.EventFilterConfiguration;
import org.openhab.binding.icalendar.internal.handler.PullJob.CalendarUpdateListener;
import org.openhab.binding.icalendar.internal.logic.AbstractPresentableCalendar;
import org.openhab.binding.icalendar.internal.logic.Event;
import org.openhab.binding.icalendar.internal.logic.EventTextFilter;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelGroupUID;
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.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link EventFilterHandler} filters events from a calendar and presents them in a dynamic way.
*
* @author Michael Wodniok - Initial Contribution
*/
@NonNullByDefault
public class EventFilterHandler extends BaseThingHandler implements CalendarUpdateListener {
private @Nullable EventFilterConfiguration configuration;
private final Logger logger = LoggerFactory.getLogger(EventFilterHandler.class);
private final List<ResultChannelSet> resultChannels;
private final TimeZoneProvider tzProvider;
private @Nullable ScheduledFuture<?> updateFuture;
private boolean initFinished;
public EventFilterHandler(Thing thing, TimeZoneProvider tzProvider) {
super(thing);
resultChannels = new CopyOnWriteArrayList<>();
initFinished = false;
this.tzProvider = tzProvider;
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
} else if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
updateStates();
} else {
updateStatus(ThingStatus.UNKNOWN);
}
}
@Override
public void dispose() {
final ScheduledFuture<?> currentUpdateFuture = updateFuture;
if (currentUpdateFuture != null) {
currentUpdateFuture.cancel(true);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
if (initFinished) {
updateStates();
}
}
}
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
Bridge iCalendarBridge = getBridge();
if (iCalendarBridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"This thing requires a bridge configured to work.");
return;
}
final EventFilterConfiguration config = getConfigAs(EventFilterConfiguration.class);
if (config.datetimeUnit == null && (config.datetimeEnd != null || config.datetimeStart != null)) {
logger.warn("Start/End date-time is set but no unit. This will ignore the filter.");
}
if (config.textEventField != null && config.textValueType == null) {
logger.warn("Event field is set but not match type. This will ignore the filter.");
}
configuration = config;
if (iCalendarBridge.getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
return;
} else {
updateChannelSet(config);
updateStates();
}
initFinished = true;
}
@Override
public void onCalendarUpdated() {
updateStates();
}
/**
* Consists of a set of channels and their group for describing a filtered event. *
*/
private class ResultChannelSet {
ChannelGroupUID resultGroup;
ChannelUID beginChannel;
ChannelUID endChannel;
ChannelUID titleChannel;
public ResultChannelSet(ChannelGroupUID group, ChannelUID begin, ChannelUID end, ChannelUID title) {
resultGroup = group;
beginChannel = begin;
endChannel = end;
titleChannel = title;
}
}
/**
* Describes some fixed time factors for unit selection.
*/
private enum TimeMultiplicator {
MINUTE(60),
HOUR(3600),
DAY(86400),
WEEK(604800);
private final int secondsPerUnit;
private TimeMultiplicator(int secondsPerUnit) {
this.secondsPerUnit = secondsPerUnit;
}
/**
* Returns the count of seconds per unit.
*
* @return Seconds per unit.
*/
public int getMultiplier() {
return secondsPerUnit;
}
}
/**
* Generates a list of channel sets according to the required amount.
*
* @param resultCount The required amount of results.
*/
private void generateExpectedChannelList(int resultCount) {
synchronized (resultChannels) {
if (resultChannels.size() == resultCount) {
return;
}
resultChannels.clear();
for (int position = 0; position < resultCount; position++) {
ChannelGroupUID currentGroup = new ChannelGroupUID(getThing().getUID(),
RESULT_GROUP_ID_PREFIX + position);
ResultChannelSet current = new ResultChannelSet(currentGroup,
new ChannelUID(currentGroup, RESULT_BEGIN_ID), new ChannelUID(currentGroup, RESULT_END_ID),
new ChannelUID(currentGroup, RESULT_TITLE_ID));
resultChannels.add(current);
}
}
}
/**
* Checks existing channels, adds missing and removes extraneous channels from the Thing.
*
* @param config The validated Configuration of the Thing.
*/
private void updateChannelSet(EventFilterConfiguration config) {
final ThingHandlerCallback handlerCallback = getCallback();
if (handlerCallback == null) {
return;
}
final List<Channel> currentChannels = getThing().getChannels();
final ThingBuilder thingBuilder = editThing();
BigDecimal maxEvents = config.maxEvents;
if (maxEvents == null || maxEvents.compareTo(BigDecimal.ZERO) < 1) {
thingBuilder.withoutChannels(currentChannels);
updateThing(thingBuilder.build());
return;
}
generateExpectedChannelList(maxEvents.intValue());
synchronized (resultChannels) {
currentChannels.stream().filter((Channel current) -> {
String currentGroupId = current.getUID().getGroupId();
if (currentGroupId == null) {
return true;
}
for (ResultChannelSet channelSet : resultChannels) {
if (channelSet.resultGroup.getId().contentEquals(currentGroupId)) {
return false;
}
}
return true;
}).forEach((Channel toDelete) -> {
thingBuilder.withoutChannel(toDelete.getUID());
});
resultChannels.stream().filter((ResultChannelSet current) -> {
return (getThing().getChannelsOfGroup(current.resultGroup.toString()).size() == 0);
}).forEach((ResultChannelSet current) -> {
for (ChannelBuilder builder : handlerCallback.createChannelBuilders(current.resultGroup,
GROUP_TYPE_UID)) {
Channel currentChannel = builder.build();
Channel existingChannel = getThing().getChannel(currentChannel.getUID());
if (existingChannel == null) {
thingBuilder.withChannel(currentChannel);
}
}
});
}
updateThing(thingBuilder.build());
}
/**
* Updates all states and channels. Reschedules an update if no error occurs.
*/
private void updateStates() {
final Bridge iCalendarBridge = getBridge();
if (iCalendarBridge == null) {
logger.debug("Bridge not instantiated!");
return;
}
final ICalendarHandler iCalendarHandler = (ICalendarHandler) iCalendarBridge.getHandler();
if (iCalendarHandler == null) {
logger.debug("ICalendarHandler not instantiated!");
return;
}
final EventFilterConfiguration config = configuration;
if (config == null) {
logger.debug("Configuration not instantiated!");
return;
}
final AbstractPresentableCalendar cal = iCalendarHandler.getRuntimeCalendar();
if (cal != null) {
updateStatus(ThingStatus.ONLINE);
Instant reference = Instant.now();
TimeMultiplicator multiplicator = null;
EventTextFilter filter = null;
int maxEvents;
Instant begin = Instant.EPOCH;
Instant end = Instant.ofEpochMilli(Long.MAX_VALUE);
try {
String textFilterValue = config.textEventValue;
if (textFilterValue != null) {
String textEventField = config.textEventField;
String textValueType = config.textValueType;
if (textEventField == null || textValueType == null) {
throw new ConfigBrokenException("Text filter settings are not set properly.");
}
try {
EventTextFilter.Field textFilterField = EventTextFilter.Field.valueOf(textEventField);
EventTextFilter.Type textFilterType = EventTextFilter.Type.valueOf(textValueType);
filter = new EventTextFilter(textFilterField, textFilterValue, textFilterType);
} catch (IllegalArgumentException e2) {
throw new ConfigBrokenException("textEventField or textValueType are not set properly.");
}
}
BigDecimal maxEventsBD = config.maxEvents;
if (maxEventsBD == null) {
throw new ConfigBrokenException("maxEvents is not set.");
}
maxEvents = maxEventsBD.intValue();
if (maxEvents < 0) {
throw new ConfigBrokenException("maxEvents is less than 0. This is not allowed.");
}
try {
final String datetimeUnit = config.datetimeUnit;
if (datetimeUnit != null) {
multiplicator = TimeMultiplicator.valueOf(datetimeUnit);
}
} catch (IllegalArgumentException e) {
throw new ConfigBrokenException("datetimeUnit is not set properly.");
}
final Boolean datetimeRound = config.datetimeRound;
if (datetimeRound != null && datetimeRound.booleanValue()) {
if (multiplicator == null) {
throw new ConfigBrokenException("datetimeUnit is missing but required for datetimeRound.");
}
ZonedDateTime refDT = reference.atZone(tzProvider.getTimeZone());
switch (multiplicator) {
case WEEK:
refDT = refDT.with(ChronoField.DAY_OF_WEEK, 1);
case DAY:
refDT = refDT.with(ChronoField.HOUR_OF_DAY, 0);
case HOUR:
refDT = refDT.with(ChronoField.MINUTE_OF_HOUR, 0);
case MINUTE:
refDT = refDT.with(ChronoField.SECOND_OF_MINUTE, 0);
}
reference = refDT.toInstant();
}
BigDecimal datetimeStart = config.datetimeStart;
if (datetimeStart != null) {
if (multiplicator == null) {
throw new ConfigBrokenException("datetimeUnit is missing but required for datetimeStart.");
}
begin = reference.plusSeconds(datetimeStart.longValue() * multiplicator.getMultiplier());
}
BigDecimal datetimeEnd = config.datetimeEnd;
if (datetimeEnd != null) {
if (multiplicator == null) {
throw new ConfigBrokenException("datetimeUnit is missing but required for datetimeEnd.");
}
end = reference.plusSeconds(datetimeEnd.longValue() * multiplicator.getMultiplier());
}
} catch (ConfigBrokenException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
return;
}
synchronized (resultChannels) {
List<Event> results = cal.getFilteredEventsBetween(begin, end, filter, maxEvents);
for (int position = 0; position < resultChannels.size(); position++) {
ResultChannelSet channels = resultChannels.get(position);
if (position < results.size()) {
Event result = results.get(position);
updateState(channels.titleChannel, new StringType(result.title));
updateState(channels.beginChannel,
new DateTimeType(result.start.atZone(tzProvider.getTimeZone())));
updateState(channels.endChannel, new DateTimeType(result.end.atZone(tzProvider.getTimeZone())));
} else {
updateState(channels.titleChannel, UnDefType.UNDEF);
updateState(channels.beginChannel, UnDefType.UNDEF);
updateState(channels.endChannel, UnDefType.UNDEF);
}
}
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Calendar has not been retrieved yet.");
}
int refreshTime = DEFAULT_FILTER_REFRESH;
if (config.refreshTime != null) {
refreshTime = config.refreshTime.intValue();
if (refreshTime < 1) {
logger.debug("refreshTime is set to invalid value. Using default.");
refreshTime = DEFAULT_FILTER_REFRESH;
}
}
ScheduledFuture<?> currentUpdateFuture = updateFuture;
if (currentUpdateFuture != null) {
currentUpdateFuture.cancel(true);
}
updateFuture = scheduler.scheduleWithFixedDelay(this::updateStates, refreshTime, refreshTime, TimeUnit.MINUTES);
}
}

View File

@@ -17,10 +17,10 @@ import static org.openhab.binding.icalendar.internal.ICalendarBindingConstants.*
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.time.ZoneId;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -37,15 +37,18 @@ import org.openhab.binding.icalendar.internal.logic.CommandTagType;
import org.openhab.binding.icalendar.internal.logic.Event;
import org.openhab.core.OpenHAB;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.items.events.ItemEventFactory;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
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.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
@@ -60,25 +63,28 @@ import org.slf4j.LoggerFactory;
* @author Andrew Fiddian-Green - Support for Command Tags embedded in the Event description
*/
@NonNullByDefault
public class ICalendarHandler extends BaseThingHandler implements CalendarUpdateListener {
public class ICalendarHandler extends BaseBridgeHandler implements CalendarUpdateListener {
private final File calendarFile;
private @Nullable ICalendarConfiguration configuration;
private final EventPublisher eventPublisherCallback;
private final HttpClient httpClient;
private final Logger logger = LoggerFactory.getLogger(ICalendarHandler.class);
private final TimeZoneProvider tzProvider;
private @Nullable ScheduledFuture<?> pullJobFuture;
private @Nullable AbstractPresentableCalendar runtimeCalendar;
private @Nullable ScheduledFuture<?> updateJobFuture;
private Instant updateStatesLastCalledTime;
public ICalendarHandler(Thing thing, HttpClient httpClient, EventPublisher eventPublisher) {
super(thing);
public ICalendarHandler(Bridge bridge, HttpClient httpClient, EventPublisher eventPublisher,
TimeZoneProvider tzProvider) {
super(bridge);
this.httpClient = httpClient;
calendarFile = new File(OpenHAB.getUserDataFolder() + File.separator
+ getThing().getUID().getAsString().replaceAll("[<>:\"/\\\\|?*]", "_") + ".ical");
eventPublisherCallback = eventPublisher;
updateStatesLastCalledTime = Instant.now();
this.tzProvider = tzProvider;
}
@Override
@@ -119,42 +125,53 @@ public class ICalendarHandler extends BaseThingHandler implements CalendarUpdate
final ICalendarConfiguration currentConfiguration = getConfigAs(ICalendarConfiguration.class);
configuration = currentConfiguration;
if ((currentConfiguration.username == null && currentConfiguration.password != null)
|| (currentConfiguration.username != null && currentConfiguration.password == null)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Only one of username and password was set. This is invalid.");
return;
}
PullJob regularPull;
try {
regularPull = new PullJob(httpClient, new URI(currentConfiguration.url), currentConfiguration.username,
currentConfiguration.password, calendarFile, currentConfiguration.maxSize * 1048576, this);
} catch (URISyntaxException e) {
logger.warn(
"The URI '{}' for downloading the calendar contains syntax errors. This will result in no downloads/updates.",
currentConfiguration.url, e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
return;
}
if (calendarFile.isFile()) {
if (reloadCalendar()) {
updateStatus(ThingStatus.ONLINE);
updateStates();
rescheduleCalendarStateUpdate();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"The calendar seems to be configured correctly, but the local copy of calendar could not be loaded.");
if ((currentConfiguration.username == null && currentConfiguration.password != null)
|| (currentConfiguration.username != null && currentConfiguration.password == null)) {
throw new ConfigBrokenException("Only one of username and password was set. This is invalid.");
}
pullJobFuture = scheduler.scheduleWithFixedDelay(regularPull, currentConfiguration.refreshTime.longValue(),
currentConfiguration.refreshTime.longValue(), TimeUnit.MINUTES);
} else {
updateStatus(ThingStatus.OFFLINE);
logger.debug(
"The calendar is currently offline as no local copy exists. It will go online as soon as a valid valid calendar is retrieved.");
pullJobFuture = scheduler.scheduleWithFixedDelay(regularPull, 0,
currentConfiguration.refreshTime.longValue(), TimeUnit.MINUTES);
PullJob regularPull;
final BigDecimal maxSizeBD = currentConfiguration.maxSize;
if (maxSizeBD == null || maxSizeBD.intValue() < 1) {
throw new ConfigBrokenException(
"maxSize is either not set or less than 1 (mebibyte), which is not allowed.");
}
final int maxSize = maxSizeBD.intValue();
try {
regularPull = new PullJob(httpClient, new URI(currentConfiguration.url), currentConfiguration.username,
currentConfiguration.password, calendarFile, maxSize * 1048576, this);
} catch (URISyntaxException e) {
throw new ConfigBrokenException(String.format(
"The URI '%s' for downloading the calendar contains syntax errors.", currentConfiguration.url));
}
final BigDecimal refreshTimeBD = currentConfiguration.refreshTime;
if (refreshTimeBD == null || refreshTimeBD.longValue() < 1) {
throw new ConfigBrokenException(
"refreshTime is either not set or less than 1 (minute), which is not allowed.");
}
final long refreshTime = refreshTimeBD.longValue();
if (calendarFile.isFile()) {
if (reloadCalendar()) {
updateStatus(ThingStatus.ONLINE);
updateStates();
rescheduleCalendarStateUpdate();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"The calendar seems to be configured correctly, but the local copy of calendar could not be loaded.");
}
pullJobFuture = scheduler.scheduleWithFixedDelay(regularPull, refreshTime, refreshTime,
TimeUnit.MINUTES);
} else {
updateStatus(ThingStatus.OFFLINE);
logger.debug(
"The calendar is currently offline as no local copy exists. It will go online as soon as a valid valid calendar is retrieved.");
pullJobFuture = scheduler.scheduleWithFixedDelay(regularPull, 0, refreshTime, TimeUnit.MINUTES);
}
} catch (ConfigBrokenException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
}
}
@@ -162,20 +179,36 @@ public class ICalendarHandler extends BaseThingHandler implements CalendarUpdate
public void onCalendarUpdated() {
if (reloadCalendar()) {
updateStates();
for (Thing childThing : getThing().getThings()) {
ThingHandler handler = childThing.getHandler();
if (handler instanceof CalendarUpdateListener) {
try {
((CalendarUpdateListener) handler).onCalendarUpdated();
} catch (Exception e) {
logger.trace("The update of a child handler failed. Ignoring.", e);
}
}
}
} else {
logger.trace("Calendar was updated, but loading failed.");
}
}
/**
* @return the calendar that is used for all operations
*/
@Nullable
public AbstractPresentableCalendar getRuntimeCalendar() {
return runtimeCalendar;
}
private void executeEventCommands(List<Event> events, CommandTagType execTime) {
// no begun or ended events => exit quietly as there is nothing to do
if (events.isEmpty()) {
return;
}
// prevent potential synchronization issues (MVN null pointer warnings) in "configuration"
@Nullable
ICalendarConfiguration syncConfiguration = configuration;
final ICalendarConfiguration syncConfiguration = configuration;
if (syncConfiguration == null) {
logger.debug("Configuration not instantiated!");
return;
@@ -318,9 +351,9 @@ public class ICalendarHandler extends BaseThingHandler implements CalendarUpdate
} else {
updateState(CHANNEL_CURRENT_EVENT_TITLE, new StringType(currentEvent.title));
updateState(CHANNEL_CURRENT_EVENT_START,
new DateTimeType(currentEvent.start.atZone(ZoneId.systemDefault())));
new DateTimeType(currentEvent.start.atZone(tzProvider.getTimeZone())));
updateState(CHANNEL_CURRENT_EVENT_END,
new DateTimeType(currentEvent.end.atZone(ZoneId.systemDefault())));
new DateTimeType(currentEvent.end.atZone(tzProvider.getTimeZone())));
}
} else {
updateState(CHANNEL_CURRENT_EVENT_PRESENT, OnOffType.OFF);
@@ -332,8 +365,9 @@ public class ICalendarHandler extends BaseThingHandler implements CalendarUpdate
final Event nextEvent = calendar.getNextEvent(now);
if (nextEvent != null) {
updateState(CHANNEL_NEXT_EVENT_TITLE, new StringType(nextEvent.title));
updateState(CHANNEL_NEXT_EVENT_START, new DateTimeType(nextEvent.start.atZone(ZoneId.systemDefault())));
updateState(CHANNEL_NEXT_EVENT_END, new DateTimeType(nextEvent.end.atZone(ZoneId.systemDefault())));
updateState(CHANNEL_NEXT_EVENT_START,
new DateTimeType(nextEvent.start.atZone(tzProvider.getTimeZone())));
updateState(CHANNEL_NEXT_EVENT_END, new DateTimeType(nextEvent.end.atZone(tzProvider.getTimeZone())));
} else {
updateState(CHANNEL_NEXT_EVENT_TITLE, UnDefType.UNDEF);
updateState(CHANNEL_NEXT_EVENT_START, UnDefType.UNDEF);

View File

@@ -53,7 +53,7 @@ import org.slf4j.LoggerFactory;
*/
@NonNullByDefault
class PullJob implements Runnable {
private final static String TMP_FILE_PREFIX = "icalendardld";
private static final String TMP_FILE_PREFIX = "icalendardld";
private final Authentication.@Nullable Result authentication;
private final File destination;
@@ -91,7 +91,7 @@ class PullJob implements Runnable {
@Override
public void run() {
final Request request = httpClient.newRequest(sourceURI).followRedirects(true).method(HttpMethod.GET);
final Authentication.@Nullable Result currentAuthentication = authentication;
final Authentication.Result currentAuthentication = authentication;
if (currentAuthentication != null) {
currentAuthentication.apply(request);
}

View File

@@ -26,6 +26,7 @@ import org.eclipse.jdt.annotation.Nullable;
*
* @author Michael Wodniok - Initial contribution
* @author Andrew Fiddian-Green - Methods getJustBegunEvents() & getJustEndedEvents()
* @author Michael Wodniok - Added getFilteredEventsBetween()
*/
@NonNullByDefault
public abstract class AbstractPresentableCalendar {
@@ -86,4 +87,16 @@ public abstract class AbstractPresentableCalendar {
* @return True if an event is present.
*/
public abstract boolean isEventPresent(Instant instant);
/**
* Return a filtered List of events with a maximum count, ordered by start.
*
* @param begin The begin of the time range where to search for events
* @param end The end of the time range where to search for events
* @param filter A filter for contents, if set to null, all events will be returned
* @param maximumCount The maximum of events returned here.
* @return A list with the filtered results.
*/
public abstract List<Event> getFilteredEventsBetween(Instant begin, Instant end, @Nullable EventTextFilter filter,
int maximumCount);
}

View File

@@ -18,24 +18,32 @@ import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.icalendar.internal.logic.EventTextFilter.Type;
import biweekly.ICalendar;
import biweekly.component.VEvent;
import biweekly.io.TimezoneAssignment;
import biweekly.io.TimezoneInfo;
import biweekly.io.text.ICalReader;
import biweekly.property.Comment;
import biweekly.property.Contact;
import biweekly.property.DateEnd;
import biweekly.property.DateStart;
import biweekly.property.Description;
import biweekly.property.DurationProperty;
import biweekly.property.Location;
import biweekly.property.Status;
import biweekly.property.Summary;
import biweekly.property.TextProperty;
import biweekly.property.Uid;
import biweekly.util.com.google.ical.compat.javautil.DateIterator;
@@ -46,6 +54,7 @@ import biweekly.util.com.google.ical.compat.javautil.DateIterator;
*
* @author Michael Wodniok - Initial contribution
* @author Andrew Fiddian-Green - Methods getJustBegunEvents() & getJustEndedEvents()
* @author Michael Wodniok - Extension for filtered events
*/
@NonNullByDefault
class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
@@ -140,7 +149,6 @@ class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
while (startDates.hasNext()) {
final Instant startInstant = startDates.next().toInstant();
if (startInstant.isAfter(instant)) {
@Nullable
final Uid currentEventUid = currentEvent.getUid();
if (currentEventUid == null || !isCounteredBy(startInstant, currentEventUid, negativeEvents)) {
candidates.add(new VEventWPeriod(currentEvent, startInstant, startInstant.plus(duration)));
@@ -167,6 +175,104 @@ class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
return (this.getCurrentComponentWPeriod(instant) != null);
}
@Override
public List<Event> getFilteredEventsBetween(Instant begin, Instant end, @Nullable EventTextFilter filter,
int maximumCount) {
List<VEventWPeriod> candidates = this.getVEventWPeriodsBetween(begin, end);
final List<Event> results = new ArrayList<>(candidates.size());
if (filter != null) {
Pattern filterPattern;
if (filter.type == Type.TEXT) {
filterPattern = Pattern.compile(".*" + Pattern.quote(filter.value) + ".*",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
} else {
filterPattern = Pattern.compile(filter.value);
}
Class<? extends TextProperty> propertyClass;
switch (filter.field) {
case SUMMARY:
propertyClass = Summary.class;
break;
case COMMENT:
propertyClass = Comment.class;
break;
case CONTACT:
propertyClass = Contact.class;
break;
case DESCRIPTION:
propertyClass = Description.class;
break;
case LOCATION:
propertyClass = Location.class;
break;
default:
throw new IllegalArgumentException("Unknown Property to filter for.");
}
List<VEventWPeriod> filteredCandidates = candidates.stream().filter(current -> {
List<? extends TextProperty> properties = current.vEvent.getProperties(propertyClass);
for (TextProperty prop : properties) {
if (filterPattern.matcher(prop.getValue()).matches()) {
return true;
}
}
return false;
}).collect(Collectors.toList());
candidates = filteredCandidates;
}
for (VEventWPeriod eventWPeriod : candidates) {
results.add(eventWPeriod.toEvent());
}
Collections.sort(results);
return results.subList(0, (maximumCount > results.size() ? results.size() : maximumCount));
}
/**
* Finds events which begin in the given frame.
*
* @param frameBegin Begin of the frame where to search events.
* @param frameEnd End of the time frame where to search events.
* @return All events which begin in the time frame.
*/
private List<VEventWPeriod> getVEventWPeriodsBetween(Instant frameBegin, Instant frameEnd) {
final List<VEvent> positiveEvents = new ArrayList<>();
final List<VEvent> negativeEvents = new ArrayList<>();
classifyEvents(positiveEvents, negativeEvents);
final List<VEventWPeriod> eventList = new ArrayList<>();
for (final VEvent positiveEvent : positiveEvents) {
final DateIterator positiveBeginDates = getRecurredEventDateIterator(positiveEvent);
positiveBeginDates.advanceTo(Date.from(frameBegin));
while (positiveBeginDates.hasNext()) {
final Instant begInst = positiveBeginDates.next().toInstant();
if (begInst.isAfter(frameEnd)) {
break;
}
Duration duration = getEventLength(positiveEvent);
if (duration == null) {
duration = Duration.ZERO;
}
final VEventWPeriod resultingVEWP = new VEventWPeriod(positiveEvent, begInst, begInst.plus(duration));
final Uid eventUid = positiveEvent.getUid();
if (eventUid != null) {
if (!isCounteredBy(begInst, eventUid, negativeEvents)) {
eventList.add(resultingVEWP);
}
} else {
eventList.add(resultingVEWP);
}
}
}
return eventList;
}
/**
* Classifies events into positive and negative ones.
*
@@ -175,7 +281,6 @@ class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
*/
private void classifyEvents(Collection<VEvent> positiveEvents, Collection<VEvent> negativeEvents) {
for (final VEvent currentEvent : usedCalendar.getEvents()) {
@Nullable
final Status eventStatus = currentEvent.getStatus();
boolean positive = (eventStatus == null || (eventStatus.isTentative() || eventStatus.isConfirmed()));
final Collection<VEvent> positiveOrNegativeEvents = (positive ? positiveEvents : negativeEvents);
@@ -205,7 +310,6 @@ class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
final Instant startInstant = startDates.next().toInstant();
final Instant endInstant = startInstant.plus(duration);
if (startInstant.isBefore(instant) && endInstant.isAfter(instant)) {
@Nullable
final Uid eventUid = currentEvent.getUid();
if (eventUid == null || !isCounteredBy(startInstant, eventUid, negativeEvents)) {
return new VEventWPeriod(currentEvent, startInstant, endInstant);
@@ -270,7 +374,6 @@ class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
*/
private boolean isCounteredBy(Instant startInstant, Uid eventUid, Collection<VEvent> counterEvents) {
for (final VEvent counterEvent : counterEvents) {
@Nullable
final Uid counterEventUid = counterEvent.getUid();
if (counterEventUid != null && eventUid.getValue().contentEquals(counterEventUid.getValue())) {
final DateIterator counterStartDates = getRecurredEventDateIterator(counterEvent);

View File

@@ -26,7 +26,7 @@ import org.eclipse.jdt.annotation.Nullable;
* @author Andrew Fiddian-Green - Added support for event description
*/
@NonNullByDefault
public class Event {
public class Event implements Comparable<Event> {
public final List<CommandTag> commandTags = new ArrayList<CommandTag>();
public final Instant end;
public final Instant start;
@@ -50,6 +50,16 @@ public class Event {
}
}
@Override
public String toString() {
String[] tagStrings = new String[this.commandTags.size()];
for (int i = 0; i < tagStrings.length; i++) {
tagStrings[i] = this.commandTags.get(i).toString();
}
return "Event(title: " + this.title + ", start: " + this.start.toString() + ", end: " + this.end.toString()
+ ", commandTags: List(" + String.join(", ", tagStrings) + ")";
}
@Override
public boolean equals(@Nullable Object other) {
if (other == null || other.getClass() != this.getClass()) {
@@ -59,4 +69,9 @@ public class Event {
return (this.title.equals(otherEvent.title) && this.start.equals(otherEvent.start)
&& this.end.equals(otherEvent.end));
}
@Override
public int compareTo(Event o) {
return start.compareTo(o.start);
}
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 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.icalendar.internal.logic;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Transport class for a simple text filter.
*
* @author Michael Wodniok - Initial contribution
*/
@NonNullByDefault
public class EventTextFilter {
public static enum Type {
TEXT,
REGEX
}
public static enum Field {
SUMMARY,
DESCRIPTION,
COMMENT,
CONTACT,
LOCATION
}
public Field field;
public String value;
public Type type;
public EventTextFilter(Field field, String value, Type type) {
this.field = field;
this.value = value;
this.type = type;
}
}

View File

@@ -5,6 +5,8 @@ binding.icalendar.description = Binding zur Nutzung von iCal-Kalendern als Pr
# thing types
thing-type.icalendar.calendar.label = Kalender
thing-type.icalendar.calendar.description = Kalender basierend auf einem lesbaren iCal-Kalender.
thing-type.icalendar.eventfilter.label = Eintragsfilter
thing-type.icalendar.eventfilter.description = Gefilterte Events aus dem zugeordneten Kalender.
# thing type config description
thing-type.config.icalendar.calendar.url.label = URL
@@ -19,6 +21,35 @@ thing-type.config.icalendar.calendar.maxSize.label = Maximale Gr
thing-type.config.icalendar.calendar.maxSize.description = Es werden nur iCal-Dateien verwendet, die bis zur angegebenen Größe (in Mebibytes) groß sind
thing-type.config.icalendar.calendar.authorizationCode.label = Autorisierungs-Code
thing-type.config.icalendar.calendar.authorizationCode.description = Code zur Autorisierung von Kommandos in Kalendareinträgen
thing-type.config.icalendar.eventfilter.maxEvents.label = Ergebnis-Maximum
thing-type.config.icalendar.eventfilter.maxEvents.description = Maximale Anzahl an Ergebnissen dieses Filters
thing-type.config.icalendar.eventfilter.refreshTime.label = Aktualisierungsintervall
thing-type.config.icalendar.eventfilter.refreshTime.description = Intervall, in dem die Ergebnisliste aktualisiert wird (Minuten)
thing-type.config.icalendar.eventfilter.datetimeUnit.label = Zeiteinheit
thing-type.config.icalendar.eventfilter.datetimeUnit.description = Einheit der Angaben zu Start und Ende
thing-type.config.icalendar.eventfilter.datetimeUnit.option.MINUTE = Minute
thing-type.config.icalendar.eventfilter.datetimeUnit.option.HOUR = Stunde
thing-type.config.icalendar.eventfilter.datetimeUnit.option.DAY = Tag
thing-type.config.icalendar.eventfilter.datetimeUnit.option.WEEK = Woche
thing-type.config.icalendar.eventfilter.datetimeStart.label = Start
thing-type.config.icalendar.eventfilter.datetimeStart.description = Startzeitpunkt relativ zu "jetzt" (inklusiv)
thing-type.config.icalendar.eventfilter.datetimeEnd.label = Ende
thing-type.config.icalendar.eventfilter.datetimeEnd.description = Endzeitpunkt relativ zu "jetzt" (exklusiv)
thing-type.config.icalendar.eventfilter.datetimeRound.label = Abrundung auf Zeiteinheit
thing-type.config.icalendar.eventfilter.datetimeRound.description = Zeitpunkt sollen auf die Zeiteinheit abgerundet werden (z.B. auf Mitternacht bei Einheit "Tag")
thing-type.config.icalendar.eventfilter.textEventField.label = Event-Feld
thing-type.config.icalendar.eventfilter.textEventField.description = Das Feld innerhalb der Ereignis, in dem gefiltert werden soll
thing-type.config.icalendar.eventfilter.textEventField.option.SUMMARY = Betreff/Titel
thing-type.config.icalendar.eventfilter.textEventField.option.DESCRIPTION = Bescheibung/Inhalt
thing-type.config.icalendar.eventfilter.textEventField.option.COMMENT = Kommentar
thing-type.config.icalendar.eventfilter.textEventField.option.CONTACT = Kontakt
thing-type.config.icalendar.eventfilter.textEventField.option.LOCATION = Ort
thing-type.config.icalendar.eventfilter.textEventValue.label = Suchausdruck
thing-type.config.icalendar.eventfilter.textValueType.label = Typ des Suchausdrucks
thing-type.config.icalendar.eventfilter.textValueType.description = "Text" prüft, ob der Ausdruck enthalten ist, "Regulärer Ausdruck" prüft, ob der Ausdruck aus den Feldwert im Ganzen zutrifft
thing-type.config.icalendar.eventfilter.textValueType.option.TEXT = Text
thing-type.config.icalendar.eventfilter.textValueType.option.REGEX = Regulärer Ausdruck
# channel types
channel-type.icalendar.event_current_title.label = Titel des aktuellen Eintrags
@@ -35,3 +66,11 @@ channel-type.icalendar.event_next_start.label = Start des n
channel-type.icalendar.event_next_start.description = Start des nächsten Eintrags
channel-type.icalendar.event_next_end.label = Ende des nächsten Eintrags
channel-type.icalendar.event_next_end.description = Ende des nächsten Eintrags
channel-group-type.icalendar.result.label = Ergebnis
channel-group-type.icalendar.result.description = Ergebnis, gefunden durch den Filter
channel-type.icalendar.result_start.label = Ergebnisstart
channel-type.icalendar.result_start.description = Startzeitpunkt des gefundenen Ergebnis'
channel-type.icalendar.result_end.label = Ergebnisende
channel-type.icalendar.result_end.description = Endzeitpunkt des gefundenen Ergebnis'
channel-type.icalendar.result_title.label = Ergebnistitel
channel-type.icalendar.result_title.description = Titel des gefundenen Ergebnis'

View File

@@ -4,7 +4,7 @@
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="calendar">
<bridge-type id="calendar">
<label>Calendar</label>
<description>Calendar based on an iCal calendar.</description>
@@ -55,7 +55,7 @@
</parameter>
</config-description>
</thing-type>
</bridge-type>
<channel-type id="event_current_title">
<item-type>String</item-type>
@@ -99,4 +99,113 @@
<description>End of the next event in calendar</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="result_start">
<item-type>DateTime</item-type>
<label>Start of Result</label>
<description>Start of the found result in calendar</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="result_end">
<item-type>DateTime</item-type>
<label>End of Result</label>
<description>End of the found result in calendar</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="result_title">
<item-type>String</item-type>
<label>Title of Result</label>
<description>Title of the found result in calendar</description>
<state readOnly="true"/>
</channel-type>
<channel-group-type id="result">
<label>Result Event</label>
<description>A resulting event found by filter</description>
<channels>
<channel typeId="result_start" id="begin"/>
<channel typeId="result_end" id="end"/>
<channel typeId="result_title" id="title"/>
</channels>
</channel-group-type>
<thing-type id="eventfilter">
<supported-bridge-type-refs>
<bridge-type-ref id="calendar"/>
</supported-bridge-type-refs>
<label>Event Filter</label>
<description>Filtered Events from the calendar</description>
<config-description>
<parameter-group name="general">
<label>General Filter Options</label>
</parameter-group>
<parameter-group name="datetime_based">
<label>Date and Time based Filter</label>
</parameter-group>
<parameter-group name="text_based">
<label>Text based Filter</label>
</parameter-group>
<parameter name="maxEvents" type="integer" min="0" groupName="general">
<label>Maximum Matches</label>
<required>true</required>
</parameter>
<parameter name="refreshTime" type="integer" min="1" groupName="general" unit="min">
<label>Refresh Time</label>
<description>The frequency in minutes the channels get refreshed</description>
<required>true</required>
<default>15</default>
</parameter>
<parameter name="datetimeUnit" type="text" groupName="datetime_based">
<limitToOptions>true</limitToOptions>
<options>
<option value="MINUTE">minute</option>
<option value="HOUR">hour</option>
<option value="DAY">day</option>
<option value="WEEK">week</option>
</options>
<default>HOUR</default>
<label>Date or Time Unit for Start and End</label>
</parameter>
<parameter name="datetimeStart" type="integer" groupName="datetime_based">
<label>Start</label>
<description>Start date/time amount to find events relative to "now" (inclusive)</description>
</parameter>
<parameter name="datetimeEnd" type="integer" groupName="datetime_based">
<label>End</label>
<description>End date/time amount to find events relative to "now" (exclusive)</description>
</parameter>
<parameter name="datetimeRound" type="boolean" groupName="datetime_based">
<label>Round to Date/Time unit</label>
<description>Setting this will round start and end date/time to the unit down (e.g. if unit is day: start and end
will be rounded to 0:00 day time)</description>
</parameter>
<parameter name="textEventField" type="text" groupName="text_based">
<label>Event Field</label>
<description>iCal field to match</description>
<limitToOptions>true</limitToOptions>
<options>
<option value="SUMMARY">summary/subject</option>
<option value="DESCRIPTION">description/content</option>
<option value="COMMENT">comment</option>
<option value="CONTACT">contact</option>
<option value="LOCATION">location</option>
</options>
</parameter>
<parameter name="textEventValue" type="text" groupName="text_based">
<label>Event Value</label>
</parameter>
<parameter name="textValueType" type="text" groupName="text_based">
<limitToOptions>true</limitToOptions>
<options>
<option value="REGEX">Regular Expression</option>
<option value="TEXT">Text</option>
</options>
<default>TEXT</default>
<label>Value Type</label>
<description>"text" checks the value for containment, "regular expression" matches whole value</description>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@@ -36,8 +36,8 @@ import org.openhab.core.types.Command;
* Tests for presentable calendar.
*
* @author Michael Wodniok - Initial contribution.
*
* @author Andrew Fiddian-Green - Tests for Command Tag code
* @author Michael Wodniok - Extended Tests for filtered Events
*
*/
public class BiweeklyPresentableCalendarTest {
@@ -542,4 +542,47 @@ public class BiweeklyPresentableCalendarTest {
assertNotNull(cmd7);
assertEquals(QuantityType.class, cmd7.getClass());
}
@SuppressWarnings("null")
@Test
public void testGetFilteredEventsBetween() {
Event[] expectedFilteredEvents1 = new Event[] {
new Event("Test Series in UTC", Instant.parse("2019-09-12T09:05:00Z"),
Instant.parse("2019-09-12T09:10:00Z"), ""),
new Event("Test Event in UTC+2", Instant.parse("2019-09-14T08:00:00Z"),
Instant.parse("2019-09-14T09:00:00Z"), "") };
List<Event> realFilteredEvents1 = calendar.getFilteredEventsBetween(Instant.parse("2019-09-12T06:00:00Z"),
Instant.parse("2019-09-15T06:00:00Z"), null, 3);
assertArrayEquals(expectedFilteredEvents1, realFilteredEvents1.toArray(new Event[0]));
Event[] expectedFilteredEvents2 = new Event[] {
new Event("Evt", Instant.parse("2019-11-10T10:00:00Z"), Instant.parse("2019-11-10T11:45:00Z"), ""),
new Event("Evt", Instant.parse("2019-11-17T10:00:00Z"), Instant.parse("2019-11-17T11:45:00Z"), ""),
new Event("Evt", Instant.parse("2019-12-01T10:00:00Z"), Instant.parse("2019-12-01T11:45:00Z"), "") };
List<Event> realFilteredEvents2 = calendar2.getFilteredEventsBetween(Instant.parse("2019-11-08T06:00:00Z"),
Instant.parse("2019-12-31T06:00:00Z"), null, 3);
assertArrayEquals(expectedFilteredEvents2, realFilteredEvents2.toArray(new Event[] {}));
Event[] expectedFilteredEvents3 = new Event[] { new Event("Test Event in UTC+2",
Instant.parse("2019-09-14T08:00:00Z"), Instant.parse("2019-09-14T09:00:00Z"), "") };
List<Event> realFilteredEvents3 = calendar.getFilteredEventsBetween(Instant.parse("2019-09-12T06:00:00Z"),
Instant.parse("2019-09-15T06:00:00Z"),
new EventTextFilter(EventTextFilter.Field.SUMMARY, "utc+2", EventTextFilter.Type.TEXT), 3);
assertArrayEquals(expectedFilteredEvents3, realFilteredEvents3.toArray(new Event[] {}));
Event[] expectedFilteredEvents4 = new Event[] { new Event("Test Series in UTC",
Instant.parse("2019-09-12T09:05:00Z"), Instant.parse("2019-09-12T09:10:00Z"), "") };
List<Event> realFilteredEvents4 = calendar.getFilteredEventsBetween(Instant.parse("2019-09-12T06:00:00Z"),
Instant.parse("2019-09-15T06:00:00Z"),
new EventTextFilter(EventTextFilter.Field.SUMMARY, ".*UTC$", EventTextFilter.Type.REGEX), 3);
assertArrayEquals(expectedFilteredEvents4, realFilteredEvents4.toArray(new Event[] {}));
List<Event> realFilteredEvents5 = calendar.getFilteredEventsBetween(Instant.parse("2019-09-15T06:00:00Z"),
Instant.parse("2019-09-12T06:00:00Z"), null, 3);
assertEquals(0, realFilteredEvents5.size());
List<Event> realFilteredEvents6 = calendar.getFilteredEventsBetween(Instant.parse("2019-09-15T06:00:00Z"),
Instant.parse("2019-12-31T00:00:00Z"), null, 3);
assertEquals(0, realFilteredEvents6.size());
}
}