Skip to content

Commit

Permalink
[icalendar] Add configuration for the behavior of the time-based even…
Browse files Browse the repository at this point in the history
…t 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 <[email protected]>
Co-authored-by: Leo Siepel <[email protected]>
  • Loading branch information
cheinema and lsiepel authored Nov 6, 2024
1 parent 41c8c45 commit f37f39c
Show file tree
Hide file tree
Showing 14 changed files with 923 additions and 41 deletions.
4 changes: 3 additions & 1 deletion bundles/org.openhab.binding.icalendar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* 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 {
Expand All @@ -37,6 +38,8 @@ public class EventFilterConfiguration {
@Nullable
public Boolean datetimeRound;
@Nullable
public String datetimeMode;
@Nullable
public String textEventField;
@Nullable
public String textEventValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
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;
Expand Down Expand Up @@ -57,6 +58,7 @@
*
* @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 {
Expand Down Expand Up @@ -278,10 +280,11 @@ private void updateStates() {

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;
Expand All @@ -295,7 +298,7 @@ private void updateStates() {
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.");
}
Expand Down Expand Up @@ -352,13 +355,16 @@ private void updateStates() {
}
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()) {
Expand Down Expand Up @@ -393,4 +399,18 @@ private void updateStates() {
}
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.");
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* @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 {
Expand Down Expand Up @@ -91,12 +92,28 @@ public static AbstractPresentableCalendar create(InputStream calendarStream) thr
/**
* 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
* @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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -142,22 +143,22 @@ public boolean isEventPresent(Instant instant) {
}

@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;
Expand Down Expand Up @@ -198,29 +199,17 @@ public List<Event> getFilteredEventsBetween(Instant begin, Instant end, @Nullabl
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.
* @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 frameEnd End of the time frame where to search events.
* @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);
Expand All @@ -232,17 +221,15 @@ private List<VEventWPeriod> getVEventWPeriodsBetween(Instant frameBegin, Instant
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;
}

Expand Down
Loading

0 comments on commit f37f39c

Please sign in to comment.