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,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