Scheduling things on a Tesla using Azure pt. 4; Google calendar

Having made good progress on the Tesla side of the implementation, it’s time to take a look at how to implement the calendar integration. But first, let’s examine why to use a calendar in the first place. After all, there are many solutions that implement similar functionality, for example using timers (like Tesla does in the car).

Personally, to be honest, I would be totally lost if it were not for a digital calendar. Google calendar to be precise. I forget things, so I register everything in it. Not everything needs to send out reminders, but just not forgetting that my son has his basketball training every Tuesday and Thursday evening, prevents me from making some kind of commitment that will cause conflicts.

In other words: my life is in that calendar. That means also the things I need to get into my car for. So my calendar seems like the perfect place to also administer when I need my car to prepare itself for driving, by defrosting or whatever. And if an appointment changes, I immediately see that that part also needs to change. Like generating documentation from code.

But not only the proximity of the reason why the car needs to do something makes total sense to me, but I can also manage that from everywhere; my computer, my phone, everywhere I can access that calendar. Unlike the timer feature of the car itself, for which I physically need to be inside the car to set it up, or modify it. And I really do not like getting in the cold, to prevent having to be in the cold the next morning.

Reacting to appointments

So how do we react to appointments starting and stopping in Google calendar? Well, basically this is event based programming, so ideally Google calendar would have some kind of call outs, or webhooks as GITHUB calls them. Taking a look at the Google calendar API, reveals that even though it supports events, all of them have to do with changes to the appointments, not the actual moment one starts or ends.

This is also reflected in, for example, the integration of Google calendar in Azure’s logic flow; it can detect the start of an event, but does so by polling Google calendar every 3 minutes. This is not documented, but becomes clear once you create a flow app triggered from Google calendar:

So polling is the way to go, apparently. For what we are trying to do, every 3 minutes seems like a good frequency. It hits the 00, 15, 30 and 45 marks of every hour exactly, which are common start times. And if a deviating time is used, 3 minutes are not too inaccurate considering the actions that are triggered (it is not like defrosting must start on the minute exactly, otherwise the windows will be still frozen).

The iCal standard

Even though I’m using Google calendar, it would be nice that it is at least easily possible to also support other calendars. So an implementation should try to use some kind of standard, and for calendars that is iCal or RFC5545. Google also provides its calendar in iCal format: in the settings and sharing part of a calendar, there are two URLs that will provide that information. One for when a calendar is explicitly set to public, one for direct full access. Give that there is no reason why the calendar should be public, the second URL suits our needs best.

So what we need to do is request the secret URL every three minutes and see if an appointment has started or ended in the last 3 minutes. Or better; since the last check, that will be more resilient if something goes wrong. And if that is the case, then whatever commands are in the appointment’s description need to be executed.

iCal4j

Parsing iCal data is not that difficult, so writing it yourself is very doable.

BEGIN:VEVENT
DTSTART;TZID=Europe/Rome:20200204T073000
DTEND;TZID=Europe/Rome:20200204T083000
RRULE:FREQ=WEEKLY;BYDAY=TU
DTSTAMP:20200223T082458Z
UID:5hod3mjh4pnt7b987@google.com
CREATED:20200202T092738Z
DESCRIPTION:
LAST-MODIFIED:20200212T072008Z
LOCATION:
SEQUENCE:3
STATUS:CONFIRMED
SUMMARY:precond
TRANSP:OPAQUE
END:VEVENT

There is a start and end time in there (DTSTART & DTEND), the thing we would like the car to do (SUMMARY), so it seems quite straight forward. Only that repeating stuff (RRULE) is a bit complex to do. But Java wouldn’t be Java if there isn’t an open-source library available, in this case the most used is iCal4j.

There is a gotcha: if you simply parse the iCal data, the second or more iteration of repeating appointments are not taken into account, only the first or initial date and time. So when parsing the iCal data you need to specify which period you want to examine, in order for iCal4j to determine if repeating appointments are present in that period.

However, exceptions to repeating appointments are a thing; you need some manual coding to resolve those (see ComponentGroup). I kinda expected iCal4j to have a more elegant solution for that, but hey, at least there is a fairly simple one. #notcomplaining

You do need ical4j version 3.0.15 or higher, the versions before did not handle exceptions to a recurring range at all.

// The milliseconds cause problems
timespanStart = timespanStart.truncatedTo(ChronoUnit.SECONDS);
timespanEnd = timespanEnd.truncatedTo(ChronoUnit.SECONDS);

// Initialize the iCal parser and fetch the calendar from Google
CalendarBuilder builder = new CalendarBuilder();
URLConnection connection = publicURL.openConnection();
Calendar calendar = builder.build(connection.getInputStream());

// This is to prevent exceptions to recuring appointments causing duplicate events
List<Uid> processedEvents = new ArrayList<>();

// For all components
for (CalendarComponent component : calendar.getComponents()) {
	if (!(component instanceof VEvent)) {
		continue;
	}
	VEvent vEvent = (VEvent)component;

	// The loop may provide the same vEvent more than once: 1x for the recurance, plus 1x for each exception
	// So we need to make sure we only process once.
	if (processedEvents.contains(vEvent.getUid())) {
		continue;
	}
	processedEvents.add(vEvent.getUid());

	// This will merge any exceptions with the recuring appointments
	ComponentGroup<CalendarComponent> group = new ComponentGroup<>(calendar.getComponents(), vEvent.getUid());

	// Determine if there is an recurrence within the time span
	Period period = new Period(new DateTime(Date.from(timespanStart)), 		
		new DateTime(Date.from(timespanEnd)));
	PeriodList recurrenceSet = group.calculateRecurrenceSet(period);
	for (Period recurrencePeriod : recurrenceSet) {
		
		// Get start and end of the recurrence
		Instant appointmentStart = recurrencePeriod.getStart()
			.toInstant().truncatedTo(ChronoUnit.SECONDS);
		Instant appointmentEnd = recurrencePeriod.getEnd()
			.toInstant().truncatedTo(ChronoUnit.SECONDS);
		
		// Decide what the appointment did within the interval,
		// if anything: create an event 
		boolean appointmentStartedInTimespan = 
			inTimespan(timespanStart, timespanEnd, appointmentStart);
		boolean appointmentEndedInTimespan = 
			inTimespan(timespanStart, timespanEnd, appointmentEnd);

		if (appointmentStartedInTimespan && appointmentEndedInTimespan) {
			events.add(new Event(Type.CANCELLED_OUT,
				appointmentStart, summary));
		}
		else if (appointmentStartedInTimespan) {
			events.add(new Event(Type.STARTED, 
				appointmentStart, summary));
		}
		else if (appointmentEndedInTimespan) {
			events.add(new Event(Type.ENDED, 
				appointmentEnd, summary));
		}
	}
}

The ‘time span’ in this case is the 3 minute interval in which we check if an event either started or ended.

Using this logic, all events -repeating or not- are found that may have an impact. If the event starts and stops within the same time span, the start and stop cancel each other out and nothing happens. If not, we need to analyse what the event wants to happen, by parsing the contents of the subject. And for that we get to the next blog, part 5; parsing the command.

So far we have seen how we got from Azure logic for a single car (pt. 1), to Azure functions supporting many cars (pt. 2), to how to use the Tesla API (pt. 3) and connecting to a Google calendar (pt. 4). The next blog will connect the calendar to the Tesla API by parsing the subject of the event.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.