From f37f39c03d0a65c174b05665f1c4ebe56f11e8c5 Mon Sep 17 00:00:00 2001 From: Christian Heinemann Date: Wed, 6 Nov 2024 16:52:08 +0100 Subject: [PATCH] [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 Co-authored-by: Leo Siepel --- .../org.openhab.binding.icalendar/README.md | 4 +- .../config/EventFilterConfiguration.java | 3 + .../internal/handler/EventFilterHandler.java | 26 ++- .../logic/AbstractPresentableCalendar.java | 27 ++- .../logic/BiweeklyPresentableCalendar.java | 51 ++--- .../internal/logic/EventTimeFilter.java | 189 ++++++++++++++++++ .../OH-INF/i18n/icalendar.properties | 5 + .../resources/OH-INF/thing/thing-types.xml | 11 + .../handler/EventFilterHandlerTest.java | 129 ++++++++++++ .../MultiDayEventSearchByActiveTest.java | 133 ++++++++++++ .../logic/MultiDayEventSearchByEndTest.java | 123 ++++++++++++ .../MultiDayEventSearchByJustEndedTest.java | 123 ++++++++++++ .../logic/MultiDayEventSearchByStartTest.java | 123 ++++++++++++ .../src/test/resources/test-multiday.ics | 17 ++ 14 files changed, 923 insertions(+), 41 deletions(-) create mode 100644 bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/logic/EventTimeFilter.java create mode 100644 bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/handler/EventFilterHandlerTest.java create mode 100644 bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/MultiDayEventSearchByActiveTest.java create mode 100644 bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/MultiDayEventSearchByEndTest.java create mode 100644 bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/MultiDayEventSearchByJustEndedTest.java create mode 100644 bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/MultiDayEventSearchByStartTest.java create mode 100644 bundles/org.openhab.binding.icalendar/src/test/resources/test-multiday.ics diff --git a/bundles/org.openhab.binding.icalendar/README.md b/bundles/org.openhab.binding.icalendar/README.md index f355cb9f4e0a9..0d3189e84f048 100644 --- a/bundles/org.openhab.binding.icalendar/README.md +++ b/bundles/org.openhab.binding.icalendar/README.md @@ -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 | diff --git a/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/config/EventFilterConfiguration.java b/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/config/EventFilterConfiguration.java index 8b030ea1bf67b..c3bd5e428d03c 100644 --- a/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/config/EventFilterConfiguration.java +++ b/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/config/EventFilterConfiguration.java @@ -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 { @@ -37,6 +38,8 @@ public class EventFilterConfiguration { @Nullable public Boolean datetimeRound; @Nullable + public String datetimeMode; + @Nullable public String textEventField; @Nullable public String textEventValue; diff --git a/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/handler/EventFilterHandler.java b/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/handler/EventFilterHandler.java index e199351d54a9a..db4c3c0b14599 100644 --- a/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/handler/EventFilterHandler.java +++ b/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/handler/EventFilterHandler.java @@ -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; @@ -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 { @@ -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; @@ -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."); } @@ -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 results = cal.getFilteredEventsBetween(begin, end, filter, maxEvents); + List 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 @@ 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."); + }; + } } diff --git a/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/logic/AbstractPresentableCalendar.java b/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/logic/AbstractPresentableCalendar.java index 9c8afbe7c156e..4f9572e3c9077 100644 --- a/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/logic/AbstractPresentableCalendar.java +++ b/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/logic/AbstractPresentableCalendar.java @@ -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 { @@ -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 getFilteredEventsBetween(Instant begin, Instant end, @Nullable EventTextFilter filter, - int maximumCount); + public abstract List 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 getFilteredEventsBetween(Instant begin, Instant end, @Nullable EventTextFilter eventTextFilter, + int maximumCount) { + return getFilteredEventsBetween(begin, end, EventTimeFilter.searchByStart(), eventTextFilter, maximumCount); + } } diff --git a/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/logic/BiweeklyPresentableCalendar.java b/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/logic/BiweeklyPresentableCalendar.java index d491d3fc986f2..431380ccbf86c 100644 --- a/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/logic/BiweeklyPresentableCalendar.java +++ b/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/logic/BiweeklyPresentableCalendar.java @@ -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 { @@ -89,14 +90,14 @@ class BiweeklyPresentableCalendar extends AbstractPresentableCalendar { @Override public List 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 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 @@ public boolean isEventPresent(Instant instant) { } @Override - public List getFilteredEventsBetween(Instant begin, Instant end, @Nullable EventTextFilter filter, - int maximumCount) { - List candidates = this.getVEventWPeriodsBetween(begin, end, maximumCount); + public List getFilteredEventsBetween(Instant begin, Instant end, EventTimeFilter eventTimeFilter, + @Nullable EventTextFilter eventTextFilter, int maximumCount) { + List candidates = this.getVEventWPeriodsBetween(begin, end, maximumCount, eventTimeFilter); final List 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 propertyClass; - switch (filter.field) { + switch (eventTextFilter.field) { case SUMMARY: propertyClass = Summary.class; break; @@ -198,29 +199,17 @@ public List 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 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 getVEventWPeriodsBetween(Instant frameBegin, Instant frameEnd, int maximumPerSeries, - boolean searchByEnd) { + EventTimeFilter eventTimeFilter) { final List positiveEvents = new ArrayList<>(); final List negativeEvents = new ArrayList<>(); classifyEvents(positiveEvents, negativeEvents); @@ -232,17 +221,15 @@ private List 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; } diff --git a/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/logic/EventTimeFilter.java b/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/logic/EventTimeFilter.java new file mode 100644 index 0000000000000..f234efe627ccd --- /dev/null +++ b/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/logic/EventTimeFilter.java @@ -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. + *

+ * 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); + } + } +} diff --git a/bundles/org.openhab.binding.icalendar/src/main/resources/OH-INF/i18n/icalendar.properties b/bundles/org.openhab.binding.icalendar/src/main/resources/OH-INF/i18n/icalendar.properties index 34675a0924a89..042bb499c9c47 100644 --- a/bundles/org.openhab.binding.icalendar/src/main/resources/OH-INF/i18n/icalendar.properties +++ b/bundles/org.openhab.binding.icalendar/src/main/resources/OH-INF/i18n/icalendar.properties @@ -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 diff --git a/bundles/org.openhab.binding.icalendar/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.icalendar/src/main/resources/OH-INF/thing/thing-types.xml index f7fbdd86cf8b7..eaa980460f7ed 100644 --- a/bundles/org.openhab.binding.icalendar/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.icalendar/src/main/resources/OH-INF/thing/thing-types.xml @@ -193,6 +193,17 @@ 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) + + true + + + + + + START + + Defines which part of an event must fall within the search period between start and end + iCal field to match diff --git a/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/handler/EventFilterHandlerTest.java b/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/handler/EventFilterHandlerTest.java new file mode 100644 index 0000000000000..9ad3f31c7f8dc --- /dev/null +++ b/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/handler/EventFilterHandlerTest.java @@ -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(); + } +} diff --git a/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/MultiDayEventSearchByActiveTest.java b/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/MultiDayEventSearchByActiveTest.java new file mode 100644 index 0000000000000..3c6fa7387df88 --- /dev/null +++ b/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/MultiDayEventSearchByActiveTest.java @@ -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(); + } +} diff --git a/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/MultiDayEventSearchByEndTest.java b/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/MultiDayEventSearchByEndTest.java new file mode 100644 index 0000000000000..9e19e32cd65bd --- /dev/null +++ b/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/MultiDayEventSearchByEndTest.java @@ -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(); + } +} diff --git a/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/MultiDayEventSearchByJustEndedTest.java b/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/MultiDayEventSearchByJustEndedTest.java new file mode 100644 index 0000000000000..507091f712051 --- /dev/null +++ b/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/MultiDayEventSearchByJustEndedTest.java @@ -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(); + } +} diff --git a/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/MultiDayEventSearchByStartTest.java b/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/MultiDayEventSearchByStartTest.java new file mode 100644 index 0000000000000..6f8d1b15f6927 --- /dev/null +++ b/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/MultiDayEventSearchByStartTest.java @@ -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(); + } +} diff --git a/bundles/org.openhab.binding.icalendar/src/test/resources/test-multiday.ics b/bundles/org.openhab.binding.icalendar/src/test/resources/test-multiday.ics new file mode 100644 index 0000000000000..cbc39f65282f8 --- /dev/null +++ b/bundles/org.openhab.binding.icalendar/src/test/resources/test-multiday.ics @@ -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 \ No newline at end of file