mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[icalendar] Add configuration for the behavior of the time-based event filter (#16105)
* Extract time-based event search strategies into a separate class This allows extensions without having to adapt the logic in BiweeklyPresentableCalendar. Signed-off-by: Christian Heinemann <ch@chlab.net> Co-authored-by: Leo Siepel <leosiepel@gmail.com>
This commit is contained in:
parent
41c8c45e18
commit
f37f39c03d
@ -10,7 +10,8 @@ The primary thing type is the calendar.
|
||||
It is based on a single iCalendar file and implemented as bridge.
|
||||
There can be multiple things having different properties representing different calendars.
|
||||
|
||||
Each calendar can have event filters which allow to get multiple events, maybe filtered by additional criteria. Time based filtering is done by each event's start.
|
||||
Each calendar can have event filters which allow to get multiple events, maybe filtered by additional criteria.
|
||||
Standard time-based filtering is done by each event's start, but it can also be configured to match other aspects.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
@ -40,6 +41,7 @@ Each `eventfilter` thing requires a bridge of type `calendar` and has following
|
||||
| `datetimeStart` | The start of the time frame where to search for events relative to current time. Combined with `datetimeUnit`. | optional |
|
||||
| `datetimeEnd` | The end of the time frame where to search for events relative to current time. Combined with `datetimeUnit`. The value must be greater than `datetimeStart` to get results. | optional |
|
||||
| `datetimeRound` | Whether to round the datetimes of start and end down to the earlier time unit. Example if set: current time is 13:00, timeunit is set to `DAY`. Resulting search will start and end at 0:00. | optional |
|
||||
| `datetimeMode` | Defines which part of an event must fall within the search period between start and end. Valid values: `START`, `ACTIVE` and `END`. | optional (default is `START`) |
|
||||
| `textEventField` | A field to filter the events text-based. Valid values: `SUMMARY`, `DESCRIPTION`, `COMMENT`, `CONTACT` and `LOCATION` (as described in RFC 5545). | optional/required for text-based filtering |
|
||||
| `textEventValue` | The text to filter events with. | optional |
|
||||
| `textValueType` | The type of the text to filter with. Valid values: `TEXT` (field must contain value, case insensitive), `REGEX` (field must match value, completely, dot matches all, usually case sensitive). | optional/required for text-based filtering |
|
||||
|
@ -21,6 +21,7 @@ import org.eclipse.jdt.annotation.Nullable;
|
||||
* The EventFilterConfiguration holds configuration for the Event Filter Item Type.
|
||||
*
|
||||
* @author Michael Wodniok - Initial contribution
|
||||
* @author Christian Heinemann - Introduction of 'datetimeMode'
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class EventFilterConfiguration {
|
||||
@ -37,6 +38,8 @@ public class EventFilterConfiguration {
|
||||
@Nullable
|
||||
public Boolean datetimeRound;
|
||||
@Nullable
|
||||
public String datetimeMode;
|
||||
@Nullable
|
||||
public String textEventField;
|
||||
@Nullable
|
||||
public String textEventValue;
|
||||
|
@ -30,6 +30,7 @@ import org.openhab.binding.icalendar.internal.handler.PullJob.CalendarUpdateList
|
||||
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.binding.icalendar.internal.logic.EventTimeFilter;
|
||||
import org.openhab.core.i18n.TimeZoneProvider;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
@ -57,6 +58,7 @@ import org.slf4j.LoggerFactory;
|
||||
*
|
||||
* @author Michael Wodniok - Initial Contribution
|
||||
* @author Michael Wodniok - Fixed subsecond search if rounding to unit
|
||||
* @author Christian Heinemann - Introduction of configuration 'datetimeMode'
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class EventFilterHandler extends BaseThingHandler implements CalendarUpdateListener {
|
||||
@ -278,10 +280,11 @@ public class EventFilterHandler extends BaseThingHandler implements CalendarUpda
|
||||
|
||||
Instant reference = Instant.now();
|
||||
TimeMultiplicator multiplicator = null;
|
||||
EventTextFilter filter = null;
|
||||
EventTextFilter eventTextFilter = null;
|
||||
int maxEvents;
|
||||
Instant begin = Instant.EPOCH;
|
||||
Instant end = Instant.ofEpochMilli(Long.MAX_VALUE);
|
||||
final EventTimeFilter eventTimeFilter;
|
||||
|
||||
try {
|
||||
String textFilterValue = config.textEventValue;
|
||||
@ -295,7 +298,7 @@ public class EventFilterHandler extends BaseThingHandler implements CalendarUpda
|
||||
EventTextFilter.Field textFilterField = EventTextFilter.Field.valueOf(textEventField);
|
||||
EventTextFilter.Type textFilterType = EventTextFilter.Type.valueOf(textValueType);
|
||||
|
||||
filter = new EventTextFilter(textFilterField, textFilterValue, textFilterType);
|
||||
eventTextFilter = new EventTextFilter(textFilterField, textFilterValue, textFilterType);
|
||||
} catch (IllegalArgumentException e2) {
|
||||
throw new ConfigBrokenException("textEventField or textValueType are not set properly.");
|
||||
}
|
||||
@ -352,13 +355,16 @@ public class EventFilterHandler extends BaseThingHandler implements CalendarUpda
|
||||
}
|
||||
end = reference.plusSeconds(datetimeEnd.longValue() * multiplicator.getMultiplier());
|
||||
}
|
||||
|
||||
eventTimeFilter = selectEventTimeFilterByConfigValue(config.datetimeMode);
|
||||
} catch (ConfigBrokenException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (resultChannels) {
|
||||
List<Event> results = cal.getFilteredEventsBetween(begin, end, filter, maxEvents);
|
||||
List<Event> results = cal.getFilteredEventsBetween(begin, end, eventTimeFilter, eventTextFilter,
|
||||
maxEvents);
|
||||
for (int position = 0; position < resultChannels.size(); position++) {
|
||||
ResultChannelSet channels = resultChannels.get(position);
|
||||
if (position < results.size()) {
|
||||
@ -393,4 +399,18 @@ public class EventFilterHandler extends BaseThingHandler implements CalendarUpda
|
||||
}
|
||||
updateFuture = scheduler.scheduleWithFixedDelay(this::updateStates, refreshTime, refreshTime, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
private EventTimeFilter selectEventTimeFilterByConfigValue(@Nullable String datetimeMode)
|
||||
throws ConfigBrokenException {
|
||||
if (datetimeMode == null) {
|
||||
return EventTimeFilter.searchByStart();
|
||||
}
|
||||
|
||||
return switch (datetimeMode) {
|
||||
case "START" -> EventTimeFilter.searchByStart();
|
||||
case "END" -> EventTimeFilter.searchByEnd();
|
||||
case "ACTIVE" -> EventTimeFilter.searchByActive();
|
||||
default -> throw new ConfigBrokenException("datetimeMode is not set properly.");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import org.eclipse.jdt.annotation.Nullable;
|
||||
* @author Michael Wodniok - Initial contribution
|
||||
* @author Andrew Fiddian-Green - Methods getJustBegunEvents() and getJustEndedEvents()
|
||||
* @author Michael Wodniok - Added getFilteredEventsBetween()
|
||||
* @author Christian Heinemann - Extension for the time-based filtering strategy
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractPresentableCalendar {
|
||||
@ -91,12 +92,28 @@ public abstract class AbstractPresentableCalendar {
|
||||
/**
|
||||
* 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 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 eventTimeFilter A filter for deciding whether an event falls into the time range.
|
||||
* @param eventTextFilter 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);
|
||||
public abstract List<Event> getFilteredEventsBetween(Instant begin, Instant end, EventTimeFilter eventTimeFilter,
|
||||
@Nullable EventTextFilter eventTextFilter, int maximumCount);
|
||||
|
||||
/**
|
||||
* Return a filtered List of events with a maximum count, ordered by start. Time based filtering is done by each
|
||||
* event's 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 eventTextFilter 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 List<Event> getFilteredEventsBetween(Instant begin, Instant end, @Nullable EventTextFilter eventTextFilter,
|
||||
int maximumCount) {
|
||||
return getFilteredEventsBetween(begin, end, EventTimeFilter.searchByStart(), eventTextFilter, maximumCount);
|
||||
}
|
||||
}
|
||||
|
@ -61,6 +61,7 @@ import biweekly.util.com.google.ical.compat.javautil.DateIterator;
|
||||
* @author Michael Wodniok - Added logic for events moved with "RECURRENCE-ID" (issue 9647)
|
||||
* @author Michael Wodniok - Extended logic for defined behavior with parallel current events
|
||||
* (issue 10808)
|
||||
* @author Christian Heinemann - Extension for the time-based filtering strategy
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
|
||||
@ -89,14 +90,14 @@ class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
|
||||
|
||||
@Override
|
||||
public List<Event> getJustBegunEvents(Instant frameBegin, Instant frameEnd) {
|
||||
return this.getVEventWPeriodsBetween(frameBegin, frameEnd, 0).stream().map(e -> e.toEvent())
|
||||
.collect(Collectors.toList());
|
||||
return this.getVEventWPeriodsBetween(frameBegin, frameEnd, 0, EventTimeFilter.searchByStart()).stream()
|
||||
.map(VEventWPeriod::toEvent).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Event> getJustEndedEvents(Instant frameBegin, Instant frameEnd) {
|
||||
return this.getVEventWPeriodsBetween(frameBegin, frameEnd, 0, true).stream().map(e -> e.toEvent())
|
||||
.collect(Collectors.toList());
|
||||
return this.getVEventWPeriodsBetween(frameBegin, frameEnd, 0, EventTimeFilter.searchByJustEnded()).stream()
|
||||
.map(VEventWPeriod::toEvent).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -142,22 +143,22 @@ class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Event> getFilteredEventsBetween(Instant begin, Instant end, @Nullable EventTextFilter filter,
|
||||
int maximumCount) {
|
||||
List<VEventWPeriod> candidates = this.getVEventWPeriodsBetween(begin, end, maximumCount);
|
||||
public List<Event> getFilteredEventsBetween(Instant begin, Instant end, EventTimeFilter eventTimeFilter,
|
||||
@Nullable EventTextFilter eventTextFilter, int maximumCount) {
|
||||
List<VEventWPeriod> candidates = this.getVEventWPeriodsBetween(begin, end, maximumCount, eventTimeFilter);
|
||||
final List<Event> results = new ArrayList<>(candidates.size());
|
||||
|
||||
if (filter != null) {
|
||||
if (eventTextFilter != null) {
|
||||
Pattern filterPattern;
|
||||
if (filter.type == Type.TEXT) {
|
||||
filterPattern = Pattern.compile(".*" + Pattern.quote(filter.value) + ".*",
|
||||
if (eventTextFilter.type == Type.TEXT) {
|
||||
filterPattern = Pattern.compile(".*" + Pattern.quote(eventTextFilter.value) + ".*",
|
||||
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
|
||||
} else {
|
||||
filterPattern = Pattern.compile(filter.value);
|
||||
filterPattern = Pattern.compile(eventTextFilter.value);
|
||||
}
|
||||
|
||||
Class<? extends TextProperty> propertyClass;
|
||||
switch (filter.field) {
|
||||
switch (eventTextFilter.field) {
|
||||
case SUMMARY:
|
||||
propertyClass = Summary.class;
|
||||
break;
|
||||
@ -199,28 +200,16 @@ class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds events which begin in the given frame.
|
||||
* Finds events which begin in the given frame by end time and date
|
||||
*
|
||||
* @param frameBegin Begin of the frame where to search events.
|
||||
* @param frameEnd End of the time frame where to search events.
|
||||
* @param maximumPerSeries Limit the results per series. Set to 0 for no limit.
|
||||
* @return All events which begin in the time frame.
|
||||
*/
|
||||
private List<VEventWPeriod> getVEventWPeriodsBetween(Instant frameBegin, Instant frameEnd, int maximumPerSeries) {
|
||||
return this.getVEventWPeriodsBetween(frameBegin, frameEnd, maximumPerSeries, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds events which begin in the given frame by end time and date
|
||||
*
|
||||
* @param frameBegin Begin of the frame where to search events.
|
||||
* @param frameEnd End of the time frame where to search events. The Instant is inclusive when searchByEnd is true.
|
||||
* @param maximumPerSeries Limit the results per series. Set to 0 for no limit.
|
||||
* @param searchByEnd Whether to search by begin of the event or by end.
|
||||
* @param eventTimeFilter Strategy that decides which events should be considered in the time frame.
|
||||
* @return All events which begin in the time frame.
|
||||
*/
|
||||
private List<VEventWPeriod> getVEventWPeriodsBetween(Instant frameBegin, Instant frameEnd, int maximumPerSeries,
|
||||
boolean searchByEnd) {
|
||||
EventTimeFilter eventTimeFilter) {
|
||||
final List<VEvent> positiveEvents = new ArrayList<>();
|
||||
final List<VEvent> negativeEvents = new ArrayList<>();
|
||||
classifyEvents(positiveEvents, negativeEvents);
|
||||
@ -232,17 +221,15 @@ class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
|
||||
if (duration == null) {
|
||||
duration = Duration.ZERO;
|
||||
}
|
||||
positiveBeginDates.advanceTo(Date.from(frameBegin.minus(searchByEnd ? duration : Duration.ZERO)));
|
||||
positiveBeginDates.advanceTo(Date.from(eventTimeFilter.searchFrom(frameBegin, duration)));
|
||||
int foundInSeries = 0;
|
||||
while (positiveBeginDates.hasNext()) {
|
||||
final Instant begInst = positiveBeginDates.next().toInstant();
|
||||
if ((!searchByEnd && (begInst.isAfter(frameEnd) || begInst.equals(frameEnd)))
|
||||
|| (searchByEnd && begInst.plus(duration).isAfter(frameEnd))) {
|
||||
if (eventTimeFilter.eventAfterFrame(frameEnd, begInst, duration)) {
|
||||
break;
|
||||
}
|
||||
// biweekly is not as precise as java.time. An exact check is required.
|
||||
if ((!searchByEnd && begInst.isBefore(frameBegin))
|
||||
|| (searchByEnd && begInst.plus(duration).isBefore(frameBegin))) {
|
||||
if (eventTimeFilter.eventBeforeFrame(frameBegin, begInst, duration)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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 java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Strategy for time based filtering.
|
||||
*
|
||||
* @author Christian Heinemann - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class EventTimeFilter {
|
||||
|
||||
/**
|
||||
* Creates the strategy to search for events that start in a specific time frame. The exact end of the time frame is
|
||||
* exclusive.
|
||||
*
|
||||
* @return The search strategy.
|
||||
*/
|
||||
public static EventTimeFilter searchByStart() {
|
||||
return new SearchByStart();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the strategy to search for events that end in a specific time frame. The exact end of the time frame is
|
||||
* inclusive.
|
||||
*
|
||||
* @return The search strategy.
|
||||
*/
|
||||
public static EventTimeFilter searchByEnd() {
|
||||
return new SearchByEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the strategy to search for events that are active in a specific time frame.
|
||||
* It finds the same events as {@link #searchByStart()} and {@link #searchByEnd()}, but additionally also events
|
||||
* that start before the time frame or end after.
|
||||
*
|
||||
* @return The search strategy.
|
||||
*/
|
||||
public static EventTimeFilter searchByActive() {
|
||||
return new SearchByActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the strategy to search for events that end in a specific time frame. The exact end of the time frame is
|
||||
* inclusive.
|
||||
* <p>
|
||||
* This is the strategy applied by {@link BiweeklyPresentableCalendar#getJustEndedEvents(Instant, Instant)}.
|
||||
* It is used here for backwards compatibility.
|
||||
* There are problems when an event ends exactly at the end of the search period.
|
||||
* Then the result is found for both this search period and one that begins immediately after it.
|
||||
* However, the usual behavior should be that if there are several non-overlapping search periods, an event will
|
||||
* only be found at most once.
|
||||
* That's why it is only offered here as non-public for internal use.
|
||||
*
|
||||
* @return The search strategy.
|
||||
*/
|
||||
static EventTimeFilter searchByJustEnded() {
|
||||
return new SearchByJustEnded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives a time to start searching for occurrences of a particular (recurring) event.
|
||||
*
|
||||
* @param frameStart Start of the frame where to search events.
|
||||
* @param eventDuration Duration of the event.
|
||||
* @return The time to start searching.
|
||||
*/
|
||||
public abstract Instant searchFrom(Instant frameStart, Duration eventDuration);
|
||||
|
||||
/**
|
||||
* Decides whether the relevant characteristic of an event occurrence is after the time frame. With the first hit,
|
||||
* no further occurrences of a recurring event are searched for.
|
||||
*
|
||||
* @param frameEnd End of the frame where to search events.
|
||||
* @param eventStart Start of the event occurrence.
|
||||
* @param eventDuration Duration of the event.
|
||||
* @return {@code true} if an occurrence of the event was found after the time frame, otherwise {@code false}.
|
||||
*/
|
||||
public abstract boolean eventAfterFrame(Instant frameEnd, Instant eventStart, Duration eventDuration);
|
||||
|
||||
/**
|
||||
* Decides whether the relevant characteristic of an event occurrence is before the time frame. Such occurrences are
|
||||
* ignored.
|
||||
*
|
||||
* @param frameStart Start of the frame where to search events.
|
||||
* @param eventStart Start of the event occurrence.
|
||||
* @param eventDuration Duration of the event.
|
||||
* @return {@code true} if an occurrence of the event was found before the time frame, otherwise {@code false}.
|
||||
*/
|
||||
public abstract boolean eventBeforeFrame(Instant frameStart, Instant eventStart, Duration eventDuration);
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
if (other == null) {
|
||||
return false;
|
||||
}
|
||||
return getClass().equals(other.getClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
|
||||
private static class SearchByStart extends EventTimeFilter {
|
||||
@Override
|
||||
public Instant searchFrom(Instant frameStart, Duration eventDuration) {
|
||||
return frameStart;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean eventAfterFrame(Instant frameEnd, Instant eventStart, Duration eventDuration) {
|
||||
return !eventStart.isBefore(frameEnd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean eventBeforeFrame(Instant frameStart, Instant eventStart, Duration eventDuration) {
|
||||
return eventStart.isBefore(frameStart);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SearchByEnd extends EventTimeFilter {
|
||||
@Override
|
||||
public Instant searchFrom(Instant frameStart, Duration eventDuration) {
|
||||
return frameStart.minus(eventDuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean eventAfterFrame(Instant frameEnd, Instant eventStart, Duration eventDuration) {
|
||||
return eventStart.plus(eventDuration).isAfter(frameEnd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean eventBeforeFrame(Instant frameStart, Instant eventStart, Duration eventDuration) {
|
||||
return !eventStart.plus(eventDuration).isAfter(frameStart);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SearchByActive extends EventTimeFilter {
|
||||
@Override
|
||||
public Instant searchFrom(Instant frameStart, Duration eventDuration) {
|
||||
return frameStart.minus(eventDuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean eventAfterFrame(Instant frameEnd, Instant eventStart, Duration eventDuration) {
|
||||
return !eventStart.isBefore(frameEnd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean eventBeforeFrame(Instant frameStart, Instant eventStart, Duration eventDuration) {
|
||||
return !eventStart.plus(eventDuration).isAfter(frameStart);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SearchByJustEnded extends EventTimeFilter {
|
||||
@Override
|
||||
public Instant searchFrom(Instant frameStart, Duration eventDuration) {
|
||||
return frameStart.minus(eventDuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean eventAfterFrame(Instant frameEnd, Instant eventStart, Duration eventDuration) {
|
||||
return eventStart.plus(eventDuration).isAfter(frameEnd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean eventBeforeFrame(Instant frameStart, Instant eventStart, Duration eventDuration) {
|
||||
return eventStart.plus(eventDuration).isBefore(frameStart);
|
||||
}
|
||||
}
|
||||
}
|
@ -30,6 +30,11 @@ thing-type.config.icalendar.calendar.username.label = User Name
|
||||
thing-type.config.icalendar.calendar.username.description = User name for fetching the calendar (usable in combination with password in HTTP basic auth)
|
||||
thing-type.config.icalendar.eventfilter.datetimeEnd.label = End
|
||||
thing-type.config.icalendar.eventfilter.datetimeEnd.description = End date/time amount to find events relative to "now" (exclusive)
|
||||
thing-type.config.icalendar.eventfilter.datetimeMode.label = Search mode
|
||||
thing-type.config.icalendar.eventfilter.datetimeMode.description = Defines which part of an event must fall within the search period between start and end
|
||||
thing-type.config.icalendar.eventfilter.datetimeMode.option.START = Events that begin in the period
|
||||
thing-type.config.icalendar.eventfilter.datetimeMode.option.ACTIVE = Events that are active at any phase in the period
|
||||
thing-type.config.icalendar.eventfilter.datetimeMode.option.END = Events that end in the period
|
||||
thing-type.config.icalendar.eventfilter.datetimeRound.label = Round to Date/Time unit
|
||||
thing-type.config.icalendar.eventfilter.datetimeRound.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)
|
||||
thing-type.config.icalendar.eventfilter.datetimeStart.label = Start
|
||||
|
@ -193,6 +193,17 @@
|
||||
<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="datetimeMode" type="text" groupName="datetime_based">
|
||||
<limitToOptions>true</limitToOptions>
|
||||
<options>
|
||||
<option value="START">Events that begin in the period</option>
|
||||
<option value="ACTIVE">Events that are active at any phase in the period</option>
|
||||
<option value="END">Events that end in the period</option>
|
||||
</options>
|
||||
<default>START</default>
|
||||
<label>Search Mode</label>
|
||||
<description>Defines which part of an event must fall within the search period between start and end</description>
|
||||
</parameter>
|
||||
<parameter name="textEventField" type="text" groupName="text_based">
|
||||
<label>Event Field</label>
|
||||
<description>iCal field to match</description>
|
||||
|
@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.isNull;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.time.ZoneId;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.openhab.binding.icalendar.internal.logic.AbstractPresentableCalendar;
|
||||
import org.openhab.binding.icalendar.internal.logic.EventTimeFilter;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.i18n.TimeZoneProvider;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
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.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
||||
import org.openhab.core.thing.binding.builder.BridgeBuilder;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
|
||||
|
||||
/**
|
||||
* Tests for {@link EventFilterHandler}.
|
||||
*
|
||||
* @author Christian Heinemann - Initial contribution
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@NonNullByDefault
|
||||
public class EventFilterHandlerTest {
|
||||
|
||||
private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProvider;
|
||||
private @Mock @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
|
||||
private @Mock @NonNullByDefault({}) ICalendarHandler iCalendarHandler;
|
||||
private @Mock @NonNullByDefault({}) AbstractPresentableCalendar calendar;
|
||||
|
||||
private @NonNullByDefault({}) EventFilterHandler eventFilterHandler;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.put("maxEvents", "1");
|
||||
configuration.put("datetimeStart", "0");
|
||||
configuration.put("datetimeEnd", "1");
|
||||
configuration.put("datetimeRound", "true");
|
||||
configuration.put("datetimeUnit", "DAY");
|
||||
|
||||
doReturn(ZoneId.of("UTC")).when(timeZoneProvider).getTimeZone();
|
||||
doReturn(calendar).when(iCalendarHandler).getRuntimeCalendar();
|
||||
|
||||
Bridge iCalendarBridge = BridgeBuilder.create(new ThingTypeUID("icalendar", "calendar"), "test").build();
|
||||
iCalendarBridge.setStatusInfo(ThingStatusInfoBuilder.create(ThingStatus.ONLINE).build());
|
||||
iCalendarBridge.setHandler(iCalendarHandler);
|
||||
|
||||
Thing eventFilterThing = ThingBuilder.create(new ThingTypeUID("icalendar", "eventfilter"), "test")
|
||||
.withBridge(iCalendarBridge.getUID()).withConfiguration(configuration).build();
|
||||
|
||||
eventFilterHandler = new EventFilterHandler(eventFilterThing, timeZoneProvider);
|
||||
eventFilterHandler.setCallback(thingHandlerCallback);
|
||||
|
||||
doReturn(iCalendarBridge).when(thingHandlerCallback).getBridge(iCalendarBridge.getUID());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchWithDefaultMode() {
|
||||
eventFilterHandler.getThing().getConfiguration().remove("datetimeMode");
|
||||
doCalendarUpdate();
|
||||
verify(calendar).getFilteredEventsBetween(any(), any(), eq(EventTimeFilter.searchByStart()), isNull(), eq(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchByStart() {
|
||||
eventFilterHandler.getThing().getConfiguration().put("datetimeMode", "START");
|
||||
doCalendarUpdate();
|
||||
verify(calendar).getFilteredEventsBetween(any(), any(), eq(EventTimeFilter.searchByStart()), isNull(), eq(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchByEnd() {
|
||||
eventFilterHandler.getThing().getConfiguration().put("datetimeMode", "END");
|
||||
doCalendarUpdate();
|
||||
verify(calendar).getFilteredEventsBetween(any(), any(), eq(EventTimeFilter.searchByEnd()), isNull(), eq(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchByActive() {
|
||||
eventFilterHandler.getThing().getConfiguration().put("datetimeMode", "ACTIVE");
|
||||
doCalendarUpdate();
|
||||
verify(calendar).getFilteredEventsBetween(any(), any(), eq(EventTimeFilter.searchByActive()), isNull(), eq(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidDatetimeModeConfigValue() {
|
||||
eventFilterHandler.getThing().getConfiguration().put("datetimeMode", "DUMMY");
|
||||
doCalendarUpdate();
|
||||
|
||||
ThingStatusInfo expectedThingStatusInfo = ThingStatusInfoBuilder.create(ThingStatus.OFFLINE)
|
||||
.withStatusDetail(ThingStatusDetail.CONFIGURATION_ERROR)
|
||||
.withDescription("datetimeMode is not set properly.").build();
|
||||
verify(thingHandlerCallback).statusUpdated(eventFilterHandler.getThing(), expectedThingStatusInfo);
|
||||
}
|
||||
|
||||
private void doCalendarUpdate() {
|
||||
eventFilterHandler.initialize();
|
||||
eventFilterHandler.getThing().setStatusInfo(ThingStatusInfoBuilder.create(ThingStatus.ONLINE).build());
|
||||
eventFilterHandler.onCalendarUpdated();
|
||||
}
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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 static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.emptyCollectionOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests of time-based filtering when using {@link EventTimeFilter#searchByActive()} in {@link
|
||||
* BiweeklyPresentableCalendar#getFilteredEventsBetween(Instant, Instant, EventTimeFilter, EventTextFilter, int)}
|
||||
* with multi-day events.
|
||||
*
|
||||
* @author Christian Heinemann - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MultiDayEventSearchByActiveTest {
|
||||
|
||||
private @NonNullByDefault({}) AbstractPresentableCalendar calendar;
|
||||
private final EventTimeFilter eventTimeFilter = EventTimeFilter.searchByActive();
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() throws IOException, CalendarException {
|
||||
calendar = new BiweeklyPresentableCalendar(new FileInputStream("src/test/resources/test-multiday.ics"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eventWithTime() {
|
||||
Event expectedFilteredEvent = new Event("Multi-day test event with time", Instant.parse("2023-12-05T09:00:00Z"),
|
||||
Instant.parse("2023-12-07T15:00:00Z"), "");
|
||||
|
||||
assertThat("Day before event starts",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-04T00:00:00Z"),
|
||||
Instant.parse("2023-12-05T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Hour before event starts",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-05T08:00:00Z"),
|
||||
Instant.parse("2023-12-05T09:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Hour when event starts",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-05T09:00:00Z"),
|
||||
Instant.parse("2023-12-05T10:00:00Z"), eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Day 1 when event starts",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-05T00:00:00Z"),
|
||||
Instant.parse("2023-12-06T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Day 2 when event is still active",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-06T00:00:00Z"),
|
||||
Instant.parse("2023-12-07T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Day 3 when event ends",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-07T00:00:00Z"),
|
||||
Instant.parse("2023-12-08T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Hour when event ends",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-07T14:00:00Z"),
|
||||
Instant.parse("2023-12-05T15:00:00Z"), eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Hour after event ends",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-07T15:00:00Z"),
|
||||
Instant.parse("2023-12-05T16:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day after event ends",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-08T00:00:00Z"),
|
||||
Instant.parse("2023-12-09T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eventWithoutTime() {
|
||||
Event expectedFilteredEvent = new Event("Multi-day test event without time", localDateAsInstant("2023-12-12"),
|
||||
localDateAsInstant("2023-12-15"), "");
|
||||
|
||||
assertThat("Day before event starts", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-11"), localDateAsInstant("2023-12-12"),
|
||||
eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day 1 when event starts", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-12"), localDateAsInstant("2023-12-13"),
|
||||
eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Day 2 when event is still active", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-13"), localDateAsInstant("2023-12-14"),
|
||||
eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Day 3 when event ends", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-14"), localDateAsInstant("2023-12-15"),
|
||||
eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Day after event ends", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-15"), localDateAsInstant("2023-12-16"),
|
||||
eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
}
|
||||
|
||||
private Instant localDateAsInstant(CharSequence text) {
|
||||
return LocalDate.parse(text).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
}
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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 static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.emptyCollectionOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests of time-based filtering when using {@link EventTimeFilter#searchByEnd()} in {@link
|
||||
* BiweeklyPresentableCalendar#getFilteredEventsBetween(Instant, Instant, EventTimeFilter, EventTextFilter, int)}
|
||||
* with multi-day events.
|
||||
*
|
||||
* @author Christian Heinemann - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MultiDayEventSearchByEndTest {
|
||||
|
||||
private @NonNullByDefault({}) AbstractPresentableCalendar calendar;
|
||||
private final EventTimeFilter eventTimeFilter = EventTimeFilter.searchByEnd();
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() throws IOException, CalendarException {
|
||||
calendar = new BiweeklyPresentableCalendar(new FileInputStream("src/test/resources/test-multiday.ics"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eventWithTime() {
|
||||
Event expectedFilteredEvent = new Event("Multi-day test event with time", Instant.parse("2023-12-05T09:00:00Z"),
|
||||
Instant.parse("2023-12-07T15:00:00Z"), "");
|
||||
|
||||
assertThat("Day before event starts",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-04T00:00:00Z"),
|
||||
Instant.parse("2023-12-05T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day 1 when event starts",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-05T00:00:00Z"),
|
||||
Instant.parse("2023-12-06T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day 2 when event is still active",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-06T00:00:00Z"),
|
||||
Instant.parse("2023-12-07T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day 3 when event ends",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-07T00:00:00Z"),
|
||||
Instant.parse("2023-12-08T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Hour when event ends",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-07T14:00:00Z"),
|
||||
Instant.parse("2023-12-07T15:00:00Z"), eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Hour after event ends",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-07T15:00:00Z"),
|
||||
Instant.parse("2023-12-07T16:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day after event ends",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-08T00:00:00Z"),
|
||||
Instant.parse("2023-12-09T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eventWithoutTime() {
|
||||
Event expectedFilteredEvent = new Event("Multi-day test event without time", localDateAsInstant("2023-12-12"),
|
||||
localDateAsInstant("2023-12-15"), "");
|
||||
|
||||
assertThat("Day before event starts", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-11"), localDateAsInstant("2023-12-12"),
|
||||
eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day 1 when event starts", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-12"), localDateAsInstant("2023-12-13"),
|
||||
eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day 2 when event is still active", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-13"), localDateAsInstant("2023-12-14"),
|
||||
eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day 3 when event ends", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-14"), localDateAsInstant("2023-12-15"),
|
||||
eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Day after event ends", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-15"), localDateAsInstant("2023-12-16"),
|
||||
eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
}
|
||||
|
||||
private Instant localDateAsInstant(CharSequence text) {
|
||||
return LocalDate.parse(text).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
}
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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 static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.emptyCollectionOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests of time-based filtering when using {@link EventTimeFilter#searchByJustEnded()} in {@link
|
||||
* BiweeklyPresentableCalendar#getFilteredEventsBetween(Instant, Instant, EventTimeFilter, EventTextFilter, int)}
|
||||
* with multi-day events.
|
||||
*
|
||||
* @author Christian Heinemann - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MultiDayEventSearchByJustEndedTest {
|
||||
|
||||
private @NonNullByDefault({}) AbstractPresentableCalendar calendar;
|
||||
private final EventTimeFilter eventTimeFilter = EventTimeFilter.searchByJustEnded();
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() throws IOException, CalendarException {
|
||||
calendar = new BiweeklyPresentableCalendar(new FileInputStream("src/test/resources/test-multiday.ics"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eventWithTime() {
|
||||
Event expectedFilteredEvent = new Event("Multi-day test event with time", Instant.parse("2023-12-05T09:00:00Z"),
|
||||
Instant.parse("2023-12-07T15:00:00Z"), "");
|
||||
|
||||
assertThat("Day before event starts",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-04T00:00:00Z"),
|
||||
Instant.parse("2023-12-05T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day 1 when event starts",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-05T00:00:00Z"),
|
||||
Instant.parse("2023-12-06T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day 2 when event is still active",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-06T00:00:00Z"),
|
||||
Instant.parse("2023-12-07T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day 3 when event ends",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-07T00:00:00Z"),
|
||||
Instant.parse("2023-12-08T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Hour when event ends",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-07T14:00:00Z"),
|
||||
Instant.parse("2023-12-07T15:00:00Z"), eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Hour after event ends",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-07T15:00:00Z"),
|
||||
Instant.parse("2023-12-07T16:00:00Z"), eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent)); // event found again!!
|
||||
|
||||
assertThat("Day after event ends",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-08T00:00:00Z"),
|
||||
Instant.parse("2023-12-09T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eventWithoutTime() {
|
||||
Event expectedFilteredEvent = new Event("Multi-day test event without time", localDateAsInstant("2023-12-12"),
|
||||
localDateAsInstant("2023-12-15"), "");
|
||||
|
||||
assertThat("Day before event starts", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-11"), localDateAsInstant("2023-12-12"),
|
||||
eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day 1 when event starts", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-12"), localDateAsInstant("2023-12-13"),
|
||||
eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day 2 when event is still active", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-13"), localDateAsInstant("2023-12-14"),
|
||||
eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day 3 when event ends", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-14"), localDateAsInstant("2023-12-15"),
|
||||
eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Day after event ends", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-15"), localDateAsInstant("2023-12-16"),
|
||||
eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent)); // event found again!!
|
||||
}
|
||||
|
||||
private Instant localDateAsInstant(CharSequence text) {
|
||||
return LocalDate.parse(text).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
}
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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 static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.emptyCollectionOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests of time-based filtering when using {@link EventTimeFilter#searchByStart()} in {@link
|
||||
* BiweeklyPresentableCalendar#getFilteredEventsBetween(Instant, Instant, EventTimeFilter, EventTextFilter, int)}
|
||||
* with multi-day events.
|
||||
*
|
||||
* @author Christian Heinemann - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class MultiDayEventSearchByStartTest {
|
||||
|
||||
private @NonNullByDefault({}) AbstractPresentableCalendar calendar;
|
||||
private final EventTimeFilter eventTimeFilter = EventTimeFilter.searchByStart();
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() throws IOException, CalendarException {
|
||||
calendar = new BiweeklyPresentableCalendar(new FileInputStream("src/test/resources/test-multiday.ics"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eventWithTime() {
|
||||
Event expectedFilteredEvent = new Event("Multi-day test event with time", Instant.parse("2023-12-05T09:00:00Z"),
|
||||
Instant.parse("2023-12-07T15:00:00Z"), "");
|
||||
|
||||
assertThat("Day before event starts",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-04T00:00:00Z"),
|
||||
Instant.parse("2023-12-05T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Hour before event starts",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-05T08:00:00Z"),
|
||||
Instant.parse("2023-12-05T09:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Hour when event starts",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-05T09:00:00Z"),
|
||||
Instant.parse("2023-12-05T10:00:00Z"), eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Day 1 when event starts",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-05T00:00:00Z"),
|
||||
Instant.parse("2023-12-06T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Day 2 when event is still active",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-06T00:00:00Z"),
|
||||
Instant.parse("2023-12-07T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day 3 when event ends",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-07T00:00:00Z"),
|
||||
Instant.parse("2023-12-08T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day after event ends",
|
||||
calendar.getFilteredEventsBetween(Instant.parse("2023-12-08T00:00:00Z"),
|
||||
Instant.parse("2023-12-09T00:00:00Z"), eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eventWithoutTime() {
|
||||
Event expectedFilteredEvent = new Event("Multi-day test event without time", localDateAsInstant("2023-12-12"),
|
||||
localDateAsInstant("2023-12-15"), "");
|
||||
|
||||
assertThat("Day before event starts", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-11"), localDateAsInstant("2023-12-12"),
|
||||
eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day 1 when event starts", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-12"), localDateAsInstant("2023-12-13"),
|
||||
eventTimeFilter, null, 1),
|
||||
contains(expectedFilteredEvent));
|
||||
|
||||
assertThat("Day 2 when event is still active", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-13"), localDateAsInstant("2023-12-14"),
|
||||
eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day 3 when event ends", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-14"), localDateAsInstant("2023-12-15"),
|
||||
eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
|
||||
assertThat("Day after event ends", //
|
||||
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-15"), localDateAsInstant("2023-12-16"),
|
||||
eventTimeFilter, null, 1),
|
||||
is(emptyCollectionOf(Event.class)));
|
||||
}
|
||||
|
||||
private Instant localDateAsInstant(CharSequence text) {
|
||||
return LocalDate.parse(text).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-WR-TIMEZONE:UTC
|
||||
BEGIN:VEVENT
|
||||
DTSTART;VALUE=DATE:20231212
|
||||
DTEND;VALUE=DATE:20231215
|
||||
SUMMARY:Multi-day test event without time
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTART:20231205T090000Z
|
||||
DTEND:20231207T150000Z
|
||||
SUMMARY:Multi-day test event with time
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
Loading…
Reference in New Issue
Block a user