Scheduling things on a Tesla using Azure pt. 5; parsing the action

We’re down to the last piece of the puzzel; calendars are examined in a regular interval, we can tell the cars what to do, but those two need to be connected.

It is very practical to quickly see in a calendar what will be happening, and the summary field of an appointment is rendered in any view, so that is the best field to use. Other fields like description are often only visible in a detail view. Below is how I’ve currently setup my Tesla this February (which is winter).

My car goes into a security cam mode every night between 2 and 6 am, by activating sentry mode at home. And I’m making sure the doors are locked and the sun roof is closed. I could also schedule charging at that time, but I want my car to be fully charged ASAP. At weekdays I’m preconditioning the car early in the morning, which means heating up the battery and the cabine. We have a lame winter this year, so that suffices to remove any ice on the windows, but if it were really cold I’d put in “defrost”. You can see that my current schedule takes me to a different project on Tuesdays, with less driving time, so I can leave later.

The text is in the summary field, and it will trigger the actions that need to be send to the car. The first idea on how to implement this would be a simple “contains” operation on the string. And yes, that is what did I as well. But that quickly becomes a problem once you need some parameters, like a charge level.

So the second iteration would be to use regular expressions. That solves the parameter thing, but it still is fragile; you have to be very aware that the patterns for the different commands, scattered all through the code, do not overlap.

But basically this is compiler stuff, and compilers do not use regular expressions. At least not simply, but those are wrapped in a lexical analyzer and parser. In Java, compiler stuff is done using ANTLR. ANTLR uses a DSL, or grammar, to define what to match and how to react.

A typical ANTLR grammar consists of three parts; the lexical analyzer, which tries to convert a stream of characters to tokens, and the parser, which tries to convert the stream of tokens to… yeah… output. That is what is defined by the program using ANTLR, the third part, the output. Often compilers generate a new file, compiled code as an executable, or a JVM class file. But in this case the output will be calls to the Tesla API.

So without further ado, the ANTLR grammar:

// PARSER RULES
actions : ( action (SEP action)* EOF
          | EOF 										{ this.actionHandler.precond(); }
          );

action : ( COMMAND ' windows'   						{ this.actionHandler.doWindows($COMMAND.text); }
         | COMMAND ' roof'      						{ this.actionHandler.doRoof($COMMAND.text); }
         | 'lock doors'         						{ this.actionHandler.lockDoors(); }
         | 'sentry' (' mode')?         					{ this.actionHandler.activateSentryMode(); }
         | 'charge ' i1=NUM '%' 
           (' then ' i2=NUM '%')? 						{ this.actionHandler.setChargeLevel(toInteger($i1.text)); this.actionHandler.setChargeLevelAfter(toInteger($i2.text)); this.actionHandler.charge(); }
         | 'charge' 									{ this.actionHandler.charge(); }
         | 'defrost' 									{ this.actionHandler.defrost(); }
         | 'precond ' n1=NUM u1=TEMPUNIT? 
           (' then ' n2=NUM u2=TEMPUNIT?)? 				{ this.actionHandler.setT(toDouble($n1.text), $u1.text); this.actionHandler.setTA(toDouble($n2.text), $u2.text); precond(); }
         | 'precond' 									{ this.actionHandler.precond(); }
         | ('temp' | 't') '=' NUM 						{ this.actionHandler.setT(toDouble($NUM.text), null); precond(); }
         | ('tempafter' | 'ta') '=' NUM 				{ this.actionHandler.setTA(toDouble($NUM.text), null); precond(); }
         | 'silent' 									{ this.actionHandler.silent(); }
         | 'unmute' 									{ this.actionHandler.unmute(); }
         | 'mute' 										{ this.actionHandler.mute(); }
         );

// LEXER RULES
SEP : WS* (','|' ') WS*;
WS : ( '\t' | ' ' | '\r' | '\n'| '\u000C' )+;
NUM : ('0'..'9')+ ('.' ('0'..'9')+)?;
TEMPUNIT : ('c'|'f');
COMMAND : ('a'..'z'|'_')('a'..'z'|'0'..'9'|'_')* ;

The lexer rules are the the bottom, because ANTLR tries to match things from the top down. The lexer rules basically are regular expressions trying to match patterns in the stream of characters.

The parser rules above them is what brings the structure to it all. What you see on line 2 is that actions consist of a single action, followed by zero or more times (the *) a separator (SEP) and another action, until the end (EOF, end-of-file) is reached.

What an action is, we’ll see soon. First it is interesting what happens in line 3. There a second branch (the | symbol means “or”) for actions is defined; an empty summary string, simply directly an EOF. This is the default behavior if you put nothing in the summary of an appointment. Scrolling to the right you see that when that 3rd line is matched, an action is executed: this.actionHandler.precond();

Starting at line 6 an action is defined, which basically is a long list of alternatives. On line 6 a command for a window is handled, but the grammar does not define what is allowed. The lexical rule on line 29 simply defines command as a series of letters, digits or underscores, starting with a letter. Maybe it should define what is allowed, because only open and close make sense. This originated from the fact that the command is a parameter in the Tesla API, but the grammar could be more strict. Making a note to myself to investigate. Further right on line 6 you see that the command is available as $COMMAND to hand over the the action method.

And the rest of the lines are variations on this. Line 9 handles sentry with an optional mode text, so you can type either. Line 12 handles charge as a lone word, but line 10 does charge with a percentage and an optional then clause. The order here is important: charge-without-percentage needs to come after the one with percentage, otherwise it will be matched first, and then the percentage and then will cause parser errors.

When the code generated by the ANTLR grammar finds something that needs to be done, it needs to communicate that somehow. The grammar is encapsulated; its job is to parse, makes sense of the text, and then tell someone what to do. It does not know of the outside world. For that the grammar defines an interface at the top of the grammar file:

@members
{
	public static interface ActionHandler {
		void doWindows(String action);
		void doRoof(String action);
		void lockDoors();
		void activateSentryMode();
		void setChargeLevel(Integer percent);
		void setChargeLevelAfter(Integer percent);
		void charge();
		void defrost();
		void precond();
		void setT(Double temp, String unit);
		void setTA(Double temp, String unit);
		void silent();
		void mute();
		void unmute();
	}
	
	/** */
	public void setActionHandler(ActionHandler v) {
		this.actionHandler = v;
	}
	public ActionHandler getActionHandler() {
		return this.actionHandler;
	}
	private ActionHandler actionHandler = null;
}

All the actions that the grammar can detect are defined as methods, for example the precond that is called when matching an empty summary on line 3 in the grammar. But on line 16 of the grammar it is called as well, when precond is explicitly present in the text; multiple paths in the grammar can lead to the same method being called.

So the grammar reduces the text to commands, and the code calling the parser needs listen to those commands by implementing the interface. Below, line 1 puts the text in the lexer, line 4 creates the parser, line 5 registers the implementation of the interface, and finally line 39 actually starts parsing.

CodePointCharStream input = CharStreams.fromString(eventDescription);
ActionsLexer lexer = new ActionsLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
ActionsParser actionsParser = new ActionsParser(tokens);
actionsParser.setActionHandler(new ActionsParser.ActionHandler() {
	
	@Override
	public void defrost() {
		//...
	}
	
	@Override
	public void precond() {
		//...
	}
	
	@Override
	public void charge() {
		//...
	}
	
	@Override
	public void setChargeLevel(Integer percent) {
		//...
	}
	
	@Override
	public void activateSentryMode() {
		//...
	}
	
	@Override
	public void lockDoors() {
		//...
	}

	//...	
});
actionsParser.actions();

Now, there are details omitted here, like that we won’t want to meddle with the car when it is driving. This is of course handled by the interface implementation; there the state of the car is known, the parser does not need to know. But there are many more small things that need to be fiddled with to make it work just right.

But all in all, that’s it: every 3 minutes get the ical version of a google calendar, parse it using ical4j, give the summary of the active appointments to the ANTLR parser, and when commands are found, call out to the Tesla API. And every morning I’m getting from a warm bed into a warm car, with a clear windshield.

Life is good.

Feel free to try it yourself, I’ve made the functionality available for free. At least as long as the hosting costs are acceptable (one of the advantages of using pay-by-use of a cloud platform). A next blog will be about how to develop those Azure serverless functions, because coding the directly on the cloud is not very practical. And of course there is a webapp written in Vaadin, where users need to enter their data to setup the connection between Google calendar and their Tesla, which might be worth inspecting.

Leave a Reply

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