Some time has passed since my previous post about fixing the issue that a Tesla does not allow scheduling preconditioning (de-icing) the car prior to a drive in the winter. The original post used an Azure Logic app as its core.
What you see above is a Logic app for my car only; the first step “When an event starts” is linked to a Tesla calendar in my personal Google Calendar. The “StartHVAC” and “EndHVAC” call out to serverless functions that hardcoded contain the data for my personal car. And the emails go to a hardcoded email address of mine. Totally not reusable, but working.
After publishing that post, people started to ask if they could use the same functionality as well. Ahm, I did just say it was not reusable, didn’t I? But being a good friend, I ventured out and rewrote the whole thing to support multiple calendars and cars.
The first thing to do is to make all those hardcoded values dynamic, and the smartest way to do that is to move them into a database. We end up with a database containing the following data:
- URL of a Google calendar
- Email address
- VIN number of the car
- Tesla username and password
Naturally this can be normalized into an account table (tesla account information), and a car table (calendar, VIN and email address).
Since the Logic app cannot trigger on “When an event starts” of many calendars, that had to be rewritten to a timer simply triggering every 3 minutes, and then code to check if an event has started in any of the calendars.
Also I did not like the fact that once an event start was found, it used “delay until” to sit and wait. This was born out of a missing “When an event ended” trigger in the Logic app’s arsenal, but I do not like having long waiting tasks; things may change once the user receives the email that the HVAC has started, but that task will always continue after an hour or so. Given I was going to write code to detect starting of events anyhow, it would be easy to also detect the ending of events. But that changes the workflow fundamentally; the ending is no longer something that happens after a start, but has become a trigger on its own.
And last but not least, waking up a Tesla turned out not to be a predictable process. In the intro I used the words “but working” instead of “working great” for a reason; if the car is in deep sleep, it can take minutes before it replies, causing an HTTP time-out way before then. And when it replies, and you ask it for it shift state (we don’t want to meddle with a car that is driving), you may get empty responses. So the code to wake up a Tesla became quite a piece of work.
As time progressed more and more logic moved from the Logic app to the serverless functions, until the Logic app was nothing more than a glorified loop:
Mind you, this still served a purpose, because the “For each” calls a serverless function and thus automatic scaling and load distribution provided by Azure is used here!
One remaining issue eventually killed the Logic app; it is not checked into my GIT repo. And I really like a consistent and complete source control revision system. So I decided to replace it with a time-triggered serverless function:
public class TimerBasedTriggerToKickOffScanningEachCar { /** * This method simply triggers the serverless function for a single calendar/car combination. * It replaces an Azure LogicFlow app, because that is not easily checked in to GIT. */ @FunctionName("TimerBasedTriggerToKickOffScanningEachCar") public void run( @TimerTrigger(name = "TimerBasedTriggerToKickOffScanningEachCar", schedule = "0 0/3 * * * *") String timerInfo , final ExecutionContext context ) { U.setExecutionContext(context, ""); U.info(timerInfo); Gson gson = new Gson(); MediaType JsonMediaType = MediaType.parse("application/json; charset=utf-8"); OkHttpClient okHttpClient = getOkHttpClient(); try { // scan all cars final List cars = R.car.findWithGoogleCalendar(); for (final Car car : cars) { final String carId = car.getAccount().getTeslaUsername() + " " + car.getTeslaVin(); final String logPrefix = carId + ": " ; // pass the handling off to the serverless function for a single car (this allows for scaling handled by Azure) ScanGoogleAndTriggerVehicleFunction.Param param = new ScanGoogleAndTriggerVehicleFunction.Param(); param.setTeslaUsername(car.getAccount().getTeslaUsername()); param.setTeslaVin(car.getTeslaVin()); String requestBody = gson.toJson(param); // Build the request Request request = new Request.Builder() .url(ScanGoogleAndTriggerVehicleFunction.URL) .header(ScanGoogleAndTriggerVehicleFunction.HEADER_ID, ScanGoogleAndTriggerVehicleFunction.HEADER_VALUE) .method("POST", RequestBody.create(JsonMediaType, requestBody)) .build(); // Execute async because we're not really interested in the result okHttpClient.newCall(request).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) throws IOException { // Get response body and print if not in success range int responseCode = response.code(); if (responseCode < 200 || responseCode > 299) { System.out.println(logPrefix + "response=" + response); } } @Override public void onFailure(Call call, IOException e) { System.out.println(logPrefix + e); e.printStackTrace(System.out); } }); } } catch (Exception e) { U.debug(ExceptionUtils.getStackTrace(e)); } finally { U.clearExecutionContext(); } } /** * */ public OkHttpClient getOkHttpClient() { OkHttpClient okHttpClient = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .readTimeout(2, TimeUnit.MINUTES) .build(); return okHttpClient; } }
Now that is working great :-D.
I on purpose did not mask the fact that HTTP is used to call out to another function, it could have been put into the ScanGoogleAndTriggerVehicleFunction class, in order to not give the impression that this is just a simple in-memory call.
In the next post(s) we’ll take a look at the code that does the actual heavy lifting, and I’ll write something about the experience with developing on Azure, but if you want to see the end result; take a look at TeslaTasks. Mind you, to save money (because Azure ain’t free), it is running on the lowest performance settings of Azure, so it may take a bit to get going.