[icalendar] Add location channels to read the location field of events (#20150)

* Add location channels to read the location field of events

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2026-01-30 17:44:09 +01:00
committed by GitHub
parent b4841ffdcf
commit a8473e44fb
13 changed files with 174 additions and 22 deletions
@@ -59,9 +59,11 @@ They are all read-only.
| current_title | String | Title of a currently present event |
| current_start | DateTime | Start of a currently present event |
| current_end | DateTime | End of a currently present event |
| current_location | String | Location of a currently present event |
| next_title | String | Title of the next event |
| next_start | DateTime | Start of the next event |
| next_end | DateTime | End of the next event |
| next_location | String | Location of the next event |
| last_update | DateTime | The time and date of the last successful update of the calendar |
### Channels for `eventfilter`
@@ -38,9 +38,11 @@ public class ICalendarBindingConstants {
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_CURRENT_EVENT_LOCATION = "current_location";
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";
public static final String CHANNEL_NEXT_EVENT_LOCATION = "next_location";
public static final String CHANNEL_LAST_UPDATE = "last_update";
public static final ChannelTypeUID LAST_UPDATE_TYPE_UID = new ChannelTypeUID(BINDING_ID, "last_update");
@@ -118,9 +118,11 @@ public class ICalendarHandler extends BaseBridgeHandler implements CalendarUpdat
case CHANNEL_CURRENT_EVENT_TITLE:
case CHANNEL_CURRENT_EVENT_START:
case CHANNEL_CURRENT_EVENT_END:
case CHANNEL_CURRENT_EVENT_LOCATION:
case CHANNEL_NEXT_EVENT_TITLE:
case CHANNEL_NEXT_EVENT_START:
case CHANNEL_NEXT_EVENT_END:
case CHANNEL_NEXT_EVENT_LOCATION:
case CHANNEL_LAST_UPDATE:
if (command instanceof RefreshType) {
updateStates();
@@ -400,12 +402,15 @@ public class ICalendarHandler extends BaseBridgeHandler implements CalendarUpdat
new DateTimeType(currentEvent.start.atZone(tzProvider.getTimeZone())));
updateState(CHANNEL_CURRENT_EVENT_END,
new DateTimeType(currentEvent.end.atZone(tzProvider.getTimeZone())));
updateState(CHANNEL_CURRENT_EVENT_LOCATION,
currentEvent.location.isEmpty() ? UnDefType.UNDEF : new StringType(currentEvent.location));
}
} 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);
updateState(CHANNEL_CURRENT_EVENT_LOCATION, UnDefType.UNDEF);
}
final Event nextEvent = calendar.getNextEvent(now);
@@ -414,10 +419,13 @@ public class ICalendarHandler extends BaseBridgeHandler implements CalendarUpdat
updateState(CHANNEL_NEXT_EVENT_START,
new DateTimeType(nextEvent.start.atZone(tzProvider.getTimeZone())));
updateState(CHANNEL_NEXT_EVENT_END, new DateTimeType(nextEvent.end.atZone(tzProvider.getTimeZone())));
updateState(CHANNEL_NEXT_EVENT_LOCATION,
nextEvent.location.isEmpty() ? UnDefType.UNDEF : new StringType(nextEvent.location));
} else {
updateState(CHANNEL_NEXT_EVENT_TITLE, UnDefType.UNDEF);
updateState(CHANNEL_NEXT_EVENT_START, UnDefType.UNDEF);
updateState(CHANNEL_NEXT_EVENT_END, UnDefType.UNDEF);
updateState(CHANNEL_NEXT_EVENT_LOCATION, UnDefType.UNDEF);
}
final Instant lastUpdate = calendarDownloadedTime;
@@ -427,7 +427,9 @@ class BiweeklyPresentableCalendar extends AbstractPresentableCalendar {
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);
final Location eventLocation = vEvent.getLocation();
final String location = eventLocation != null ? eventLocation.getValue() : "";
return new Event(title, start, end, description, location);
}
}
}
@@ -31,11 +31,13 @@ public class Event implements Comparable<Event> {
public final Instant end;
public final Instant start;
public final String title;
public final String location;
public Event(String title, Instant start, Instant end, String description) {
public Event(String title, Instant start, Instant end, String description, String location) {
this.title = title;
this.start = start;
this.end = end;
this.location = location;
if (description.isEmpty()) {
return;
@@ -67,7 +69,16 @@ public class Event implements Comparable<Event> {
}
final Event otherEvent = (Event) other;
return (this.title.equals(otherEvent.title) && this.start.equals(otherEvent.start)
&& this.end.equals(otherEvent.end));
&& this.end.equals(otherEvent.end) && this.location.equals(otherEvent.location));
}
@Override
public int hashCode() {
int result = title.hashCode();
result = 31 * result + start.hashCode();
result = 31 * result + end.hashCode();
result = 31 * result + location.hashCode();
return result;
}
@Override
@@ -13,12 +13,18 @@
<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="current_location" typeId="event_current_location"/>
<channel id="next_title" typeId="event_next_title"/>
<channel id="next_start" typeId="event_next_start"/>
<channel id="next_end" typeId="event_next_end"/>
<channel id="next_location" typeId="event_next_location"/>
<channel id="last_update" typeId="last_update"/>
</channels>
<properties>
<property name="thingTypeVersion">2</property>
</properties>
<config-description>
<parameter-group name="source">
<label>Source Settings</label>
@@ -60,7 +66,6 @@
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>
<channel-type id="event_current_title">
@@ -102,6 +107,15 @@
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="event_current_location">
<item-type>String</item-type>
<label>Current Event Location</label>
<description>Location of the currently present event</description>
<tags>
<tag>Status</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="event_next_title">
<item-type>String</item-type>
<label>Next Event Title</label>
@@ -131,6 +145,15 @@
</tags>
<state readOnly="true" pattern="%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS"/>
</channel-type>
<channel-type id="event_next_location">
<item-type>String</item-type>
<label>Next Event Location</label>
<description>Location of the next event in calendar</description>
<tags>
<tag>Status</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="result_start">
<item-type>DateTime</item-type>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
<thing-type uid="icalendar:calendar">
<instruction-set targetVersion="2">
<add-channel id="current_location">
<type>icalendar:event_current_location</type>
</add-channel>
<add-channel id="next_location">
<type>icalendar:event_next_location</type>
</add-channel>
</instruction-set>
</thing-type>
</update:update-descriptions>
@@ -12,7 +12,12 @@
*/
package org.openhab.binding.icalendar.internal.logic;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.FileInputStream;
import java.io.IOException;
@@ -50,6 +55,7 @@ public class BiweeklyPresentableCalendarTest {
private AbstractPresentableCalendar calendar_issue9647;
private AbstractPresentableCalendar calendar_issue10808;
private AbstractPresentableCalendar calendar_issue11084;
private AbstractPresentableCalendar calendar_location;
@BeforeEach
public void setUp() throws IOException, CalendarException {
@@ -62,6 +68,8 @@ public class BiweeklyPresentableCalendarTest {
new FileInputStream("src/test/resources/test-issue10808.ics"));
calendar_issue11084 = new BiweeklyPresentableCalendar(
new FileInputStream("src/test/resources/test-issue11084.ics"));
calendar_location = new BiweeklyPresentableCalendar(
new FileInputStream("src/test/resources/test-location.ics"));
}
/**
@@ -591,30 +599,31 @@ public class BiweeklyPresentableCalendarTest {
public void testGetFilteredEventsBetween() {
Event[] expectedFilteredEvents1 = new Event[] {
new Event("Test Series in UTC", Instant.parse("2019-09-12T09:05:00Z"),
Instant.parse("2019-09-12T09:10:00Z"), ""),
Instant.parse("2019-09-12T09:10:00Z"), "", ""),
new Event("Test Event in UTC+2", Instant.parse("2019-09-14T08:00:00Z"),
Instant.parse("2019-09-14T09:00:00Z"), "") };
Instant.parse("2019-09-14T09:00:00Z"), "", "") };
List<Event> realFilteredEvents1 = calendar.getFilteredEventsBetween(Instant.parse("2019-09-12T06:00:00Z"),
Instant.parse("2019-09-15T06:00:00Z"), null, 3);
assertArrayEquals(expectedFilteredEvents1, realFilteredEvents1.toArray(new Event[0]));
Event[] expectedFilteredEvents2 = new Event[] {
new Event("Evt", Instant.parse("2019-11-10T10:00:00Z"), Instant.parse("2019-11-10T11:45:00Z"), ""),
new Event("Evt", Instant.parse("2019-11-17T10:00:00Z"), Instant.parse("2019-11-17T11:45:00Z"), ""),
new Event("Evt", Instant.parse("2019-12-01T10:00:00Z"), Instant.parse("2019-12-01T11:45:00Z"), "") };
new Event("Evt", Instant.parse("2019-11-10T10:00:00Z"), Instant.parse("2019-11-10T11:45:00Z"), "", ""),
new Event("Evt", Instant.parse("2019-11-17T10:00:00Z"), Instant.parse("2019-11-17T11:45:00Z"), "", ""),
new Event("Evt", Instant.parse("2019-12-01T10:00:00Z"), Instant.parse("2019-12-01T11:45:00Z"), "",
"") };
List<Event> realFilteredEvents2 = calendar2.getFilteredEventsBetween(Instant.parse("2019-11-08T06:00:00Z"),
Instant.parse("2019-12-31T06:00:00Z"), null, 3);
assertArrayEquals(expectedFilteredEvents2, realFilteredEvents2.toArray(new Event[] {}));
Event[] expectedFilteredEvents3 = new Event[] { new Event("Test Event in UTC+2",
Instant.parse("2019-09-14T08:00:00Z"), Instant.parse("2019-09-14T09:00:00Z"), "") };
Instant.parse("2019-09-14T08:00:00Z"), Instant.parse("2019-09-14T09:00:00Z"), "", "") };
List<Event> realFilteredEvents3 = calendar.getFilteredEventsBetween(Instant.parse("2019-09-12T06:00:00Z"),
Instant.parse("2019-09-15T06:00:00Z"),
new EventTextFilter(EventTextFilter.Field.SUMMARY, "utc+2", EventTextFilter.Type.TEXT), 3);
assertArrayEquals(expectedFilteredEvents3, realFilteredEvents3.toArray(new Event[] {}));
Event[] expectedFilteredEvents4 = new Event[] { new Event("Test Series in UTC",
Instant.parse("2019-09-12T09:05:00Z"), Instant.parse("2019-09-12T09:10:00Z"), "") };
Instant.parse("2019-09-12T09:05:00Z"), Instant.parse("2019-09-12T09:10:00Z"), "", "") };
List<Event> realFilteredEvents4 = calendar.getFilteredEventsBetween(Instant.parse("2019-09-12T06:00:00Z"),
Instant.parse("2019-09-15T06:00:00Z"),
new EventTextFilter(EventTextFilter.Field.SUMMARY, ".*UTC$", EventTextFilter.Type.REGEX), 3);
@@ -635,9 +644,9 @@ public class BiweeklyPresentableCalendarTest {
Event[] expectedFilteredEvents8 = new Event[] {
new Event("Restabfall", LocalDate.parse("2021-01-04").atStartOfDay(ZoneId.systemDefault()).toInstant(),
LocalDate.parse("2021-01-05").atStartOfDay(ZoneId.systemDefault()).toInstant(), ""),
LocalDate.parse("2021-01-05").atStartOfDay(ZoneId.systemDefault()).toInstant(), "", ""),
new Event("Gelbe Tonne", LocalDate.parse("2021-01-04").atStartOfDay(ZoneId.systemDefault()).toInstant(),
LocalDate.parse("2021-01-05").atStartOfDay(ZoneId.systemDefault()).toInstant(), "") };
LocalDate.parse("2021-01-05").atStartOfDay(ZoneId.systemDefault()).toInstant(), "", "") };
List<Event> realFilteredEvents8 = calendar_issue9647.getFilteredEventsBetween(
LocalDate.parse("2021-01-04").atStartOfDay(ZoneId.systemDefault()).toInstant(),
LocalDate.parse("2021-01-05").atStartOfDay(ZoneId.systemDefault()).toInstant(), null, 3);
@@ -647,4 +656,46 @@ public class BiweeklyPresentableCalendarTest {
Instant.parse("2021-08-16T16:45:00.123456Z"), Instant.parse("2021-08-16T16:46:00.768643Z"), null, 3);
assertEquals(0, realFilteredEvents9.size());
}
/**
* Tests location field extraction from events.
*/
@Test
public void testEventLocation() {
// Test event with location
Event eventWithLocation = calendar_location.getCurrentEvent(Instant.parse("2024-02-01T10:30:00Z"));
assertNotNull(eventWithLocation);
assertEquals("Meeting with Location", eventWithLocation.title);
assertEquals("Conference Room A", eventWithLocation.location);
// Test event without location
Event eventWithoutLocation = calendar_location.getCurrentEvent(Instant.parse("2024-02-01T14:30:00Z"));
assertNotNull(eventWithoutLocation);
assertEquals("Event without Location", eventWithoutLocation.title);
assertEquals("", eventWithoutLocation.location);
// Test event with URL location
Event eventWithUrlLocation = calendar_location.getNextEvent(Instant.parse("2024-02-01T14:30:00Z"));
assertNotNull(eventWithUrlLocation);
assertEquals("Virtual Meeting", eventWithUrlLocation.title);
assertEquals("https://example.com/meeting", eventWithUrlLocation.location);
}
/**
* Tests that location is preserved across next event retrieval.
*/
@Test
public void testNextEventLocation() {
// Get next event from before first event
Event nextEvent = calendar_location.getNextEvent(Instant.parse("2024-02-01T09:00:00Z"));
assertNotNull(nextEvent);
assertEquals("Meeting with Location", nextEvent.title);
assertEquals("Conference Room A", nextEvent.location);
// Get next event after first event
Event nextEventAfterFirst = calendar_location.getNextEvent(Instant.parse("2024-02-01T11:00:00Z"));
assertNotNull(nextEventAfterFirst);
assertEquals("Event without Location", nextEventAfterFirst.title);
assertEquals("", nextEventAfterFirst.location);
}
}
@@ -48,7 +48,7 @@ public class MultiDayEventSearchByActiveTest {
@Test
public void eventWithTime() {
Event expectedFilteredEvent = new Event("Multi-day test event with time", Instant.parse("2023-12-05T09:00:00Z"),
Instant.parse("2023-12-07T15:00:00Z"), "");
Instant.parse("2023-12-07T15:00:00Z"), "", "");
assertThat("Day before event starts",
calendar.getFilteredEventsBetween(Instant.parse("2023-12-04T00:00:00Z"),
@@ -99,7 +99,7 @@ public class MultiDayEventSearchByActiveTest {
@Test
public void eventWithoutTime() {
Event expectedFilteredEvent = new Event("Multi-day test event without time", localDateAsInstant("2023-12-12"),
localDateAsInstant("2023-12-15"), "");
localDateAsInstant("2023-12-15"), "", "");
assertThat("Day before event starts", //
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-11"), localDateAsInstant("2023-12-12"),
@@ -48,7 +48,7 @@ public class MultiDayEventSearchByEndTest {
@Test
public void eventWithTime() {
Event expectedFilteredEvent = new Event("Multi-day test event with time", Instant.parse("2023-12-05T09:00:00Z"),
Instant.parse("2023-12-07T15:00:00Z"), "");
Instant.parse("2023-12-07T15:00:00Z"), "", "");
assertThat("Day before event starts",
calendar.getFilteredEventsBetween(Instant.parse("2023-12-04T00:00:00Z"),
@@ -89,7 +89,7 @@ public class MultiDayEventSearchByEndTest {
@Test
public void eventWithoutTime() {
Event expectedFilteredEvent = new Event("Multi-day test event without time", localDateAsInstant("2023-12-12"),
localDateAsInstant("2023-12-15"), "");
localDateAsInstant("2023-12-15"), "", "");
assertThat("Day before event starts", //
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-11"), localDateAsInstant("2023-12-12"),
@@ -48,7 +48,7 @@ public class MultiDayEventSearchByJustEndedTest {
@Test
public void eventWithTime() {
Event expectedFilteredEvent = new Event("Multi-day test event with time", Instant.parse("2023-12-05T09:00:00Z"),
Instant.parse("2023-12-07T15:00:00Z"), "");
Instant.parse("2023-12-07T15:00:00Z"), "", "");
assertThat("Day before event starts",
calendar.getFilteredEventsBetween(Instant.parse("2023-12-04T00:00:00Z"),
@@ -89,7 +89,7 @@ public class MultiDayEventSearchByJustEndedTest {
@Test
public void eventWithoutTime() {
Event expectedFilteredEvent = new Event("Multi-day test event without time", localDateAsInstant("2023-12-12"),
localDateAsInstant("2023-12-15"), "");
localDateAsInstant("2023-12-15"), "", "");
assertThat("Day before event starts", //
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-11"), localDateAsInstant("2023-12-12"),
@@ -48,7 +48,7 @@ public class MultiDayEventSearchByStartTest {
@Test
public void eventWithTime() {
Event expectedFilteredEvent = new Event("Multi-day test event with time", Instant.parse("2023-12-05T09:00:00Z"),
Instant.parse("2023-12-07T15:00:00Z"), "");
Instant.parse("2023-12-07T15:00:00Z"), "", "");
assertThat("Day before event starts",
calendar.getFilteredEventsBetween(Instant.parse("2023-12-04T00:00:00Z"),
@@ -89,7 +89,7 @@ public class MultiDayEventSearchByStartTest {
@Test
public void eventWithoutTime() {
Event expectedFilteredEvent = new Event("Multi-day test event without time", localDateAsInstant("2023-12-12"),
localDateAsInstant("2023-12-15"), "");
localDateAsInstant("2023-12-15"), "", "");
assertThat("Day before event starts", //
calendar.getFilteredEventsBetween(localDateAsInstant("2023-12-11"), localDateAsInstant("2023-12-12"),
@@ -0,0 +1,36 @@
BEGIN:VCALENDAR
PRODID:-//Test//Test//EN
VERSION:2.0
BEGIN:VEVENT
CREATED:20240101T120000Z
LAST-MODIFIED:20240101T120000Z
DTSTAMP:20240101T120000Z
UID:test-location-event-1
SUMMARY:Meeting with Location
LOCATION:Conference Room A
DTSTART:20240201T100000Z
DTEND:20240201T110000Z
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
CREATED:20240101T120000Z
LAST-MODIFIED:20240101T120000Z
DTSTAMP:20240101T120000Z
UID:test-location-event-2
SUMMARY:Event without Location
DTSTART:20240201T140000Z
DTEND:20240201T150000Z
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
CREATED:20240101T120000Z
LAST-MODIFIED:20240101T120000Z
DTSTAMP:20240101T120000Z
UID:test-location-event-3
SUMMARY:Virtual Meeting
LOCATION:https://example.com/meeting
DTSTART:20240202T100000Z
DTEND:20240202T110000Z
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR