A State Machine Primer with HABlladin, the openHAB Genie

Tags: #<Tag:0x00007fc1f97eb680> #<Tag:0x00007fc1f97eb568>

(Gunzips and rubs openHAB TARball. Out pops HABlladin, the openHAB genie.)

Hey, how’s it going?! What is is you’d like? You get three wishes.

Well, it’s simple, really. When I come home after dark and open the garage door, I want the overhead lights to come on because the lamps in the garage door openers are pitiful at best.

OK, that’s easy. Here’s a script that senses the door position, and when it opens and it’s after sunset, it turns on the lights. Done. Wish number two?

Well… ah, I’d like the garage lights to go off automatically, too, just like the ones in the garage door opener.

Well why didn’t you say so? Here, we’ll just add a timer to turn them off, and now you’re all set! Wish number three?

But if there’s somebody in the garage at the time and they’re depending on the lights to unload groceries, then they’ll be left in the dark.

Oh. Hadn’t thought of that.

Yeah, it’s a sticking point.

Hmm. You could always use an occupancy sensor, you know. That would work, wouldn’t it?

I dunno’. Besides, I don’t have one.

Is that your wish, an occupancy sensor?

Only if you can guarantee that it will work.

No, sorry, no guarantees.

You know what might work?

No, what’s that?

A state machine. I could really use a state machine.

OK! Wish granted!

Now, what’s a state machine?

A state machine is simply a logical construct that keeps track of the state… of… a machine. It’s really simple, actually.

A dishwasher, for example, is a state machine. It has states: clean, dirty, and unloaded. And it transitions from one state to another, caused by triggers or conditions. If the machine is dirty and you press the start button, the next state should be clean. When you unload it, the state is unloaded. Throw dishes in it, and it’s now dirty again. There’s a really good Wikipedia article on state machines, actually, which gives a lot more detail. But suffice it to say, if you’ve put up with the narrative thus far, you probably know what a state machine is already.

Right. How do you implement one in openHAB?

You first have to do what any good designer will do, which is to design the state machine using a state diagram, a graphical representation of a state machine. Here is my state diagram which shows the states of my garage lights.

I see that each circle represents a state, and that there are arrows between them which show what causes the “machine” to move between states, the transitions. But what’s up with those extra two states?

In this case, I’ve given a lot of thought to how I’ll use this state machine. When the lights are off or on, I should be able to turn them on or off with no involvement from the garage door. Those are the two left states, and you can see that touching “paddle on” (the top of the switch paddle) turns the light “on,” and touching the “paddle off” (the bottom) turns it “off” again.

When the door gets involved, though, things get a bit more complex. A “door up” event moves from “off” to “on (5 min),” a timed event when the lights are on that should last for five minutes. If nobody does anything else, that timer expires and (here’s the clever bit) the “machine” moves to a state where it blinks the light once and then waits for another minute. This gives the occupant time to realize that the lights are about to go out. If the occupant doesn’t care, they go out after a minute. But if the occupant wants to keep them on, she taps the “on” paddle and they’ll stay on until they’re otherwise turned off.

By the way, this was the first draft. There are some other things missing that I’ve included in the code, such as what happens when the other garage door goes up and the machine is in the “blink” state.

What happens then?

It goes back to the “timed” state.

That makes sense. So… how do you code this?

openHAB is an event-driven system, so we have to set up a machine and then change the state of that machine with events, collecting into one chunk of code, or event handler, all of the transitions for that event.

Sounds hard.

It isn’t, once you realize how it’s done.

First, we’ll need some items in our items file. These first items are associated with this specific example and are for a HomeSeer HS-WS100+, which is node 15 on my network (and will almost certainly not be node 15 on your network):

Switch	GarageLightsProxy	"Garage Lights Proxy"
Switch  GarageLights		"Garage Lights"			{ zwave="15:command=SWITCH_BINARY,refresh_interval=60" }
Number  GarageLights_1TU	"Garage Lights_Scene 1TU"   	{ zwave="15:command=CENTRAL_SCENE,scene=1,key=0" }
Number  GarageLights_2TU	"Garage Lights_Scene 2TU"   	{ zwave="15:command=CENTRAL_SCENE,scene=1,key=3" }
Number  GarageLights_3TU	"Garage Lights_Scene 3TU"   	{ zwave="15:command=CENTRAL_SCENE,scene=1,key=4" }
Number  GarageLights_1TD	"Garage Lights_Scene 1TD"   	{ zwave="15:command=CENTRAL_SCENE,scene=2,key=0" }
Number  GarageLights_2TD	"Garage Lights_Scene 2TD"   	{ zwave="15:command=CENTRAL_SCENE,scene=2,key=3" }
Number  GarageLights_3TD	"Garage Lights_Scene 3TD"   	{ zwave="15:command=CENTRAL_SCENE,scene=2,key=4" }

…and this item is specific to the state machine:

Number  GLSM "Garage Lights State Machine State [%d]"

It’ll be used to store the state of the machine.

In our rules file, we need some variables:

var Timer GLSMtimer 
var Lock GLSMlock = new ReentrantLock()
var Number GLSM_OFF = 0
var Number GLSM_ON = 1
var Number GLSM_TIMED_ON = 2
var Number GLSM_TIMED_BLINK = 3

What’s with all the UPPER CASE variables? Did you have your “WIN AT THE INTERNET” key on?

No, those are constants, and I’ll use them to denote the current state of the machine. You see, we’ll keep track of the machine state by using a Number, as the GLSM item definition above shows, and it’s a lot easier to read and write code that uses constants like this instead of just numbers. Some people use ALL UPPER CASE, like I do, and others don’t. It’s a personal preference, nothing special about it.

Oh, whew. Thought we were going to have a flame war.

Nope. We’re good.

Now, on to the state machine itself. Here’s the code that implements the state machine:

/**
 *  GLSM updates events get caught by this event.
 *  This is the GLSM.
 **/
rule "GLSM - Garage Lights State Machine Events Handler"
	when
		Item GLSM received update
	then
		logInfo("GLSM","GLSM received an update. GLSM state is "+GLSM.state)
		GLSMlock.lock()
		if (GLSM.state==Uninitialized) {
			logInfo("GLSM","...state is Unitialized. Turning off GarageLights, initializing state machine to GLSM_OFF.")
			sendCommand(GarageLights, OFF)
			postUpdate(GLSM, GLSM_OFF)
		} else if (GLSM.state==GLSM_OFF) {
			logInfo("GLSM","...state is GLSM_OFF. Turning off GarageLights and cancelling a timer (if it existed).")
			if (GLSMtimer != null) {
                		GLSMtimer.cancel()
                		GLSMtimer=null            
			}
			sendCommand(GarageLights, OFF)
		} else if (GLSM.state==GLSM_ON) {
			logInfo("GLSM","...state is GLSM_ON. Turning on GarageLights and cancelling a timer (if it existed).")
			if (GLSMtimer != null) {
                		GLSMtimer.cancel()
                		GLSMtimer=null            
			}
			sendCommand(GarageLights, ON)
		} else if (GLSM.state==GLSM_TIMED_ON) {
			logInfo("GLSM","...state is GLSM_TIMED_ON. Turning on GarageLights, setting a timer to go to GLSM_TIMED_BLINK.")
			sendCommand(GarageLights, ON)
			if (GLSMtimer != null) {
                		GLSMtimer.cancel()
                		GLSMtimer=null
            		}            
            		GLSMtimer = createTimer(now.plusSeconds(3*60)) [|
				logInfo("GLSM","GLSM TIMED_ON timer expired. Updating to GLSM_TIMED_BLINK.")
				GLSM.postUpdate(GLSM_TIMED_BLINK)
			]
		} else if (GLSM.state==GLSM_TIMED_BLINK) {
			logInfo("GLSM","...state is GLSM_TIMED_BLINK. Blinking GarageLights, setting a timer to go to GLSM_OFF.")
			sendCommand(GarageLights, OFF)
			sendCommand(GarageLights, ON)
			if (GLSMtimer != null) {
                		GLSMtimer.cancel()
                		GLSMtimer=null
            		}            
            		GLSMtimer = createTimer(now.plusSeconds(60)) [|
				logInfo("GLSM","GLSM TIMED_BLINK timer expired. Updating to GLSM_OFF.")
				GLSM.postUpdate(GLSM_OFF)
			]
		}
		GLSMlock.unlock()
end

First, note that this code gets executed every time we send it an update. Our event handlers are responsible for being triggered by events and for updating the state machine by invoking the postUpdate method.

Second, note that the state machine is just a big if/then/else tree. You could use a switch here just as easily. When the conditional matches the new state, the machine performs some action, in our case turning on and off the light, and setting up and cancelling timers. Because we separate the actions (in the state machine) from the transitions (in the event handlers), you run a much lower risk of causing unintended side effects.

Thirdly, note that an item whose state is unknown, because it hasn’t been set yet, is denoted in XTend as Uninitialized without quotes. It’s not null, either. It’s kinda’ weird.

Fourthly, afterSunset is a global variable (ick! bad programmer!) I’ve used to denote whether or not it’s after sunset. You’ll have to set that yourself, and you probably should use an item instead.

Finally, note that this code is not concurrent. That means that even if there are multiple updates to the state machine fired off in rapid succession, openHAB will execute this code once per update, and because of the lock() method call at the top, it’ll wait until the first update is done and the unlock() method is called at the bottom before letting the next execution occur. That’s important because you might end up with a race condition resulting in a machine whose state doesn’t match the real world.

Any questions?

Yeah, what’s "XTend?"

That’s the scripting language we’re working in. Google it for more documentation. Really, you should know that, being a genie and all.

Anywho…

Lastly, we need to handle all the events which cause the transitions from state to state. Here are all of the event handlers. We’ll look at one in more detail in a moment:

/**
 *  Commands to turn on the garage lights get caught by this event to update the state machine appropriately
 **/
rule "Garage Lights Commanded on"
	when
		Item GarageLightsProxy changed to ON
	then
		logInfo("GLSM","Garage Lights commanded on. Updating to GLSM_ON")
		GLSM.postUpdate(GLSM_ON)
end

/**
 *  Commands to turn off the garage lights get caught by this event to update the state machine appropriately
 **/
rule "Garage Lights Commanded off"
	when
		Item GarageLightsProxy changed to OFF
	then
		logInfo("GLSM","Garage Lights commanded off. Updating to GLSM_OFF")
		GLSM.postUpdate(GLSM_OFF)
end


/**
 *  All "paddle up" events get caught by this event
 **/
rule "Garage Lights Switch 1 Tap Up"
	when
		Item GarageLights_1TU received update
	then
		logInfo("GLSM","GLSM received 1TU: GLSM state is "+GLSM.state)
		if (GLSM.state==Uninitialized) {
			logInfo("GLSM","...was Uninitialized, updating to GLSM_ON.")
			GLSM.postUpdate(GLSM_ON)
		} else if (GLSM.state==GLSM_OFF) {
			logInfo("GLSM","...was GLSM_OFF, updating to GLSM_ON.")
			GLSM.postUpdate(GLSM_ON)
		} else if (GLSM.state==GLSM_ON) {
			logInfo("GLSM","...was GLSM_ON, no action.")
			/* do nothing */
		} else if ((GLSM.state==GLSM_TIMED_ON)||(GLSM.state==GLSM_TIMED_BLINK)) {
			logInfo("GLSM","...was in a timed state, updating to GLSM_ON.")
			GLSM.postUpdate(GLSM_ON)
		}
end

/**
 *  All "paddle down" events get caught by this event
 **/
rule "Garage Lights Switch 1 Tap Down"
	when
		Item GarageLights_1TD received update
	then
		logInfo("GLSM","GLSM received 1TD: GLSM state is "+GLSM.state)
		if (GLSM.state==Uninitialized) {
			logInfo("GLSM","...was Uninitialized, updating to GLSM_OFF.")
			GLSM.postUpdate(GLSM_OFF)
		} else if (GLSM.state==GLSM_OFF) {
			logInfo("GLSM","...was GLSM_OFF, no action.")
			/* do nothing */
		} else if (GLSM.state==GLSM_ON) {
			logInfo("GLSM","...was GLSM_ON, updating to GLSM_OFF.")
			GLSM.postUpdate(GLSM_OFF)
		} else if ((GLSM.state==GLSM_TIMED_ON)||(GLSM.state==GLSM_TIMED_BLINK)) {
			logInfo("GLSM","...was in a timed state, updating to GLSM_OFF.")
			GLSM.postUpdate(GLSM_OFF)
		}
end

/**
 *  "Garage door up" events get caught by this event
 **/
rule "GLSM - Garage Door Up Event Handler"
	when
		Item LeftGarageDoor changed to OPEN or
		Item RightGarageDoor changed to OPEN
	then
		logInfo("GLSM","GLSM Door Up Handler received a door OPEN change. GLSM state is "+GLSM.state)
		if (!afterSunset && ((GLSM.state==Uninitialized)||(GLSM.state==GLSM_OFF)||
			(GLSM.state==GLSM_TIMED_ON)||(GLSM.state==GLSM_TIMED_BLINK))) {
			logInfo("GLSM","...updating to GLSM_TIMED_ON.")
			GLSM.postUpdate(GLSM_TIMED_ON)
		} else if (GLSM.state==GLSM_ON) {
			/* do nothing */
		}
end

The first thing you should notice is that we’ve defined two cases for the GarageLightsProxy item (right at the top). It allows the state machine to be updated by manual controls. Manual controls can’t interact directly with the items controlled by the state machine for one obvious reason: the state machine needs to keep track of the state of the things it’s controlling. Another not-so-obvious reason is because updating from the sitemap/manual control has the same effect as using the sendCommand method, and that causes undesired changes in state, too. (There may be a better way to do this, and I’m open to suggestions.)

Back to looking at an event handler. There aren’t a whole lot of complex event transitions to make here, but there are some that occur in several places. For example, there are three “paddle up” transition arrows shown (and a fourth which is not shown). So every time we get the “paddle up” event, which corresponds to an update of the GarageLights_1TU item, we have to do one of three (four) things. Here’s that event handler:

/**
 *  All "paddle up" events get caught by this event
 **/
rule "Garage Lights Switch 1 Tap Up"
	when
		Item GarageLights_1TU received update
	then
		logInfo("GLSM","GLSM received 1TU: GLSM state is "+GLSM.state)
		if (GLSM.state==Uninitialized) {
			logInfo("GLSM","...was Uninitialized, updating to GLSM_ON.")
			GLSM.postUpdate(GLSM_ON)
		} else if (GLSM.state==GLSM_OFF) {
			logInfo("GLSM","...was GLSM_OFF, updating to GLSM_ON.")
			GLSM.postUpdate(GLSM_ON)
		} else if (GLSM.state==GLSM_ON) {
			logInfo("GLSM","...was GLSM_ON, no action.")
			/* do nothing */
		} else if ((GLSM.state==GLSM_TIMED_ON)||(GLSM.state==GLSM_TIMED_BLINK)) {
			logInfo("GLSM","...was in a timed state, updating to GLSM_ON.")
			GLSM.postUpdate(GLSM_ON)
		}
end

You’ll notice that each line of the if/then/else clause examines the current state of GLSM.state and, if it matches, it posts an update to the state machine telling it what state it should go to as a result of the “paddle up” event. The “if” handles an uninitialized state machine, and the next one turns on the light by moving/updating the state machine to the GLSM_ON state. The next one does nothing since the state machine is already in the GLSM_ON state, and the fourth is a good example of showing how to look for two states at once, since both of the timed states result in the same new state, GLSM_ON. (Admittedly, this clause could have been combined with two others and made for shorter code.)

Note again that the events don’t actually do anything. They just tell the state machine what state to be in, and the state machine takes care of all the doing.

Clever. I notice you like logging.

Debugging in openHAB is a pain in the…

Careful…

…rear, and logging is your friend.

That’s it, really. Set up a thing to hold the state, write the state machine which does the actions for each state, and then implement the events which make the transitions from state to state. And if you design your state machine carefully, your code will do exactly what you want it to do (typos aside!) the first time.

Great! So, you got what you wanted, then?

I sure did! And I learned one heckuva lot about openHAB scripting, though I wished it had been better documented.

Ah-ha! There’s your third wish, for better openHAB documentation! Done!

Hey, wait!

Too late.

But I don’t see anything different!

Have you even read what you wrote?

That’s cheating!

Sorry. Rules are rules. Gotta’ go! See you again in the future!

Thanks!

(I hope you found this tutorial useful and perhaps even fun to read.)

(edited after posting to clean up the ugly, ugly tabs, or lack thereof, throughout, a nasty byproduct of editing first in Emacs and then in TextWrangler)

20 Likes

Excellent tutorial. Very entertaining and informative.

Just a couple of comments:

  • The indentation in the code is a little off making it hard to follow.

  • You might have a missing } in your second to last state.

                        if (GLSMtimer != null) {
                GLSMtimer.cancel()
                GLSMtimer=null

Particularly with this kind of code, getting the indentation right really helps one follow the code and see these sorts of errors, if there actually is an error.

This is true for OH 1.x. For OH 2.0 the “Uninitialized” state is NULL (not to be confused with null). Also, this is an OH thing, not an XTend thing.

Perhaps using this. :slight_smile:

Have you considered separating the bodies of each state in the if else machine and moving them to their own rules? In my mind as I’ve been trying to tackle coming up with a good generic example for state machines I have been toying with using the machine part to trigger events that kick off Rules rather than ending up with a several page long rule. Obviously there are positives and minuses. Thoughts?

It’s a challenge and there is a recent lengthy discussion about it on another thread. At the end of the day it is difficult to detect the origin of an event (i.e. rules, binding, sitemap). So probably your best bet is to use a Proxy Item from the sitemap that only the sitemap commands and rules to forward those commands as necessary, perhaps setting a flag or something if necessary to indicate the origin. I do something like this in my Dead Man’s Switch design pattern but boy is it ugly.

Excellent work! Thank you for sharing.

1 Like

Really useful and well written!

1 Like

Hi, Rich,

Thanks for the encouragement and the comments. I fixed the indentation (as noted, a big ugly resulting from starting in Emacs and moving to TextWrangler, and I’ve not bothered to completely entab my code). I don’t know if I just fixed the curly brace or not. I think it looks right now… but I’ve stared at it for a long time now. (It looks great in TextWrangler, but those tab stops are different than this editor.) If it still looks wrong, please let me know.

Thanks for the clues about Uninitialized and null and NULL. I’ll eventually move to OH2 whereupon I will have forgotten this distinction and it will cause a great wailing and gnashing of teeth. And thanks for pointing out the global sunrise/sunset event design pattern. Cool!

Well, no, I never really thought outside of “Let’s make this work!” I think what I like about the I/T/E string, or better still, a SWITCH statement, is that it is, at its core, simple and easy to see what it’s doing. On the other hand, it’s a long, not-quite-beautiful rule. (I wouldn’t call it ugly, but “gangly” comes to mind.)

Thanks again,
Bill

A quick scan looks right to me. Its amazing how much less complicated it looks when properly indented. :slight_smile:

I always get anxious when I have a rule that doesn’t fit on a single screen. :wink:

Great post! Keep them coming. :smiley:

This is a really good post - thank you! This will help me write state machines for OH.

Just an FYI: there is a GUI that amg0 wrote on top of the Vera interface that allows you to create the state diagram in the browser and add links and actions in it directly. Not only that, it is animated, so you can see the current state and see the value of the active timer if any. It would be really cool to get something like this for OH2 someday. Or does something like this already exist?

For those using Scripted Automation, there is a Python state machine library posted to Finished State Machine in Jython that I hope will make it to the Helper Libraries.