(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)