added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
5033
bundles/org.openhab.binding.icalendar/src/test/resources/test3.ics
Normal file
5033
bundles/org.openhab.binding.icalendar/src/test/resources/test3.ics
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user