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

This commit fixes #8022.

Signed-off-by: Michael Wodniok <michi@noorganization.org>
This commit is contained in:
Michael Wodniok 2020-10-24 22:35:07 +02:00 committed by GitHub
parent fa9e3db34b
commit 7312890d44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1058 additions and 78 deletions

View File

@ -6,12 +6,16 @@ Furthermore it is possible to embed `command tags` in the calendar event descrip
## Supported Things
The only thing type is the calendar.
It is based on a single iCalendar file.
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.
## Thing Configuration
### Configuration for `calendar`
Each `calendar` thing requires the following configuration parameters:
| parameter name | description | optional |
@ -23,20 +27,60 @@ Each `calendar` thing requires the following configuration parameters:
| `maxSize` | The maximum size of the iCal-file in Mebibytes. | mandatory (default available) |
| `authorizationCode` | The authorization code to permit the execution of embedded command tags. If set, the binding checks that the authorization code in the command tag matches before executing any commands. | optional |
### Configuration for `eventfilter`
Each `eventfilter` thing requires a bridge of type `calendar` and has following configuration options:
| parameter name | description | optional |
|------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------|
| `maxEvents` | The count of expected results. | mandatory |
| `refreshTime` | The frequency in minutes the channels get refreshed. | mandatory (default available) |
| `datetimeUnit` | A unit for time settings in this filter. Valid values: `MINUTE`, `HOUR`, `DAY` and `WEEK`. | optional (required for time-based filtering) |
| `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 |
| `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), `REGEX` (field must match value, completely, dot matches all, case insensetive). | optional/required for text-based filtering |
## Channels
The channels describe the current and the next forthcoming event.
### Channels for `calendar`
The channels of `calendar` describe the current and the next forthcoming event.
They are all read-only.
| Channel | Type | Description |
|-------------------|-----------|--------------------------------------------------------------------------------|
| current_presence | Switch | Current presence of an event, `ON` if there is currently an event, `OFF` otherwise |
| current_title | String | Title of a currently present event |
| current_start | DateTime | Start of a currently present event |
| current_end | DateTime | End of a currently present event |
| next_title | String | Title of the next event |
| next_start | DateTime | Start of the next event |
| next_end | DateTime | End of the next event |
| Channel | Type | Description |
|-------------------|-----------|-------------------------------------------------------------------------------------|
| current_presence | Switch | Current presence of an event, `ON` if there is currently an event, `OFF` otherwise |
| current_title | String | Title of a currently present event |
| current_start | DateTime | Start of a currently present event |
| current_end | DateTime | End of a currently present event |
| next_title | String | Title of the next event |
| next_start | DateTime | Start of the next event |
| next_end | DateTime | End of the next event |
### Channels for `eventfilter`
The channels of `eventfilter` are generated using following scheme, all are read-only.
| Channel-scheme | Type | Description |
|---------------------|-----------|------------------------|
| `result_<no>#begin` | DateTime | The begin of an event |
| `result_<no>#end` | DateTime | The end of an event |
| `result_<no>#title` | String | The title of an event |
The scheme replaces `<no>` by the results index, beginning at `0`. An `eventfilter` having `maxEvents` set to 3 will have following channels:
* `result_0#begin`
* `result_0#end`
* `result_0#title`
* `result_1#begin`
* `result_1#end`
* `result_1#title`
* `result_2#begin`
* `result_2#end`
* `result_2#title`
## Command Tags
@ -76,16 +120,19 @@ The `Authorization_Code` may *optionally* be used as follows:
All required information must be provided in the thing definition, either via UI or in the `.things` file..
```
Thing icalendar:calendar:deadbeef "My calendar" @ "Internet" [ url="http://example.org/calendar.ical", refreshTime=60 ]
Bridge icalendar:calendar:deadbeef "My calendar" @ "Internet" [ url="http://example.org/calendar.ical", refreshTime=60 ]
Thing icalendar:eventfilter:feedd0d0 "Tomorrows events" (icalendar:calendar:deadbeef) [ maxEvents=1, datetimeUnit="DAY", datetimeStart=1, datetimeEnd=2, datetimeRound=true ]
```
Link the channels as usual to items:
```
String current_event_name "current event [%s]" <calendar> { channel="icalendar:calendar:deadbeef:current_title" }
DateTime current_event_until "current until [%1$tT, %1$tY-%1$tm-%1$td]" <calendar> { channel="icalendar:calendar:deadbeef:current_end" }
String next_event_name "next event [%s]" <calendar> { channel="icalendar:calendar:deadbeef:next_title" }
DateTime next_event_at "next at [%1$tT, %1$tY-%1$tm-%1$td]" <calendar> { channel="icalendar:calendar:deadbeef:next_start" }
String current_event_name "current event [%s]" <calendar> { channel="icalendar:calendar:deadbeef:current_title" }
DateTime current_event_until "current until [%1$tT, %1$tY-%1$tm-%1$td]" <calendar> { channel="icalendar:calendar:deadbeef:current_end" }
String next_event_name "next event [%s]" <calendar> { channel="icalendar:calendar:deadbeef:next_title" }
DateTime next_event_at "next at [%1$tT, %1$tY-%1$tm-%1$td]" <calendar> { channel="icalendar:calendar:deadbeef:next_start" }
String first_event_name_tomorrow "first event [%s]" <calendar> { channel="icalendar:eventfilter:feedd0d0:event_0#title" }
DateTime first_event_at_tomorrow "first at [%1$tT, %1$tY-%1$tm-%1$td]" <calendar> { channel="icalendar:eventfilter:feedd0d0:event_0#begin" }
```
Sitemap just showing the current event and the beginning of the next:
@ -98,6 +145,10 @@ sitemap local label="My Calendar Sitemap" {
Text item=next_event_name label="next event [%s]"
Text item=next_event_at label="next at [%1$tT, %1$tY-%1$tm-%1$td]"
}
Frame label="tomorrow" {
Text item=first_event_name_tomorrow
Text item=first_event_at_tomorrow
}
}
```
@ -114,3 +165,7 @@ Command tags in a calendar event (in the case that configuration parameter `auth
BEGIN:Calendar_Test_Switch:ON
END:Calendar_Test_Switch:OFF
```
## Breaking changes
In OH3 `calendar` was changed from Thing to Bridge. You need to recreate calendars (or replace `Thing` by `Bridge` in your `.things` file).

View File

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

View File

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

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.icalendar.internal.config;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The EventFilterConfiguration holds configuration for the Event Filter Item Type.
*
* @author Michael Wodniok - Initial contribution
*/
@NonNullByDefault
public class EventFilterConfiguration {
@Nullable
public BigDecimal maxEvents;
@Nullable
public BigDecimal refreshTime;
@Nullable
public String datetimeUnit;
@Nullable
public BigDecimal datetimeStart;
@Nullable
public BigDecimal datetimeEnd;
@Nullable
public Boolean datetimeRound;
@Nullable
public String textEventField;
@Nullable
public String textEventValue;
@Nullable
public String textValueType;
}

View File

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

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.icalendar.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception or semantically describe configuration errors. Message is meant to be shown to the user.
*
* @author Michael Wodniok - Initial contribution
*/
@NonNullByDefault
public class ConfigBrokenException extends Exception {
private static final long serialVersionUID = -3805312008429711152L;
public ConfigBrokenException(String message) {
super(message);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.icalendar.internal.logic;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Transport class for a simple text filter.
*
* @author Michael Wodniok - Initial contribution
*/
@NonNullByDefault
public class EventTextFilter {
public static enum Type {
TEXT,
REGEX
}
public static enum Field {
SUMMARY,
DESCRIPTION,
COMMENT,
CONTACT,
LOCATION
}
public Field field;
public String value;
public Type type;
public EventTextFilter(Field field, String value, Type type) {
this.field = field;
this.value = value;
this.type = type;
}
}

View File

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

View File

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

View File

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