added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.icalendar</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,33 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab2-addons
== Third Party Dependencies
Third Party Dependencies are used inside the code of this project, but not
redistributed. Instead, Openhab retrieves the dependencies directly from
public sources.
=== net.sf.biweekly:biweekly
Author: Michael Angstadt, Apache Software Foundation, Google Inc.
URL: https://github.com/mangstadt/biweekly
License: BSD 2-Clause License, Apache License Version 2.0
=== com.github.mangstadt:vinnie
Author: Michael Angstadt, Apache Software Foundation
URL: https://github.com/mangstadt/vinnie
License: BSD 2-Clause License, Apache License Version 2.0
=== com.fasterxml.jackson.core:jackson-core
Author: FasterXML LLC
URL: https://github.com/FasterXML/jackson-core
License: Apache License Version 2.0

View File

@@ -0,0 +1,116 @@
# iCalendar Binding
This binding is intended to use a web-based iCal calendar as an event trigger or presence switch.
It implements several channels that indicate the current calendar event and upcoming calendar events.
Furthermore it is possible to embed `command tags` in the calendar event description in order to issue commands directly to other items in the system, without the need to create special rules.
## Supported Things
The only thing type is the calendar.
It is based on a single iCalendar file.
There can be multiple things having different properties representing different calendars.
## Thing Configuration
Each `calendar` thing requires the following configuration parameters:
| parameter name | description | optional |
|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------|
| `url` | The URL of an iCal Calendar to be used as a source of events. | mandatory |
| `refreshTime` | The frequency in minutes with which the calendar gets refreshed from the source. | mandatory |
| `username` | The username for pulling the calendar. If set, the binding pulls the calendar using basic auth. Only valid in combination with `password`. | optional |
| `password` | The password for pulling the calendar. If set, the binding pulls the calendar using basic auth. Only valid in combination with `username`. | optional |
| `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 |
## Channels
The channels 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 |
## Command Tags
Each calendar event may include one or more command tags in its description text.
These command tags are used to issue commands directly to other items in the system when the event begins or ends.
A command tag must consist of at least three fields.
A fourth field is optional.
The syntax is as follows:
```
BEGIN:Item_Name:New_State_Value
BEGIN:Item_Name:New_State_Value:Authorization_Code
END:Item_Name:New_State_Value
END:Item_Name:New_State_Value:Authorization_Code
```
The first field **must** be either `BEGIN` or `END`.
If it is `BEGIN` then the command will be executed at the beginning of the calendar event.
If it is `END` then the command will be executed at the end of the calendar event.
A calendar event may contain multiple `BEGIN` or `END` tags.
If an event contains both `BEGIN` and `END` tags, the item is (say) to be turned `ON` at the beginning of an event and turned `OFF` again at the end of the event.
The `Item_Name` field must be the name of an item.
The `New_State_Value` is the state value that will be sent to the item.
It must be a value which is compatible with the item type.
See openHAB Core definitions for [command types](https://www.openhab.org/docs/concepts/items.html#state-and-command-type-formatting) for valid types and formats.
The `Authorization_Code` may *optionally* be used as follows:
- When the thing configuration parameter `authorizationCode` is not blank, the binding will compare the `Authorization_Code` field against the `authorizationCode` configuration parameter, and it will only execute the command if the two strings are the same.
- When the thing configuration parameter `authorizationCode` is blank, the binding will NOT check this `Authorization_Code` field, and so it will always execute the command.
## Full Example
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 ]
```
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" }
```
Sitemap just showing the current event and the beginning of the next:
```
sitemap local label="My Calendar Sitemap" {
Frame label="events" {
Text item=current_event_name label="current event [%s]"
Text item=current_event_until label="current until [%1$tT, %1$tY-%1$tm-%1$td]"
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]"
}
}
```
Command tags in a calendar event (in the case that configuration parameter `authorizationCode` equals `abc`):
```
BEGIN:Calendar_Test_Temperature:12.3°C:abc
END:Calendar_Test_Temperature:23.4°F:abc
```
Command tags in a calendar event (in the case that configuration parameter `authorizationCode` is not set):
```
BEGIN:Calendar_Test_Switch:ON
END:Calendar_Test_Switch:OFF
```

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.icalendar</artifactId>
<name>openHAB Add-ons :: Bundles :: iCalendar Binding</name>
<properties>
<dep.noembedding>jackson-core,jackson-annotations,jackson-databind</dep.noembedding>
<jackson.version>2.9.10</jackson.version>
</properties>
<dependencies>
<!-- own dependencies -->
<dependency>
<groupId>net.sf.biweekly</groupId>
<artifactId>biweekly</artifactId>
<version>0.6.3</version>
<scope>compile</scope>
</dependency>
<!-- dependencies of biweekly -->
<dependency>
<groupId>com.github.mangstadt</groupId>
<artifactId>vinnie</artifactId>
<version>2.0.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.icalendar-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-icalendar" description="iCalendar Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature dependency="true">openhab.tp-jackson</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.icalendar/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,43 @@
/**
* 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link ICalendarBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Michael Wodniok - Initial contribution
*/
@NonNullByDefault
public class ICalendarBindingConstants {
public static final String BINDING_ID = "icalendar";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_CALENDAR = new ThingTypeUID(BINDING_ID, "calendar");
// List of all Channel ids
public static final String CHANNEL_CURRENT_EVENT_TITLE = "current_title";
public static final String CHANNEL_CURRENT_EVENT_START = "current_start";
public static final String CHANNEL_CURRENT_EVENT_END = "current_end";
public static final String CHANNEL_CURRENT_EVENT_PRESENT = "current_presence";
public static final String CHANNEL_NEXT_EVENT_TITLE = "next_title";
public static final String CHANNEL_NEXT_EVENT_START = "next_start";
public static final String CHANNEL_NEXT_EVENT_END = "next_end";
// additional constants
public static final int HTTP_TIMEOUT_SECS = 60;
}

View File

@@ -0,0 +1,72 @@
/**
* 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;
import static org.openhab.binding.icalendar.internal.ICalendarBindingConstants.THING_TYPE_CALENDAR;
import java.util.Collections;
import java.util.Set;
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.ICalendarHandler;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
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;
/**
* The {@link ICalendarHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Michael Wodniok - Initial contribution
* @author Andrew Fiddian-Green - EventPublisher code
*/
@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 final HttpClient sharedHttpClient;
private final EventPublisher eventPublisher;
@Activate
public ICalendarHandlerFactory(@Reference HttpClientFactory httpClientFactory,
@Reference EventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
sharedHttpClient = httpClientFactory.getCommonHttpClient();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
final ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (!supportsThingType(thingTypeUID)) {
return null;
}
return new ICalendarHandler(thing, sharedHttpClient, eventPublisher);
}
}

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.config;
import java.math.BigDecimal;
/**
* The {@link ICalendarConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Michael Wodniok - Initial contribution
* @author Andrew Fiddian-Green - Support for authorizationCode
*/
public class ICalendarConfiguration {
public String authorizationCode;
public Integer maxSize;
public String password;
public BigDecimal refreshTime;
public String url;
public String username;
}

View File

@@ -0,0 +1,356 @@
/**
* 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.io.File;
import java.io.FileInputStream;
import java.io.IOException;
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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.icalendar.internal.config.ICalendarConfiguration;
import org.openhab.binding.icalendar.internal.handler.PullJob.CalendarUpdateListener;
import org.openhab.binding.icalendar.internal.logic.AbstractPresentableCalendar;
import org.openhab.binding.icalendar.internal.logic.CalendarException;
import org.openhab.binding.icalendar.internal.logic.CommandTag;
import org.openhab.binding.icalendar.internal.logic.CommandTagType;
import org.openhab.binding.icalendar.internal.logic.Event;
import org.openhab.core.config.core.ConfigConstants;
import org.openhab.core.events.EventPublisher;
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.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.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ICalendarHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Michael Wodniok - Initial contribution
* @author Andrew Fiddian-Green - Support for Command Tags embedded in the Event description
*/
@NonNullByDefault
public class ICalendarHandler extends BaseThingHandler 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 @Nullable ScheduledFuture<?> pullJobFuture;
private @Nullable AbstractPresentableCalendar runtimeCalendar;
private @Nullable ScheduledFuture<?> updateJobFuture;
private Instant updateStatesLastCalledTime;
public ICalendarHandler(Thing thing, HttpClient httpClient, EventPublisher eventPublisher) {
super(thing);
this.httpClient = httpClient;
calendarFile = new File(ConfigConstants.getUserDataFolder() + File.separator
+ getThing().getUID().getAsString().replaceAll("[<>:\"/\\\\|?*]", "_") + ".ical");
eventPublisherCallback = eventPublisher;
updateStatesLastCalledTime = Instant.now();
}
@Override
public void dispose() {
final ScheduledFuture<?> currentUpdateJobFuture = updateJobFuture;
if (currentUpdateJobFuture != null) {
currentUpdateJobFuture.cancel(true);
}
final ScheduledFuture<?> currentPullJobFuture = pullJobFuture;
if (currentPullJobFuture != null) {
currentPullJobFuture.cancel(true);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
switch (channelUID.getId()) {
case CHANNEL_CURRENT_EVENT_PRESENT:
case CHANNEL_CURRENT_EVENT_TITLE:
case CHANNEL_CURRENT_EVENT_START:
case CHANNEL_CURRENT_EVENT_END:
case CHANNEL_NEXT_EVENT_TITLE:
case CHANNEL_NEXT_EVENT_START:
case CHANNEL_NEXT_EVENT_END:
if (command instanceof RefreshType) {
updateStates();
}
break;
default:
logger.warn("Framework sent command to unknown channel with id '{}'", channelUID.getId());
}
}
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
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.");
}
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);
}
}
@Override
public void onCalendarUpdated() {
if (reloadCalendar()) {
updateStates();
} else {
logger.trace("Calendar was updated, but loading failed.");
}
}
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;
if (syncConfiguration == null) {
logger.debug("Configuration not instantiated!");
return;
}
// loop through all events in the list
for (Event event : events) {
// loop through all command tags in the event
for (CommandTag cmdTag : event.commandTags) {
// only process the BEGIN resp. END tags
if (cmdTag.getTagType() != execTime) {
continue;
}
if (!cmdTag.isAuthorized(syncConfiguration.authorizationCode)) {
logger.warn("Event: {}, Command Tag: {} => Command not authorized!", event.title,
cmdTag.getFullTag());
continue;
}
final Command cmdState = cmdTag.getCommand();
if (cmdState == null) {
logger.warn("Event: {}, Command Tag: {} => Error creating Command State!", event.title,
cmdTag.getFullTag());
continue;
}
// (try to) execute the command
try {
eventPublisherCallback.post(ItemEventFactory.createCommandEvent(cmdTag.getItemName(), cmdState));
if (logger.isDebugEnabled()) {
String cmdType = cmdState.getClass().toString();
int index = cmdType.lastIndexOf(".") + 1;
if ((index > 0) && (index < cmdType.length())) {
cmdType = cmdType.substring(index);
}
logger.debug("Event: {}, Command Tag: {} => {}.postUpdate({}: {})", event.title,
cmdTag.getFullTag(), cmdTag.getItemName(), cmdType, cmdState);
}
} catch (IllegalArgumentException | IllegalStateException e) {
logger.warn("Event: {}, Command Tag: {} => Unable to push command to target item!", event.title,
cmdTag.getFullTag());
logger.debug("Exception occured while pushing to item!", e);
}
}
}
}
/**
* Reloads the calendar from local ical-file. Replaces the class internal calendar - if loading succeeds. Else
* logging details at warn-level logger.
*
* @return Whether the calendar was loaded successfully.
*/
private boolean reloadCalendar() {
if (!calendarFile.isFile()) {
logger.info("Local file for reloading calendar is missing.");
return false;
}
final ICalendarConfiguration config = configuration;
if (config == null) {
logger.warn("Can't reload calendar when configuration is missing.");
return false;
}
try (final FileInputStream fileStream = new FileInputStream(calendarFile)) {
final AbstractPresentableCalendar calendar = AbstractPresentableCalendar.create(fileStream);
runtimeCalendar = calendar;
rescheduleCalendarStateUpdate();
} catch (IOException | CalendarException e) {
logger.warn("Loading calendar failed: {}", e.getMessage());
return false;
}
return true;
}
/**
* Reschedules the next update of the states.
*/
private void rescheduleCalendarStateUpdate() {
final ScheduledFuture<?> currentUpdateJobFuture = updateJobFuture;
if (currentUpdateJobFuture != null) {
if (!(currentUpdateJobFuture.isCancelled() || currentUpdateJobFuture.isDone())) {
currentUpdateJobFuture.cancel(true);
}
updateJobFuture = null;
}
final AbstractPresentableCalendar currentCalendar = runtimeCalendar;
if (currentCalendar == null) {
return;
}
final Instant now = Instant.now();
if (currentCalendar.isEventPresent(now)) {
final Event currentEvent = currentCalendar.getCurrentEvent(now);
if (currentEvent == null) {
logger.debug(
"Could not schedule next update of states, due to unexpected behaviour of calendar implementation.");
return;
}
updateJobFuture = scheduler.schedule(() -> {
ICalendarHandler.this.updateStates();
ICalendarHandler.this.rescheduleCalendarStateUpdate();
}, currentEvent.end.getEpochSecond() - now.getEpochSecond(), TimeUnit.SECONDS);
} else {
final Event nextEvent = currentCalendar.getNextEvent(now);
final ICalendarConfiguration currentConfig = this.configuration;
if (currentConfig == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Something is broken, the configuration is not available.");
return;
}
if (nextEvent == null) {
updateJobFuture = scheduler.schedule(() -> {
ICalendarHandler.this.rescheduleCalendarStateUpdate();
}, 1L, TimeUnit.DAYS);
} else {
updateJobFuture = scheduler.schedule(() -> {
ICalendarHandler.this.updateStates();
ICalendarHandler.this.rescheduleCalendarStateUpdate();
}, nextEvent.start.getEpochSecond() - now.getEpochSecond(), TimeUnit.SECONDS);
}
}
}
/**
* Updates the states of the Thing and its channels.
*/
private void updateStates() {
final AbstractPresentableCalendar calendar = runtimeCalendar;
if (calendar == null) {
updateStatus(ThingStatus.OFFLINE);
} else {
updateStatus(ThingStatus.ONLINE);
final Instant now = Instant.now();
if (calendar.isEventPresent(now)) {
updateState(CHANNEL_CURRENT_EVENT_PRESENT, OnOffType.ON);
final Event currentEvent = calendar.getCurrentEvent(now);
if (currentEvent == null) {
logger.warn("Unexpected inconsistency of internal API. Not Updating event details.");
} else {
updateState(CHANNEL_CURRENT_EVENT_TITLE, new StringType(currentEvent.title));
updateState(CHANNEL_CURRENT_EVENT_START,
new DateTimeType(currentEvent.start.atZone(ZoneId.systemDefault())));
updateState(CHANNEL_CURRENT_EVENT_END,
new DateTimeType(currentEvent.end.atZone(ZoneId.systemDefault())));
}
} else {
updateState(CHANNEL_CURRENT_EVENT_PRESENT, OnOffType.OFF);
updateState(CHANNEL_CURRENT_EVENT_TITLE, UnDefType.UNDEF);
updateState(CHANNEL_CURRENT_EVENT_START, UnDefType.UNDEF);
updateState(CHANNEL_CURRENT_EVENT_END, UnDefType.UNDEF);
}
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())));
} else {
updateState(CHANNEL_NEXT_EVENT_TITLE, UnDefType.UNDEF);
updateState(CHANNEL_NEXT_EVENT_START, UnDefType.UNDEF);
updateState(CHANNEL_NEXT_EVENT_END, UnDefType.UNDEF);
}
// process all Command Tags in all Calendar Events which ENDED since updateStates was last called
// the END Event tags must be processed before the BEGIN ones
executeEventCommands(calendar.getJustEndedEvents(updateStatesLastCalledTime, now), CommandTagType.END);
// process all Command Tags in all Calendar Events which BEGAN since updateStates was last called
// the END Event tags must be processed before the BEGIN ones
executeEventCommands(calendar.getJustBegunEvents(updateStatesLastCalledTime, now), CommandTagType.BEGIN);
// save time when updateStates was previously called
// the purpose is to prevent repeat command execution of events that have already been executed
updateStatesLastCalledTime = now;
}
}
}

View File

@@ -0,0 +1,203 @@
/**
* 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.HTTP_TIMEOUT_SECS;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Authentication;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.util.BasicAuthentication;
import org.eclipse.jetty.client.util.InputStreamResponseListener;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.icalendar.internal.logic.AbstractPresentableCalendar;
import org.openhab.binding.icalendar.internal.logic.CalendarException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The Job for pulling an update of a calendar. Fires
* {@link CalendarUpdateListener#onCalendarUpdated()} after successful update.
*
* @author Michael Wodniok - Initial contribution
*/
@NonNullByDefault
class PullJob implements Runnable {
private final static String TMP_FILE_PREFIX = "icalendardld";
private final Authentication.@Nullable Result authentication;
private final File destination;
private final HttpClient httpClient;
private final CalendarUpdateListener listener;
private final Logger logger = LoggerFactory.getLogger(PullJob.class);
private final int maxSize;
private final URI sourceURI;
/**
* Constructor of PullJob for creating a single pull of a calendar.
*
* @param httpClient A HttpClient for getting the source
* @param sourceURI The source as URI
* @param username Optional username for basic auth. Must be set together with a password.
* @param password Optional password for basic auth. Must be set together with an username.
* @param destination The destination the downloaded calendar should be saved to.
* @param maxSize The maximum size of the downloaded calendar in bytes.
* @param listener The listener that should be fired when update succeed.
*/
public PullJob(HttpClient httpClient, URI sourceURI, @Nullable String username, @Nullable String password,
File destination, int maxSize, CalendarUpdateListener listener) {
this.httpClient = httpClient;
this.sourceURI = sourceURI;
if (username != null && password != null) {
authentication = new BasicAuthentication.BasicResult(this.sourceURI, username, password);
} else {
authentication = null;
}
this.destination = destination;
this.listener = listener;
this.maxSize = maxSize;
}
@Override
public void run() {
final Request request = httpClient.newRequest(sourceURI).followRedirects(true).method(HttpMethod.GET);
final Authentication.@Nullable Result currentAuthentication = authentication;
if (currentAuthentication != null) {
currentAuthentication.apply(request);
}
final InputStreamResponseListener asyncListener = new InputStreamResponseListener();
request.send(asyncListener);
Response response;
try {
response = asyncListener.get(HTTP_TIMEOUT_SECS, TimeUnit.SECONDS);
} catch (InterruptedException | TimeoutException | ExecutionException e1) {
logger.warn("Response for calendar request could not be retrieved. Error message is: {}", e1.getMessage());
return;
}
if (response.getStatus() != HttpStatus.OK_200) {
logger.warn("Response status for getting \"{}\" was {} instead of 200. Ignoring it.", sourceURI,
response.getStatus());
return;
}
final String responseLength = response.getHeaders().get(HttpHeader.CONTENT_LENGTH);
if (responseLength != null) {
try {
if (Integer.parseInt(responseLength) > maxSize) {
logger.warn(
"Calendar is too big ({} bytes > {} bytes), aborting request. You may change the maximum calendar size in configuration, if appropriate.",
responseLength, maxSize);
response.abort(new ResponseTooBigException());
return;
}
} catch (NumberFormatException e) {
logger.debug(
"While requesting calendar Content-Length was set, but is malformed. Falling back to read-loop.",
e);
}
}
File tmpTargetFile;
try {
tmpTargetFile = File.createTempFile(TMP_FILE_PREFIX, null);
} catch (IOException e) {
logger.warn("Not able to create temporary file for downloading iCal. Error message is: {}", e.getMessage());
return;
}
try (final FileOutputStream tmpOutStream = new FileOutputStream(tmpTargetFile);
final InputStream httpInputStream = asyncListener.getInputStream()) {
final byte[] buffer = new byte[1024];
int readBytesTotal = 0;
int currentReadBytes = -1;
while ((currentReadBytes = httpInputStream.read(buffer)) > -1) {
readBytesTotal += currentReadBytes;
if (readBytesTotal > maxSize) {
logger.warn(
"Calendar is too big (> {} bytes). Stopping receiving calendar. You may change the maximum calendar size in configuration, if appropriate.",
maxSize);
response.abort(new ResponseTooBigException());
return;
}
tmpOutStream.write(buffer, 0, currentReadBytes);
}
} catch (IOException e) {
logger.warn("Not able to write temporary file with downloaded iCal. Error Message is: {}", e.getMessage());
return;
}
try (final FileInputStream tmpInput = new FileInputStream(tmpTargetFile)) {
AbstractPresentableCalendar.create(tmpInput);
} catch (IOException | CalendarException e) {
logger.warn(
"Not able to read downloaded iCal. Validation failed or file not readable. Error message is: {}",
e.getMessage());
return;
}
try {
Files.move(tmpTargetFile.toPath(), destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
logger.warn("Failed to replace iCal file. Error message is: {}", e.getMessage());
return;
}
try {
listener.onCalendarUpdated();
} catch (Exception e) {
logger.debug("An Exception was thrown while calling back", e);
}
}
/**
* Interface for calling back when the update succeed.
*/
public static interface CalendarUpdateListener {
/**
* Callback when update was successful and result was placed onto target file.
*/
public void onCalendarUpdated();
}
/**
* Exception for failure if size of the response is greater than allowed.
*/
private static class ResponseTooBigException extends Exception {
/**
* The only local definition. Rest of implementation is taken from Exception or is default.
*/
private static final long serialVersionUID = 7033851403473533793L;
}
}

View File

@@ -0,0 +1,89 @@
/**
* 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 java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* A calendar which provides the interface to the calendar implementation for
* the binding, encapsulating the implementation of the real calendar.
*
* @author Michael Wodniok - Initial contribution
* @author Andrew Fiddian-Green - Methods getJustBegunEvents() & getJustEndedEvents()
*/
@NonNullByDefault
public abstract class AbstractPresentableCalendar {
/**
* Creates an implementing Instance of AbstractPresentableCalendar.
*
* @param calendarStream A Stream containing the iCal data.
* @return The instance.
* @throws IOException When something while reading stream fails.
* @throws CalendarException When something while parsing fails.
*/
public static AbstractPresentableCalendar create(InputStream calendarStream) throws IOException, CalendarException {
return new BiweeklyPresentableCalendar(calendarStream);
}
/**
* Searches the event currently (at given Instant) present.
*
* @param instant The Instant, the event should be returned for.
* @return The current {@link Event} containing the data of the event or
* null if no event is present.
*/
public abstract @Nullable Event getCurrentEvent(Instant instant);
/**
* Return a list of events that have just begun within the time frame
*
* @param frameBegin the start of the time frame
* @param frameEnd the start of the time frame
* @return list of iCalendar Events that BEGIN within the time frame
*/
public abstract List<Event> getJustBegunEvents(Instant frameBegin, Instant frameEnd);
/**
* Return a list of events that have just ended within the time frame
*
* @param frameBegin the start of the time frame
* @param frameEnd the start of the time frame
* @return list of iCalendar Events that END within the time frame
*/
public abstract List<Event> getJustEndedEvents(Instant frameBegin, Instant frameEnd);
/**
* The next event after given instant.
*
* @param instant The Instant after which the next event should be
* searched.
* @return The next event after the given Instant or null if there is any
* further in the calendar.
*/
public abstract @Nullable Event getNextEvent(Instant instant);
/**
* Checks whether an event is present at given Instant.
*
* @param instant The Instant, that should be checked.
* @return True if an event is present.
*/
public abstract boolean isEventPresent(Instant instant);
}

View File

@@ -0,0 +1,313 @@
/**
* 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 java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import biweekly.ICalendar;
import biweekly.component.VEvent;
import biweekly.io.TimezoneAssignment;
import biweekly.io.TimezoneInfo;
import biweekly.io.text.ICalReader;
import biweekly.property.DateEnd;
import biweekly.property.DateStart;
import biweekly.property.Description;
import biweekly.property.DurationProperty;
import biweekly.property.Status;
import biweekly.property.Summary;
import biweekly.property.Uid;
import biweekly.util.com.google.ical.compat.javautil.DateIterator;
/**
* Implementation of {@link AbstractPresentableCalendar} with ical4j. Please
* use {@link AbstractPresentableCalendar#create(InputStream)} for productive
* instantiation.
*
* @author Michael Wodniok - Initial contribution
* @author Andrew Fiddian-Green - Methods getJustBegunEvents() & getJustEndedEvents()
*/
@NonNullByDefault
class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
private final ICalendar usedCalendar;
BiweeklyPresentableCalendar(InputStream streamed) throws IOException, CalendarException {
try (final ICalReader reader = new ICalReader(streamed)) {
final ICalendar currentCalendar = reader.readNext();
if (currentCalendar == null) {
throw new CalendarException("No calendar was parsed.");
}
this.usedCalendar = currentCalendar;
}
}
@Override
public @Nullable Event getCurrentEvent(Instant instant) {
final VEventWPeriod currentComponentWPeriod = this.getCurrentComponentWPeriod(instant);
if (currentComponentWPeriod == null) {
return null;
}
return currentComponentWPeriod.toEvent();
}
@Override
public List<Event> getJustBegunEvents(Instant frameBegin, Instant frameEnd) {
final List<Event> eventList = new ArrayList<>();
// process all the events in the iCalendar
for (final VEvent event : usedCalendar.getEvents()) {
// iterate over all begin dates
final DateIterator begDates = getRecurredEventDateIterator(event);
while (begDates.hasNext()) {
final Instant begInst = begDates.next().toInstant();
if (begInst.isBefore(frameBegin)) {
continue;
} else if (begInst.isAfter(frameEnd)) {
break;
}
// fall through => means we are within the time frame
Duration duration = getEventLength(event);
if (duration == null) {
duration = Duration.ofMinutes(1);
}
eventList.add(new VEventWPeriod(event, begInst, begInst.plus(duration)).toEvent());
break;
}
}
return eventList;
}
@Override
public List<Event> getJustEndedEvents(Instant frameBegin, Instant frameEnd) {
final List<Event> eventList = new ArrayList<>();
// process all the events in the iCalendar
for (final VEvent event : usedCalendar.getEvents()) {
final Duration duration = getEventLength(event);
if (duration == null) {
continue;
}
// iterate over all begin dates
final DateIterator begDates = getRecurredEventDateIterator(event);
while (begDates.hasNext()) {
final Instant begInst = begDates.next().toInstant();
final Instant endInst = begInst.plus(duration);
if (endInst.isBefore(frameBegin)) {
continue;
} else if (endInst.isAfter(frameEnd)) {
break;
}
// fall through => means we are within the time frame
eventList.add(new VEventWPeriod(event, begInst, endInst).toEvent());
break;
}
}
return eventList;
}
@Override
public @Nullable Event getNextEvent(Instant instant) {
final Collection<VEventWPeriod> candidates = new ArrayList<VEventWPeriod>();
final Collection<VEvent> negativeEvents = new ArrayList<VEvent>();
final Collection<VEvent> positiveEvents = new ArrayList<VEvent>();
classifyEvents(positiveEvents, negativeEvents);
for (final VEvent currentEvent : positiveEvents) {
final DateIterator startDates = this.getRecurredEventDateIterator(currentEvent);
final Duration duration = getEventLength(currentEvent);
if (duration == null) {
continue;
}
startDates.advanceTo(Date.from(instant));
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)));
break;
}
}
}
}
VEventWPeriod earliestNextEvent = null;
for (final VEventWPeriod positiveCandidate : candidates) {
if (earliestNextEvent == null || earliestNextEvent.start.isAfter(positiveCandidate.start)) {
earliestNextEvent = positiveCandidate;
}
}
if (earliestNextEvent == null) {
return null;
}
return earliestNextEvent.toEvent();
}
@Override
public boolean isEventPresent(Instant instant) {
return (this.getCurrentComponentWPeriod(instant) != null);
}
/**
* Classifies events into positive and negative ones.
*
* @param positiveEvents A List where to add positive ones.
* @param negativeEvents A List where to add negative ones.
*/
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);
positiveOrNegativeEvents.add(currentEvent);
}
}
/**
* Searches for a current event at given Instant.
*
* @param instant The Instant to use for finding events.
* @return A VEventWPeriod describing the event or null if there is none.
*/
private @Nullable VEventWPeriod getCurrentComponentWPeriod(Instant instant) {
final List<VEvent> negativeEvents = new ArrayList<VEvent>();
final List<VEvent> positiveEvents = new ArrayList<VEvent>();
classifyEvents(positiveEvents, negativeEvents);
for (final VEvent currentEvent : positiveEvents) {
final DateIterator startDates = this.getRecurredEventDateIterator(currentEvent);
final Duration duration = getEventLength(currentEvent);
if (duration == null) {
continue;
}
startDates.advanceTo(Date.from(instant.minus(duration)));
while (startDates.hasNext()) {
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);
}
}
if (startInstant.isAfter(instant.plus(duration))) {
break;
}
}
}
return null;
}
/**
* Finds a duration of the event.
*
* @param vEvent The event to find out the duration.
* @return Either a Duration describing the events length or null, if no information is available.
*/
private static @Nullable Duration getEventLength(VEvent vEvent) {
final DurationProperty duration = vEvent.getDuration();
if (duration != null) {
final biweekly.util.Duration eventDuration = duration.getValue();
return Duration.ofMillis(eventDuration.toMillis());
}
final DateStart start = vEvent.getDateStart();
final DateEnd end = vEvent.getDateEnd();
if (start == null || end == null) {
return null;
}
return Duration.between(start.getValue().toInstant(), end.getValue().toInstant());
}
/**
* Retrieves a DateIterator to iterate through the events occurrences.
*
* @param vEvent The VEvent to create the iterator for.
* @return The DateIterator for {@link VEvent}
*/
private DateIterator getRecurredEventDateIterator(VEvent vEvent) {
final TimezoneInfo tzinfo = this.usedCalendar.getTimezoneInfo();
final DateStart firstStart = vEvent.getDateStart();
TimeZone tz;
if (tzinfo.isFloating(firstStart)) {
tz = TimeZone.getDefault();
} else {
final TimezoneAssignment startAssignment = tzinfo.getTimezone(firstStart);
tz = (startAssignment == null ? TimeZone.getTimeZone("UTC") : startAssignment.getTimeZone());
}
return vEvent.getDateIterator(tz);
}
/**
* Checks whether an counter event blocks an event with given uid and start.
*
* @param startInstant The start of the event.
* @param eventUid The uid of the event.
* @param counterEvents Events that may counter.
* @return True if a counter event exists that matches uid and start, else false.
*/
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);
counterStartDates.advanceTo(Date.from(startInstant));
if (counterStartDates.hasNext()) {
final Instant counterStartInstant = counterStartDates.next().toInstant();
if (counterStartInstant.equals(startInstant)) {
return true;
}
}
}
}
return false;
}
/**
* A Class describing an event together with a start and end instant.
*
* @author Michael Wodniok - Initial contribution.
*/
private static class VEventWPeriod {
final VEvent vEvent;
final Instant start;
final Instant end;
public VEventWPeriod(VEvent vEvent, Instant start, Instant end) {
this.vEvent = vEvent;
this.start = start;
this.end = end;
}
public Event toEvent() {
final Summary eventSummary = vEvent.getSummary();
final String title = eventSummary != null ? eventSummary.getValue() : "-";
final Description eventDescription = vEvent.getDescription();
final String description = eventDescription != null ? eventDescription.getValue() : "";
return new Event(title, start, end, description);
}
}
}

View File

@@ -0,0 +1,38 @@
/**
* 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;
/**
* Exception Class to encapsulate Exception data for binding.
*
* @author Michael Wodniok - Initial contribution
*/
@NonNullByDefault
public class CalendarException extends Exception {
private static final long serialVersionUID = -2071400154241449096L;
public CalendarException(String message) {
super(message);
}
public CalendarException(String message, Exception source) {
super(message, source);
}
public CalendarException(Exception source) {
super("Implementation specific exception occurred", source);
}
}

View File

@@ -0,0 +1,181 @@
/**
* 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 java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.RewindFastforwardType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.types.Command;
import org.openhab.core.types.TypeParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is a class that implements a Command Tag that may be embedded in an
* Event Description. Valid Tags must follow one of the following forms..
*
* BEGIN:<itemName>:<targetState>
* BEGIN:<itemName>:<targetState>:<authorizationCode>
* END:<itemName>:<targetState>
* END:<itemName>:<targetState>:<authorizationCode>
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class CommandTag {
private static final List<Class<? extends Command>> otherCommandTypes = Arrays.asList(QuantityType.class,
OnOffType.class, OpenClosedType.class, UpDownType.class, HSBType.class, PlayPauseType.class,
RewindFastforwardType.class, StringType.class);
private static final List<Class<? extends Command>> percentCommandType = Arrays.asList(PercentType.class);
private static final Logger logger = LoggerFactory.getLogger(CommandTag.class);
private String inputLine;
private CommandTagType tagType;
private String itemName;
private String targetState;
private String authorizationCode;
private @Nullable Command theCommand;
public CommandTag(String inputLine) throws IllegalArgumentException {
this.inputLine = inputLine.trim();
if (!CommandTagType.prefixValid(inputLine)) {
throw new IllegalArgumentException(
String.format("Command Tag Exception \"%s\" => Bad tag prefix!", inputLine));
}
if (!inputLine.contains(":")) {
throw new IllegalArgumentException(
String.format("Command Tag Exception \"%s\" => Missing \":\" delimiters!", inputLine));
}
String[] fields = inputLine.split(":");
if (fields.length < 3) {
throw new IllegalArgumentException(
String.format("Command Tag Exception \"%s\" => Not enough fields!", inputLine));
}
if (fields.length > 4) {
throw new IllegalArgumentException(
String.format("Command Tag Exception \"%s\" => Too many fields!", inputLine));
}
try {
tagType = CommandTagType.valueOf(fields[0]);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
String.format("Command Tag Exception \"%s\" => Invalid Tag Type!", inputLine));
}
itemName = fields[1].trim();
if (itemName.isEmpty()) {
throw new IllegalArgumentException(
String.format("Command Tag Exception \"%s\" => Item name empty!", inputLine));
}
if (!itemName.matches("^\\w+$")) {
throw new IllegalArgumentException(
String.format("Command Tag Exception \"%s\" => Bad syntax for Item name!", inputLine));
}
targetState = fields[2].trim();
if (targetState.isEmpty()) {
throw new IllegalArgumentException(
String.format("Command Tag Exception \"%s\" => Target State empty!", inputLine));
}
// string is in double quotes => force StringType
if (targetState.startsWith("\"") && targetState.endsWith("\"")) {
// new StringType() should always succeed
theCommand = new StringType(targetState.replace("\"", ""));
}
// string is in single quotes => ditto
else if (targetState.startsWith("'") && targetState.endsWith("'")) {
// new StringType() should always succeed
theCommand = new StringType(targetState.replace("'", ""));
}
// string ends with % => try PercentType
else if (targetState.endsWith("%")) {
theCommand = TypeParser.parseCommand(percentCommandType,
targetState.substring(0, targetState.length() - 1));
if (theCommand == null) {
throw new IllegalArgumentException(String
.format("Command Tag Exception \"%s\" => Invalid Target State percent value!", inputLine));
}
}
// try all other possible CommandTypes
if (theCommand == null) {
// TypeParser.parseCommand(otherCommandTypes should always succeed (with new StringType())
theCommand = TypeParser.parseCommand(otherCommandTypes, targetState);
}
if (fields.length == 4) {
authorizationCode = fields[3].trim();
} else {
authorizationCode = "";
}
}
public static @Nullable CommandTag createCommandTag(String inputLine) {
if (inputLine.isEmpty() || !CommandTagType.prefixValid(inputLine)) {
logger.trace("Command Tag Trace: \"{}\" => NOT a (valid) Command Tag!", inputLine);
return null;
}
try {
final CommandTag tag = new CommandTag(inputLine);
logger.trace("Command Tag Trace: \"{}\" => Fully valid Command Tag!", inputLine);
return tag;
} catch (IllegalArgumentException e) {
logger.warn("{}", e.getMessage());
return null;
}
}
public @Nullable Command getCommand() {
return theCommand;
}
public String getFullTag() {
return inputLine;
}
public String getItemName() {
return itemName;
}
public CommandTagType getTagType() {
return tagType;
}
public boolean isAuthorized(@Nullable String userAuthorizationCode) {
return (userAuthorizationCode == null || userAuthorizationCode.isEmpty()
|| userAuthorizationCode.equals(authorizationCode));
}
}

View File

@@ -0,0 +1,34 @@
/**
* 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;
import org.eclipse.jdt.annotation.Nullable;
/**
* An type enumerator to indicate whether a Command Tag is of type BEGIN or END; as in the following examples:
*
* BEGIN:<item_name>:<new_state>
* END:<item_name>:<new_state>
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public enum CommandTagType {
BEGIN,
END;
public static boolean prefixValid(@Nullable String line) {
return (line != null) && (line.startsWith(BEGIN.toString()) || line.startsWith(END.toString()));
}
}

View File

@@ -0,0 +1,62 @@
/**
* 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 java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* A single event.
*
* @author Michael Wodniok - Initial contribution
* @author Andrew Fiddian-Green - Added support for event description
*/
@NonNullByDefault
public class Event {
public final List<CommandTag> commandTags = new ArrayList<CommandTag>();
public final Instant end;
public final Instant start;
public final String title;
public Event(String title, Instant start, Instant end, String description) {
this.title = title;
this.start = start;
this.end = end;
if (description.isEmpty()) {
return;
}
String[] lines = description.replace("<p>", "").replace("</p>", "\n").split("\n");
for (String line : lines) {
CommandTag tag = CommandTag.createCommandTag(line);
if (tag != null) {
commandTags.add(tag);
}
}
}
@Override
public boolean equals(@Nullable Object other) {
if (other == null || other.getClass() != this.getClass()) {
return false;
}
final Event otherEvent = (Event) other;
return (this.title.equals(otherEvent.title) && this.start.equals(otherEvent.start)
&& this.end.equals(otherEvent.end));
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="icalendar" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>iCalendar Binding</name>
<description>Binding for using iCal calendars</description>
<author>Michael Wodniok</author>
</binding:binding>

View File

@@ -0,0 +1,37 @@
# binding
binding.icalendar.name = iCalendar-Binding
binding.icalendar.description = Binding zur Nutzung von iCal-Kalendern als Präsenz-Schalter.
# thing types
thing-type.icalendar.calendar.label = Kalender
thing-type.icalendar.calendar.description = Kalender basierend auf einem lesbaren iCal-Kalender.
# thing type config description
thing-type.config.icalendar.calendar.url.label = URL
thing-type.config.icalendar.calendar.url.description = URL des Kalenders
thing-type.config.icalendar.calendar.refreshTime.label = Aktualisierungsintervall
thing-type.config.icalendar.calendar.refreshTime.description = Intervall, in dem nach Updates im Kalender gesucht wird (Minuten)
thing-type.config.icalendar.calendar.username.label = Benutzername
thing-type.config.icalendar.calendar.username.description = Benutzername zum Abruf der Kalender-URL (für Basic-Auth, nur sinnvoll mit gesetztem Passwort)
thing-type.config.icalendar.calendar.password.label = Passwort
thing-type.config.icalendar.calendar.password.description = Passwort zum Abruf der Kalender-URL (für Basic-Auth, nur sinnvoll mit gesetztem Benutzername)
thing-type.config.icalendar.calendar.maxSize.label = Maximale Größe
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
# channel types
channel-type.icalendar.event_current_title.label = Titel des aktuellen Eintrags
channel-type.icalendar.event_current_title.description = Titel des aktuell präsenten Eintrags
channel-type.icalendar.event_current_start.label = Start des aktuellen Eintrags
channel-type.icalendar.event_current_start.description = Start des aktuell präsenten Eintrags
channel-type.icalendar.event_current_end.label = Ende des aktuellen Eintrags
channel-type.icalendar.event_current_end.description = Ende des aktuell präsenten Eintrags
channel-type.icalendar.event_current_presence.label = Präsenz eines aktuellen Eintrags
channel-type.icalendar.event_current_presence.description = Schalter, der die Präsenz eines aktuellen Eintrags darstellt
channel-type.icalendar.event_next_title.label = Titel des nächsten Eintrags
channel-type.icalendar.event_next_title.description = Titel des nächsten Eintrags
channel-type.icalendar.event_next_start.label = Start des nächsten Eintrags
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

View File

@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="icalendar"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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">
<label>Calendar</label>
<description>Calendar based on an iCal calendar.</description>
<channels>
<channel id="current_title" typeId="event_current_title"/>
<channel id="current_start" typeId="event_current_start"/>
<channel id="current_end" typeId="event_current_end"/>
<channel id="current_presence" typeId="event_current_presence"/>
<channel id="next_title" typeId="event_next_title"/>
<channel id="next_start" typeId="event_next_start"/>
<channel id="next_end" typeId="event_next_end"/>
</channels>
<config-description>
<parameter-group name="source">
<label>Source Settings</label>
</parameter-group>
<parameter-group name="parsing">
<label>Parsing Settings</label>
</parameter-group>
<parameter name="url" type="text" required="true" groupName="source">
<label>URL</label>
<description>URL for downloading iCalendar events</description>
<context>url</context>
</parameter>
<parameter name="refreshTime" type="integer" required="true" min="1" unit="min" groupName="source">
<label>Refresh Time</label>
<description>Frequency to scan for changes in minutes</description>
</parameter>
<parameter name="username" type="text" required="false" groupName="source">
<label>User Name</label>
<description>User name for fetching the calendar (usable in combination with password in HTTP basic auth)</description>
</parameter>
<parameter name="password" type="text" required="false" groupName="source">
<label>Password</label>
<description>Password for fetching the calendar (usable in combination with user name in HTTP basic auth)</description>
<context>password</context>
</parameter>
<parameter name="maxSize" type="integer" required="true" min="1" groupName="source">
<label>Maximum Calendar Size</label>
<description>The maximum size of the calendar in Megabytes</description>
<default>16</default>
<unitLabel>MB</unitLabel>
</parameter>
<parameter name="authorizationCode" type="text" required="false" groupName="parsing">
<label>Command Authorization Code</label>
<description>Authorization Code to allow the execution of Command Tags (may be empty)</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="event_current_title">
<item-type>String</item-type>
<label>Current Event Title</label>
<description>Title of the currently present event</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="event_current_start">
<item-type>DateTime</item-type>
<label>Current Event Start</label>
<description>Start of the currently present event</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="event_current_end">
<item-type>DateTime</item-type>
<label>Current Event End</label>
<description>End of the currently present event</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="event_current_presence">
<item-type>Switch</item-type>
<label>Current Event Presence</label>
<description>Current presence of an event</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="event_next_title">
<item-type>String</item-type>
<label>Next Event Title</label>
<description>Title of the next starting event in calendar</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="event_next_start">
<item-type>DateTime</item-type>
<label>Next Event Start</label>
<description>Start of the next event in calendar</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="event_next_end">
<item-type>DateTime</item-type>
<label>Next Event End</label>
<description>End of the next event in calendar</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,545 @@
/**
* 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 static org.junit.Assert.*;
import java.io.FileInputStream;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.RewindFastforwardType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.types.Command;
/**
* Tests for presentable calendar.
*
* @author Michael Wodniok - Initial contribution.
*
* @author Andrew Fiddian-Green - Tests for Command Tag code
*
*/
public class BiweeklyPresentableCalendarTest {
private AbstractPresentableCalendar calendar;
private AbstractPresentableCalendar calendar2;
private AbstractPresentableCalendar calendar3;
@Before
public void setUp() throws IOException, CalendarException {
calendar = new BiweeklyPresentableCalendar(new FileInputStream("src/test/resources/test.ics"));
calendar2 = new BiweeklyPresentableCalendar(new FileInputStream("src/test/resources/test2.ics"));
calendar3 = new BiweeklyPresentableCalendar(new FileInputStream("src/test/resources/test3.ics"));
}
/**
* Tests recurrence and whether TimeZone is interpolated in a right way.
*/
@Test
public void testIsEventPresent() {
// Test series
assertFalse(calendar.isEventPresent(Instant.parse("2019-09-08T09:04:00Z")));
assertTrue(calendar.isEventPresent(Instant.parse("2019-09-08T09:08:00Z")));
assertFalse(calendar.isEventPresent(Instant.parse("2019-09-08T09:11:00Z")));
assertFalse(calendar.isEventPresent(Instant.parse("2019-09-09T09:04:00Z")));
assertFalse(calendar.isEventPresent(Instant.parse("2019-09-09T09:08:00Z")));
assertFalse(calendar.isEventPresent(Instant.parse("2019-09-09T09:11:00Z")));
assertFalse(calendar.isEventPresent(Instant.parse("2019-09-10T09:04:00Z")));
assertTrue(calendar.isEventPresent(Instant.parse("2019-09-10T09:08:00Z")));
assertFalse(calendar.isEventPresent(Instant.parse("2019-09-10T09:11:00Z")));
assertFalse(calendar.isEventPresent(Instant.parse("2019-09-11T09:04:00Z")));
assertTrue(calendar.isEventPresent(Instant.parse("2019-09-11T09:08:00Z")));
assertFalse(calendar.isEventPresent(Instant.parse("2019-09-11T09:11:00Z")));
assertFalse(calendar.isEventPresent(Instant.parse("2019-09-12T09:04:00Z")));
assertTrue(calendar.isEventPresent(Instant.parse("2019-09-12T09:08:00Z")));
assertFalse(calendar.isEventPresent(Instant.parse("2019-09-12T09:11:00Z")));
assertFalse(calendar.isEventPresent(Instant.parse("2019-09-13T09:04:00Z")));
assertFalse(calendar.isEventPresent(Instant.parse("2019-09-13T09:08:00Z")));
assertFalse(calendar.isEventPresent(Instant.parse("2019-09-13T09:11:00Z")));
// Test in CEST (UTC+2)
assertFalse(calendar.isEventPresent(Instant.parse("2019-09-14T07:59:00Z")));
assertTrue(calendar.isEventPresent(Instant.parse("2019-09-14T08:03:00Z")));
assertFalse(calendar.isEventPresent(Instant.parse("2019-09-14T09:01:00Z")));
// Test Series with cancelled event by Davdroid
assertFalse(calendar2.isEventPresent(Instant.parse("2019-11-03T09:55:00Z")));
assertTrue(calendar2.isEventPresent(Instant.parse("2019-11-03T10:01:00Z")));
assertFalse(calendar2.isEventPresent(Instant.parse("2019-11-03T13:00:00Z")));
assertFalse(calendar2.isEventPresent(Instant.parse("2019-11-24T09:55:00Z")));
assertFalse(calendar2.isEventPresent(Instant.parse("2019-11-24T10:01:00Z")));
assertFalse(calendar2.isEventPresent(Instant.parse("2019-11-24T13:00:00Z")));
}
/**
* This test relies on a working isEventPresent and assumes the calculation
* of recurrence is done the same way.
*/
@Test
public void testGetCurrentEvent() {
Event currentEvent = calendar.getCurrentEvent(Instant.parse("2019-09-10T09:07:00Z"));
assertNotNull(currentEvent);
assertTrue("Test Series in UTC".contentEquals(currentEvent.title));
assertEquals(0, Instant.parse("2019-09-10T09:05:00Z").compareTo(currentEvent.start));
assertEquals(0, Instant.parse("2019-09-10T09:10:00Z").compareTo(currentEvent.end));
Event nonExistingEvent = calendar.getCurrentEvent(Instant.parse("2019-09-09T09:07:00Z"));
assertNull(nonExistingEvent);
}
/**
* This test relies on a working isEventPresent and assumes the calculation
* of recurrence is done the same way.
*/
@Test
public void testGetNextEvent() {
// positive case: next event of series
Event nextEventOfSeries = calendar.getNextEvent(Instant.parse("2019-09-10T09:07:00Z"));
assertNotNull(nextEventOfSeries);
assertTrue("Test Series in UTC".contentEquals(nextEventOfSeries.title));
assertEquals(0, Instant.parse("2019-09-11T09:05:00Z").compareTo(nextEventOfSeries.start));
assertEquals(0, Instant.parse("2019-09-11T09:10:00Z").compareTo(nextEventOfSeries.end));
// positive case: next event after series
Event nextEventOutsideSeries = calendar.getNextEvent(Instant.parse("2019-09-12T09:07:00Z"));
assertNotNull(nextEventOutsideSeries);
assertTrue("Test Event in UTC+2".contentEquals(nextEventOutsideSeries.title));
assertEquals(0, Instant.parse("2019-09-14T08:00:00Z").compareTo(nextEventOutsideSeries.start));
assertEquals(0, Instant.parse("2019-09-14T09:00:00Z").compareTo(nextEventOutsideSeries.end));
// positive case: next event should be also set if currently none is present
Event nextEventIndependent = calendar.getNextEvent(Instant.parse("2019-09-13T09:07:00Z"));
assertTrue(nextEventOutsideSeries.equals(nextEventIndependent));
// negative case: after last event there is no next
Event nonExistingEvent = calendar.getNextEvent(Instant.parse("2019-09-14T12:00:00Z"));
assertNull(nonExistingEvent);
// mixed case: cancelled events also not show up as next
Event nextEventAfterCancelled = calendar2.getNextEvent(Instant.parse("2019-11-24T09:55:00Z"));
assertNotNull(nextEventAfterCancelled);
assertEquals(0, Instant.parse("2019-12-01T10:00:00Z").compareTo(nextEventAfterCancelled.start));
}
/**
* This test checks for Events that have just begun or ended, and if so it checks for Command Tags
* and checks if these tags are valid
*/
@Test
public void testCommandTagCode() {
List<Event> events = null;
int eventCount = 2;
int tagsPerEvent = 8;
// test just begun events: first in the series
events = calendar3.getJustBegunEvents(Instant.parse("2020-01-28T15:55:00Z"),
Instant.parse("2020-01-28T16:05:00Z"));
assertNotNull(events);
assertEquals(eventCount, events.size());
for (Event event : events) {
List<CommandTag> cmdTags = event.commandTags;
assertEquals(tagsPerEvent, cmdTags.size());
int beginTags = 0;
for (CommandTag cmdTag : cmdTags) {
if (cmdTag.getTagType() == CommandTagType.BEGIN) {
assertTrue(cmdTag.isAuthorized("abc"));
assertTrue(cmdTag.getItemName().matches("^\\w+$"));
assertTrue(cmdTag.getCommand() != null);
beginTags++;
}
}
assertEquals(tagsPerEvent / 2, beginTags);
}
// test just begun events: third in the series
events = calendar3.getJustBegunEvents(Instant.parse("2020-01-30T15:55:00Z"),
Instant.parse("2020-01-30T16:05:00Z"));
assertNotNull(events);
assertEquals(eventCount, events.size());
for (Event event : events) {
List<CommandTag> cmdTags = event.commandTags;
assertEquals(tagsPerEvent, cmdTags.size());
int beginTags = 0;
for (CommandTag cmdTag : cmdTags) {
if (cmdTag.getTagType() == CommandTagType.BEGIN) {
assertTrue(cmdTag.isAuthorized("abc"));
assertTrue(cmdTag.getItemName().matches("^\\w+$"));
assertTrue(cmdTag.getCommand() != null);
beginTags++;
}
}
assertEquals(tagsPerEvent / 2, beginTags);
}
// test outside of window: begun events, too early
events = calendar3.getJustBegunEvents(Instant.parse("2020-01-28T15:50:00Z"),
Instant.parse("2020-01-28T15:55:00Z"));
assertNotNull(events);
assertEquals(0, events.size());
// test outside of window: begun events, too late
events = calendar3.getJustBegunEvents(Instant.parse("2020-01-28T16:05:00Z"),
Instant.parse("2020-01-28T16:10:00Z"));
assertNotNull(events);
assertEquals(0, events.size());
// test just ended events: first in the series
events = calendar3.getJustEndedEvents(Instant.parse("2020-01-28T16:25:00Z"),
Instant.parse("2020-01-28T16:35:00Z"));
assertNotNull(events);
assertEquals(eventCount, events.size());
for (Event event : events) {
List<CommandTag> cmdTags = event.commandTags;
assertEquals(tagsPerEvent, cmdTags.size());
int endTags = 0;
for (CommandTag cmdTag : cmdTags) {
if (cmdTag.getTagType() == CommandTagType.END) {
assertTrue(cmdTag.isAuthorized("abc"));
assertTrue(cmdTag.getItemName().matches("^\\w+$"));
assertTrue(cmdTag.getCommand() != null);
endTags++;
}
}
assertEquals(tagsPerEvent / 2, endTags);
}
// test outside of window: ended events, too early
events = calendar3.getJustEndedEvents(Instant.parse("2020-01-28T16:20:00Z"),
Instant.parse("2020-01-28T16:25:00Z"));
assertNotNull(events);
assertEquals(0, events.size());
// test outside of window: ended events, too late
events = calendar3.getJustEndedEvents(Instant.parse("2020-01-28T16:35:00Z"),
Instant.parse("2020-01-28T16:40:00Z"));
assertNotNull(events);
assertEquals(0, events.size());
// test a valid just begun event with both good and bad authorization codes
events = calendar3.getJustBegunEvents(Instant.parse("2020-01-28T15:55:00Z"),
Instant.parse("2020-01-28T16:05:00Z"));
assertNotNull(events);
assertTrue(events.size() > 0);
List<CommandTag> cmdTags = events.get(0).commandTags;
assertTrue(cmdTags.size() > 0);
CommandTag cmd = cmdTags.get(0);
// accept correct, empty or null configuration codes
assertTrue(cmd.isAuthorized("abc"));
assertTrue(cmd.isAuthorized(""));
assertTrue(cmd.isAuthorized(null));
// reject incorrect configuration code
assertFalse(cmd.isAuthorized("123"));
// test tag syntax: Test Series #1
events = calendar3.getJustBegunEvents(Instant.parse("2020-01-28T19:25:00Z"),
Instant.parse("2020-01-28T19:35:00Z"));
assertNotNull(events);
assertEquals(1, events.size());
cmdTags = events.get(0).commandTags;
assertEquals(11, cmdTags.size());
// BEGIN:Calendar_Test_Color:ON:abc
assertEquals("Calendar_Test_Color", cmdTags.get(0).getItemName());
assertTrue(cmdTags.get(0).isAuthorized("abc"));
Command cmd0 = cmdTags.get(0).getCommand();
assertNotNull(cmd0);
assertEquals(OnOffType.class, cmd0.getClass());
// BEGIN:Calendar_Test_Contact:OPEN:abc
Command cmd1 = cmdTags.get(1).getCommand();
assertNotNull(cmd1);
assertEquals(OpenClosedType.class, cmd1.getClass());
// BEGIN:Calendar_Test_Dimmer:ON:abc
Command cmd2 = cmdTags.get(2).getCommand();
assertNotNull(cmd2);
assertEquals(OnOffType.class, cmd2.getClass());
// BEGIN:Calendar_Test_Number:12.3:abc
Command cmd3 = cmdTags.get(3).getCommand();
assertNotNull(cmd3);
assertEquals(QuantityType.class, cmd3.getClass());
// BEGIN:Calendar_Test_Temperature:12.3°C:abc
Command cmd4 = cmdTags.get(4).getCommand();
assertNotNull(cmd4);
assertEquals(QuantityType.class, cmd4.getClass());
// BEGIN:Calendar_Test_Pressure:12.3hPa:abc
Command cmd5 = cmdTags.get(5).getCommand();
assertNotNull(cmd5);
assertEquals(QuantityType.class, cmd5.getClass());
// BEGIN:Calendar_Test_Speed:12.3m/s:abc
Command cmd6 = cmdTags.get(6).getCommand();
assertNotNull(cmd6);
assertEquals(QuantityType.class, cmd6.getClass());
// BEGIN:Calendar_Test_Player:PLAY:abc
Command cmd7 = cmdTags.get(7).getCommand();
assertNotNull(cmd7);
assertEquals(PlayPauseType.class, cmd7.getClass());
// BEGIN:Calendar_Test_RollerShutter:UP:abc
Command cmd8 = cmdTags.get(8).getCommand();
assertNotNull(cmd8);
assertEquals(UpDownType.class, cmd8.getClass());
// BEGIN:Calendar_Test_String:Test Series #1:abc
Command cmd9 = cmdTags.get(9).getCommand();
assertNotNull(cmd9);
assertEquals(StringType.class, cmd9.getClass());
// BEGIN:Calendar_Test_Switch:ON:abc
Command cmd10 = cmdTags.get(10).getCommand();
assertNotNull(cmd10);
assertEquals(OnOffType.class, cmd10.getClass());
// test tag syntax: Test Series #4
events = calendar3.getJustBegunEvents(Instant.parse("2020-01-28T20:10:00Z"),
Instant.parse("2020-01-28T20:20:00Z"));
assertNotNull(events);
assertEquals(1, events.size());
cmdTags = events.get(0).commandTags;
assertEquals(11, cmdTags.size());
// BEGIN:Calendar_Test_Color:0%:abc
cmd0 = cmdTags.get(0).getCommand();
assertNotNull(cmd0);
assertEquals(PercentType.class, cmd0.getClass());
// BEGIN:Calendar_Test_Contact:CLOSED:abc
cmd1 = cmdTags.get(1).getCommand();
assertNotNull(cmd1);
assertEquals(OpenClosedType.class, cmd1.getClass());
// BEGIN:Calendar_Test_Dimmer:0%:abc
cmd2 = cmdTags.get(2).getCommand();
assertNotNull(cmd2);
assertEquals(PercentType.class, cmd2.getClass());
// BEGIN:Calendar_Test_Number:-12.3:abc
cmd3 = cmdTags.get(3).getCommand();
assertNotNull(cmd3);
assertEquals(QuantityType.class, cmd3.getClass());
// BEGIN:Calendar_Test_Temperature:-12.3°C:abc
cmd4 = cmdTags.get(4).getCommand();
assertNotNull(cmd4);
assertEquals(QuantityType.class, cmd4.getClass());
// BEGIN:Calendar_Test_Pressure:500mmHg:abc
cmd5 = cmdTags.get(5).getCommand();
assertNotNull(cmd5);
assertEquals(QuantityType.class, cmd5.getClass());
// BEGIN:Calendar_Test_Speed:12300000mm/h:abc
cmd6 = cmdTags.get(6).getCommand();
assertNotNull(cmd6);
assertEquals(QuantityType.class, cmd6.getClass());
// BEGIN:Calendar_Test_Player:REWIND:abc
cmd7 = cmdTags.get(7).getCommand();
assertNotNull(cmd7);
assertEquals(RewindFastforwardType.class, cmd7.getClass());
// BEGIN:Calendar_Test_RollerShutter:100%:abc
cmd8 = cmdTags.get(8).getCommand();
assertNotNull(cmd8);
assertEquals(PercentType.class, cmd8.getClass());
// BEGIN:Calendar_Test_String:Test Series #4:abc
cmd9 = cmdTags.get(9).getCommand();
assertNotNull(cmd9);
assertEquals(StringType.class, cmd9.getClass());
// BEGIN:Calendar_Test_Switch:OFF:abc
cmd10 = cmdTags.get(10).getCommand();
assertNotNull(cmd10);
assertEquals(OnOffType.class, cmd10.getClass());
// test tag syntax: Test Series #5
events = calendar3.getJustBegunEvents(Instant.parse("2020-01-28T20:25:00Z"),
Instant.parse("2020-01-28T20:35:00Z"));
assertNotNull(events);
assertEquals(1, events.size());
cmdTags = events.get(0).commandTags;
assertEquals(11, cmdTags.size());
// BEGIN:Calendar_Test_Color:240,100,100:abc
cmd0 = cmdTags.get(0).getCommand();
assertNotNull(cmd0);
assertEquals(HSBType.class, cmd0.getClass());
// BEGIN:Calendar_Test_Contact:OPEN:abc
cmd1 = cmdTags.get(1).getCommand();
assertNotNull(cmd1);
assertEquals(OpenClosedType.class, cmd1.getClass());
// BEGIN:Calendar_Test_Dimmer:50%:abc
cmd2 = cmdTags.get(2).getCommand();
assertNotNull(cmd2);
assertEquals(PercentType.class, cmd2.getClass());
// BEGIN:Calendar_Test_Number:-0:abc
cmd3 = cmdTags.get(3).getCommand();
assertNotNull(cmd3);
assertEquals(QuantityType.class, cmd3.getClass());
// BEGIN:Calendar_Test_Temperature:0K:abc
cmd4 = cmdTags.get(4).getCommand();
assertNotNull(cmd4);
assertEquals(QuantityType.class, cmd4.getClass());
// BEGIN:Calendar_Test_Pressure:12.3hPa:abc
cmd5 = cmdTags.get(5).getCommand();
assertNotNull(cmd5);
assertEquals(QuantityType.class, cmd5.getClass());
// BEGIN:Calendar_Test_Speed:12.3km/h:abc
cmd6 = cmdTags.get(6).getCommand();
assertNotNull(cmd6);
assertEquals(QuantityType.class, cmd6.getClass());
// BEGIN:Calendar_Test_Player:PLAY:abc
cmd7 = cmdTags.get(7).getCommand();
assertNotNull(cmd7);
assertEquals(PlayPauseType.class, cmd7.getClass());
// BEGIN:Calendar_Test_RollerShutter:50%:abc
cmd8 = cmdTags.get(8).getCommand();
assertNotNull(cmd8);
assertEquals(PercentType.class, cmd8.getClass());
// BEGIN:Calendar_Test_String:Test Series #5:abc
cmd9 = cmdTags.get(9).getCommand();
assertNotNull(cmd9);
assertEquals(StringType.class, cmd9.getClass());
// BEGIN:Calendar_Test_Switch:ON:abc
cmd10 = cmdTags.get(10).getCommand();
assertNotNull(cmd10);
assertEquals(OnOffType.class, cmd10.getClass());
// test bad command tag syntax: Test Series #6
events = calendar3.getJustBegunEvents(Instant.parse("2020-01-28T20:40:00Z"),
Instant.parse("2020-01-28T20:50:00Z"));
assertNotNull(events);
assertEquals(1, events.size());
cmdTags = events.get(0).commandTags;
// Test Series #6 contains only "bad" command tags as follows..
// tags with wrong case prefix..
// begin
// Begin
// BEGIn
// tags that are missing ":" field delimiters..
// BEGIN
// tags with too few field delimiters..
// BEGIN:www
// tags with too many field delimiters..
// BEGIN:www:xxx:yyy:zzz
// tags with an invalid prefix..
// BEGINX:xxx:yyy:zzz
// ENDX:xxx:yyy:zzz
// BEGIN :xxx:yyy:zzz
// BEGIN :xxx:yyy:zzz
// tags with an empty Item Name
// BEGIN::yyy:zzz
// BEGIN: :yyy:zzz
// BEGIN: :yyy:zzz
// tags with bad Item Name
// BEGIN:!:yyy:zzz
// BEGIN:@:yyy:zzz
// BEGIN:£:yyy:zzz
// tags with an empty Target State value
// BEGIN:xxx::zzz
// BEGIN:xxx: :zzz
// BEGIN:xxx: :zzz
// Note: All of the above tags must be rejected! => Assert cmdTags.size() == 0 !
assertEquals(0, cmdTags.size());
// test HTML command tag syntax: Test Series #7
events = calendar3.getJustBegunEvents(Instant.parse("2020-01-28T20:55:00Z"),
Instant.parse("2020-01-28T21:05:00Z"));
assertNotNull(events);
assertEquals(1, events.size());
cmdTags = events.get(0).commandTags;
assertEquals(8, cmdTags.size());
// <p>BEGIN:Calendar_Test_Temperature:12.3°C:abc</p>
cmd0 = cmdTags.get(0).getCommand();
assertNotNull(cmd0);
assertEquals(QuantityType.class, cmd0.getClass());
// <p>END:Calendar_Test_Temperature:23.4°C:abc</p>
cmd1 = cmdTags.get(1).getCommand();
assertNotNull(cmd1);
assertEquals(QuantityType.class, cmd1.getClass());
// <p>BEGIN:Calendar_Test_Switch:ON:abc</p>
cmd2 = cmdTags.get(2).getCommand();
assertNotNull(cmd2);
assertEquals(OnOffType.class, cmd2.getClass());
// <p>END:Calendar_Test_Switch:OFF:abc</p>
cmd3 = cmdTags.get(3).getCommand();
assertNotNull(cmd3);
assertEquals(OnOffType.class, cmd3.getClass());
// <p>BEGIN:Calendar_Test_String:the quick:abc</p>
cmd4 = cmdTags.get(4).getCommand();
assertNotNull(cmd4);
assertEquals(StringType.class, cmd4.getClass());
// <p>END:Calendar_Test_String:brown fox:abc</p>
cmd5 = cmdTags.get(5).getCommand();
assertNotNull(cmd5);
assertEquals(StringType.class, cmd5.getClass());
// </p><p>BEGIN:Calendar_Test_Number:12.3:abc</p>
cmd6 = cmdTags.get(6).getCommand();
assertNotNull(cmd6);
assertEquals(QuantityType.class, cmd6.getClass());
// <p>END:Calendar_Test_Number:23.4:abc</p>
cmd7 = cmdTags.get(7).getCommand();
assertNotNull(cmd7);
assertEquals(QuantityType.class, cmd7.getClass());
}
}

View File

@@ -0,0 +1,45 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
CREATED:20190908T072348Z
LAST-MODIFIED:20190908T072425Z
DTSTAMP:20190908T072425Z
UID:2aefe156-ef9c-4c5e-a9bd-1c0f23174d4a
SUMMARY:Test Event in UTC+2
DTSTART;TZID=Europe/Berlin:20190914T100000
DTEND;TZID=Europe/Berlin:20190914T110000
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
CREATED:20190908T072100Z
LAST-MODIFIED:20190908T072329Z
DTSTAMP:20190908T072329Z
UID:dafba377-5b10-4edc-b536-b2f93677279d
SUMMARY:Test Series in UTC
RRULE:FREQ=DAILY;COUNT=5
EXDATE:20190909T090500Z
DTSTART:20190908T090500Z
DTEND:20190908T091000Z
TRANSP:OPAQUE
SEQUENCE:1
X-MOZ-GENERATION:1
END:VEVENT
END:VCALENDAR

View File

@@ -0,0 +1,125 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:+//IDN bitfire.at//DAVx5/2.2.3.1-ose ical4j/2.2.3
BEGIN:VTIMEZONE
TZID:Europe/Berlin
TZURL:http://tzurl.org/zoneinfo/Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19810329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19160430T230000
RDATE:19160430T230000
RDATE:19170416T020000
RDATE:19180415T020000
RDATE:19400401T020000
RDATE:19430329T020000
RDATE:19440403T020000
RDATE:19450402T020000
RDATE:19460414T020000
RDATE:19470406T030000
RDATE:19480418T020000
RDATE:19490410T020000
RDATE:19800406T020000
END:DAYLIGHT
BEGIN:DAYLIGHT
TZOFFSETFROM:+0200
TZOFFSETTO:+0300
TZNAME:CEMT
DTSTART:19450524T010000
RDATE:19450524T010000
RDATE:19470511T020000
END:DAYLIGHT
BEGIN:DAYLIGHT
TZOFFSETFROM:+0300
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19450924T030000
RDATE:19450924T030000
RDATE:19470629T030000
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19961027T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
BEGIN:STANDARD
TZOFFSETFROM:+005328
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:18930401T000000
RDATE:18930401T000000
END:STANDARD
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19161001T010000
RDATE:19161001T010000
RDATE:19170917T030000
RDATE:19180916T030000
RDATE:19421102T030000
RDATE:19431004T030000
RDATE:19441002T030000
RDATE:19451118T030000
RDATE:19461007T030000
RDATE:19471005T030000
RDATE:19481003T030000
RDATE:19491002T030000
RDATE:19800928T030000
RDATE:19810927T030000
RDATE:19820926T030000
RDATE:19830925T030000
RDATE:19840930T030000
RDATE:19850929T030000
RDATE:19860928T030000
RDATE:19870927T030000
RDATE:19880925T030000
RDATE:19890924T030000
RDATE:19900930T030000
RDATE:19910929T030000
RDATE:19920927T030000
RDATE:19930926T030000
RDATE:19940925T030000
RDATE:19950924T030000
END:STANDARD
BEGIN:STANDARD
TZOFFSETFROM:+0100
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19460101T000000
RDATE:19460101T000000
RDATE:19800101T000000
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTAMP:20191123T214104Z
UID:2bcc7543-27c8-4519-96af-d932fb9a9067
SEQUENCE:3
SUMMARY:Evt
DTSTART;TZID=Europe/Berlin:20190915T110000
DURATION:PT6300S
RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=SU
STATUS:CONFIRMED
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20191123T214104Z
UID:2bcc7543-27c8-4519-96af-d932fb9a9067
RECURRENCE-ID:20191124T100000Z
SEQUENCE:1
SUMMARY:Evt
DTSTART;TZID=Europe/Berlin:20191124T110000
DTEND;TZID=Europe/Berlin:20191124T124500
STATUS:CANCELLED
END:VEVENT
END:VCALENDAR

File diff suppressed because it is too large Load Diff