ISY Programming Methodology (state-machine approach) edited from posts by ergodic in June 2010 The ISY uses what technically would be termed "functional" (i.e.; stateless, variable-less), "event" programming and that is not the way people think at all. So you need some methodology that bridges that gap. Ad-hoc programming in the ISY just doesn't work for me. If you can identify what you want to do and diagram it, then have some rote mechanical method to develop the ISY programs from that, it is much, much easier to get something that works, that you are sure should work at least, and also can be expanded or changed later without introducing errors that are difficult to find and correct. The end programs may not be pretty to look at, but what ISY programs are? I'll describe a method that is easy to understand, quick, and once you've outlined your logic on paper, it is straightforward to produce a set of ISY programs that implement it reliably. I'll start with a simple example: Press a keypad button ("KPL D") three times in succession to turn on a light. If more than 5 seconds elapses between presses, then the sequence resets and you have to start over.I use something like this for my garage door open programming: it prevents the door from opening if you happen to hit the KPL button by accident. You can also have a little fun with this idea, for example, turning the aux buttons of a KPL into a combination lock. I'll also note that to actually do this, you may know that Insteon requires we create a scene ("Scene KPL-D") with the button in it, just to be able to toggle the keypad light on and off from the ISY. To start, draw big, labeled circles to represent all the different "states" of your process. These are generally the places where you are "doing" or "waiting" for something. For example, "the garage door is open and we're waiting for the close button." Each situation is different but generally the states will fall out pretty quickly once you've grasped the idea and you start to diagram the flow of your particular logic. States generally map to a particular point of logic: (e.g.; "My garage door was opened by a motion sensor and we're waiting for the off from the motion sensor.") And you'll always have a starting "quiet" state that does nothing. In this example the states are pretty easy to see, there are four: a start/reset state, a state for the button having been pressed once, pressed twice, and pressed three times. We'll use labels S0, S1, S2 and S3. Then you draw arrows between the states. Label the arrows with the triggers that cause that particular state change. Anyone with an information science background will immediately recognize this as a basic "state machine". No matter, just diagram how your logic flows from one state to the next. Don't worry about the ISY just yet. And don't worry if your logic isn't fully developed, you can always go back later and expand on it. For this little example, we draw an arrow out from the edge of the S0 circle to the edge of the S1 circle, indicating a press of KPL-D when you're in state S0 takes you to state S1 (first button press). Likewise S1 to S2, and S2 to S3. These are of course all basically the same transitions, but the only way a state-based system has to "remember" anything is by the state it is in. We don't need no stinkin' variables or, more precisely, the states themselves are our variables. This is partly why this model works so well for the variable-less ISY. The important thing to understand about state diagrams is that you have no idea how you got to the state you're in. If you need to "know" that for your logic, you create more states. Inside or near each state's circle you note what that state does when it is activated. For example, S1 turns off the KPL light to indicate it is ready for another press, waits 5 seconds and then, assuming nothing else has interrupted it, it returns to state S0. S1, S2 and S3 all will have arrows back to S0 representing this "timeout" transition (i.e.; you waited too long to press a key and the state reverts to S0). I like to draw these explicit transitions from inside the circle. I'll also point out that in a less trivial example than this, for example, one involving motion sensors, you often have arrows going from a state back to itself. For example, if a motion sensor sends a repeated "on" signal while you are waiting in a state, you want to restart the timeout countdown in that state by re-entering the state. Now, if you've suffered with this explanation this far, I recommend you try implementing this in your ISY to get a feel for the process. The basic idea is to separate figuring out what you want from the actual ISY programming. Now that we have our diagram, we can easily implement our logic by set of ISY program from a rote procedure. Each state will be represented by two ISY programs. One program will test the conditions, which correspond to all the incoming "arrows" to that state that represent the various trigger conditions into it. A second program performs whatever that state is supposed to do. Here were are at the eternal sticking point: Why two programs for one state? All we need is one set of conditions and one "Then" body to execute it. So ... one program, right? Wrong. The ISY executes either the Then or Else body of a program whenever any of its trigger conditions are TESTED. (This means the Then or Else body of a program will be interrupted and restarted if that program is currently executing a Wait or Repeat command.) This causes the program to be reset and could be (often is) really a Bad Thing; we want the code for our state to run only when one of the trigger conditions (incoming state changes) is True, not each and every time they are simply checked by the ISY. If you don't understand why, keep reading and thinking about it until it makes sense. It is critical to understanding the difference between the way you might think and the way the ISY thinks. In this example, we end up with eight programs for our four states. We'll call them here S0Cond, S0Body, S1Cond, S1Body, and so forth. The structures of the four S#Cond programs are basically the same; just tests for conditions of the incoming triggered transitions that activate into that state. If any of the conditions are True, the "Then" part calls the corresponding S#Body for that state. You must run a separate S#Body program. Do not put your state code in the "Then" part of the S#Cond program. It might work after a fashion but it will not consistently work the way you want. So here's the basic skeleton of our S1Cond program:
If
Program 'S0Body' is True
And Control 'KPL D' is switched On
Then
Run Program 'S1Body' (Then Path)
The condition program connects each transition coming into this state with a pair of "AND" ed tests. One test identifies the state you're coming from and the other test identifies the event causing the transition from that state to this one. This is simpler to do than to explain. In this example there is just one transition into S1 from state S0. If there are multiple transitions coming into the state, you merely "OR" a set of these parenthesized tests together to trigger any of the possible incoming transitions that can get you to this state. The ISY nicely takes care of checking these conditions automatically any time a transition might happen. What is put in an S#Body program? The first thing are statements that set all of the other state's body programs False. This cancels whatever those programs might be doing at the time a transition is made to this state, which effects the actual transition to this state. Remember, we can only be in one state at a time. That also declares the other state programs as being False to the ISY. This is crucial because we are using the True/False status of the S#Body programs to define what state we are in. After setting all other state body programs to False, an S#Body program does whatever you want to be done in the state (if anything). Finally, an S#Body program performs any "tail" transition that happens if the state times out or exits before any other condition triggers the ISY to cancel it. These "tail" transitions are performed by calling the body program for the target state. This is usually the last thing that an S#Body program does. They are not included in the trigger condition list for the target state's S#Cond tests. All other incoming (triggered) transitions are included in those tests but these are programmed transitions, not events. That's why I draw those arrows starting from inside the circle, as they are implemented differently, Here's our S1Body for example:
If
- No Conditions
Then
Run Program 'S0Body' (Else Path)
Run Program 'S2Body' (Else Path)
Run Program 'S3Body' (Else Path)
Set Scene 'Scene: KPL-D' Off
Wait 5 seconds
Run Program 'S0Body' (Then Path)
Again, generating this program is more or less a rote process. In fact I typically first just create one template "cond" and one template "body" and use the ISY to copy and edit since they're all so similar. You also want to use a careful (and terse) naming system to keep things clear. I recommend using a common prefix so that you can more easily watch them grouped together in the ISY Program Summary. The only things to think about for the S#Body programs are how long to wait before making an explicit timeout transition out of the state, and what to do around that waiting. For the most part, you have the freedom to do whatever you please without worrying about strange interactions because you know the program isn't going to be interrupted - its execution is 'protected' by its companion S#Cond program. You may have no timeout inside a state. For example, here's the S3Body:
If
- No Conditions
Then
Run Program 'S0Body' (Else Path)
Run Program 'S1Body' (Else Path)
Run Program 'S2Body' (Else Path)
Set 'Our Test Light' On
Set Scene 'Scene: KPL-D' Off
Run Program 'S0Body' (Then Path)
This state just notes that it is in state S3 then resets the KPL button off, turns on the target light ("success!") and transitions back to the resting state S0. (The S0Body does nothing except turn off the other three states and exit itself, staying True.) Finally, we need something to initialize the state of this logic to S0 when the ISY boots up. We require exactly one of the S#Body programs to be "True" at any time, which indicates the state we are in. We do this by simply marking the S0Body program as the only program in this group of programs to "run at startup". If you want extra insurance, you can create a separate program that is triggered by all four S#Body programs being False and, when True, runs "S0Body (Then)". This usually is not necessary but it does take care of unusual conditions such as someone manually setting S0Body False from the ISY console. Here's another example: you want to turn your Denon amplifier off at 11:45 and turn it back on at 7AM but only if it was on at 11:45 in the first place. This application has two states: the beginning/idle state that we'll call Denon.S0 and a state Denon.S1 that represents the amp being on at 11:45, waits for 7AM to turn it off, and returns to state Denon.S0. Two states require four programs ... Oops, not quite. We have no easy way in the ISY to wait INSIDE a program for a specific time of day. There are ways you could cook up to shim around this problem such as a little program to trigger True at 7AM and make this test in a repeat loop. But all that nonsense is precisely what we're trying to get away from. The solution, as always here, is simply to add another state. With this change, the state Denon.S1 merely serves to record that the amplifier was on at 11:45 and exits doing nothing except staying True. A new third state, Denon.S2, triggers from Denon.S1 at 7AM, turns off the amplifier and returns to state Denon.S0. Now we have three states, which requires six programs. Denon.S0.Cond:
If
- No Conditions
Then
Run Program 'Denon.S0.Body' (Then Path)
Denon.S0.Body: (set to run-at-startup)
If
- No Conditions
Then
Run Program Denon.S1.Body (Else Path)
Run Program Denon.S2.Body (Else Path)
Denon S1.Cond:
If
Program Denon.S0.Body is True
And Time is 11:45PM and Status 'AMP' is On
Then
Run Program 'Denon.S1.Body'
Denon.S1.Body:
If
- No Conditions
Then
Run Program Denon.S0.Body (Else Path)
Set 'AMP' Off
Run Program Denon.S2.Body (Else Path)
Denon S2.Cond:
If
Program Denon.S1.Body is True
And Time is 7AM
Then
Run Program 'Denon.S2.Body'
Denon.S2.Body:
If
- No Conditions
Then
Run Program Denon.S0.Body (Else Path)
Run Program Denon.S1.Body (Else Path)
Set 'AMP' On
Run Program Denon.S0.Body (Then Path)
OK, let's take the gloves off. I hear you: "But I can do this little thing in 3 programs (or 2 or 1 or one-half a condition test with my eyes closed, or whatever). Why 6? You've got programs doing basically nothing - we can get rid of them." Yeah, yeah, yeah. If you want to chase the infamous "Busy Beaver" problem, far be it from me to dissuade you. Enjoy yourself. But if you've followed me this far, I'd guess you already know from experience that with the ISY, madness lies down that road. Maybe here you can indeed toss out "Denon.S0.Cond" but maybe later you'll really wish it were there when you make some enhancement, Like the one I make below. So my advice: stick with the structure. The main point here is that, using this technique, everything is simple and more or less as understandable - at least as understandable as anything can be with ISY programs. If my logic diagram does what I want, I can more or less be assured that the programs I generate from it this method will implement it correctly. If I have to change the logic or add more states, I know exactly what to do and where to look to do it. I don't have a Denon amp to test this with but I am still reasonably confident that - absent any typos - the above set of 6 programs will do the job exactly as described without endless fiddling and forum posts. Here's an illustration of how this technique can save some heartburn. Suppose we want to enhance this application a little. If someone turns on the amp between 11:45 and 7AM, then we want to reset the logic and not turn the amp back on at 7AM. This is done easily. Just add a transition arrow from state Denon.S1 to Denon.S0, triggering on the condition "if AMP is switched on". Put that condition into the Denon.S0.Cond program and you're done. Let's apply the technique to an application described in a post on this forum: Between 10PM and sunrise ¦ if we get either of two off signals but with either of two "test" lights still on we turn off a few lights. If neither of those two lights are on then we turn off a lot of other lights. From resting state S0, we have a transition to a state S1 between 10PM and sunrise if either of the two test lights are also still on. We have a second state S2 activated from S0 between 10PM and sunrise if neither of the two test lights is on. There are a couple of other ways to structure this state diagram but, however you graph it, you should be able to produce the 6 programs to implement it easily. Again, if you're tempted to "optimize" them down to 4 or 2 programs, my unequivocal advice is to resist that temptation. Now this technique is hardly perfection. You can still have race conditions in your code, unexpected device interactions, Insteon signal collisions or missed signals. And the ISY has no variables or macro substitution so you can easily get copy/paste errors if you change something in one place and don't mirror it somewhere else where it needs to be. All the usual stuff. But it is much, much easier to debug and solve these problems inside this kind of structure. It's kind of cool to watch the True indicator jump from program to program (state to state) in the Program Summary screen as the programs run. I always know exactly what the programs should be doing and why. A poor man's debugger. If I see two True body programs, I know I probably added a state and forgot to clear it in one of the other states. Here is a program set I have for my kitchen. There are two motion sensors. At night, turn up the lights over the kitchen island to 50% if they're off or less than that when somebody walks in. Regardless whether it is day or night, after 15 minutes of no activity shutoff all the kitchen lights using a scene. There are four states: S0: Resting state Go to S1 when motion is detected and it is night Go to S2 when motion is detected and it is not night S1: Nighttime state Turn up the island light Go to S2 S2: Waiting-for-both-MS off state Go to S3 when status of both MS goes to 'off' (failsafe) after 4 hours, go to S3 (haven't really thought this through) S3: Waiting-for-either-MS-on state Go to S2 when on is received from either MS Otherwise after 15 minutes turn off the lights and go to S0
Here's a state diagram:
(motion
and night)
S1 <--------- S0 <---+
| (motion / |
| and not / |
| night) / |
v / |
+-------} S2 <-----+ |
| | |
| | (both MS off) |
| | |
| v |
| S3 |
| / \ |
| / \ |
+------+ +----------------+
(either (after 15 min)
MS on) The programs that implement this application are shown below. There are two oddball programs. The first is the "IsNight" program that has no code for either Then or Else. The program is True or False depending on whether it is "nighttime". I use this because the definition of "nighttime" is used in two conditions and I want to be sure if I change it later I don't need to remember to do it in more than one place. The second is the "Body.S1.Sub" program. I need something to test whether the island lights are currently below 50% and only raise them in that case. Otherwise, if the lights were already brighter, they'd get dimmed -- far more annoying than you might think if you've never actually had it happen to you. The ISY has no "if...then" testing inside program clauses, which would be most welcome. So I break out this little routine that is called from S1 to check the lights and raise them only in they are below 50%. You could do this with an added state or two but it really isn't necessary. Here are the programs Kit Motion.Cond.S0:
If
- No Conditions
Then
Run Program 'Kit Motion.Body.S0' (Then Path)
Kit Motion.Body.S0: (set to run-at-startup)
If
- No Conditions
Then
Run Program 'Kit Motion.Body.S1' (Else Path)
Run Program 'Kit Motion.Body.S2' (Else Path)
Run Program 'Kit Motion.Body.S4' (Else Path)
Kit Motion.Cond.S1:
If
Program 'Kit Motion.Body.S0' is True
And Program 'Kit Motion.IsNight' is True
And Program 'Kit Motion.IsMotion' is True
Then
Run Program 'Kit Motion.Body.S1' (Then Path)
Kit Motion.Body.S1:
If
- No Conditions
Then
Run Program 'Kit Motion.Body.S0' (Else Path)
Run Program 'Kit Motion.Body.S2' (Else Path)
Run Program 'Kit Motion.Body.S4' (Else Path)
Run Program 'Kit Motion.Body.S1.Sub' (If)
Run Program 'Kit Motion.Body.S2' (Then Path)
Kit Motion.Body.S1.Sub:
If
Program 'Kit Motion.Body.S1' is True
And Status 'Kitchen Island' < 50%
Then
Set 'Kitchen Island' 50%
Kit Motion.Cond.S2:
If
(
Program 'Kit Motion.Body.S4' is True
And Program 'Kit Motion.IsMotion' is True
)
Or
(
Program 'Kit Motion.Body.S0' is True
And Program 'Kit Motion.IsMotion' is True
And Program 'Kit Motion.IsNight' is False
)
Then
Run Program 'Kit Motion.Body.S2' (Then Path)
Kit Motion.Body.S2:
If
- No Conditions
Then
Run Program 'Kit Motion.Body.S0' (Else Path)
Run Program 'Kit Motion.Body.S1' (Else Path)
Run Program 'Kit Motion.Body.S4' (Else Path)
Wait 4 hours
Run Program 'Kit Motion.Body.S4' (Then Path)
Kit Motion.Cond.S4:
If
Program 'Kit Motion.Body.S2' is True
And Program 'Kit Motion.IsMotion' is False
Then
Run Program 'Kit Motion.Body.S4' (Then Path)
Kit Motion.Body.S4:
If
- No Conditions
Then
Run Program 'Kit Motion.Body.S0' (Else Path)
Run Program 'Kit Motion.Body.S1' (Else Path)
Run Program 'Kit Motion.Body.S2' (Else Path)
Wait 15 minutes
Set Scene 'Scene: Kit On' Off
Run Program 'Kit Motion.Body.S0' (Then Path)
Kit Motion: IsMotion
If
Status 'Kitchen: Motion (Oven)-Sensor' is On
Or Status 'Kitchen: Motion (Sink)-Sensor' is On
Then
- No Statements
Else
- No Statements
Kit Motion: IsNight
If
From Sunset - 30 minutes
To Sunrise + 30 minutes (next day)
Then
- No Statements
Else
- No Statements
Kit Motion: Reset State (just for debugging, not needed)
If
Program 'Kit Motion.Body.S4' is False
And Program 'Kit Motion.Body.S2' is False
And Program 'Kit Motion.Body.S1' is False
And Program 'Kit Motion.Body.S0' is False
Then
Run Program 'Kit Motion.Body.S0' (Then Path)
ISY programming is not simple. This is illustrated by the Kit.Motion.Body.S1.Sub substate. One issue is whether the Kit.Motion.Body.S1.Sub substate has to be called explicitly. If it is not called explicitly, the ISY may never get a chance to invoke it because S1 runs through without any wait. (It isn't disabled.) It has been suggested that if Kit Motion.Body.S1.Sub is enabled, then it should run either Then or Else (as 'Kitchen Island' is, or is not, less than 50%, respectively) as soon as Kit Motion.Body.S1 becomes True. This happens as soon as its Then is called, which in this case is from Kit Motion.Cond.S1. But Kit Motion.Cond.S2 also becomes True as soon as Kit Motion.Body.S1 is True, and then calls Kit Motion.Body.S2, which will make Kit Motion.Body.S1 False. It is possible that this could create a race condition; this could be tested simply by removing the call. In summary, this method of boils down to the following steps: 1) Draw your diagram. Begin by drawing a circle for the resting state and begin thinking about what events trigger out of that. The rest will flow from that fairly quickly although it may take a few tries to get the logic right. 2) For every state in the diagram you will have two ISY programs: a body.state and a cond.state. Exactly one body.state program can be True at a time; that tells what state you are currently in. 3) The body.state program has no conditions, just a "Then" clause. It first sets all other body.state programs False. It then does whatever you want that state to do, if anything. Sometimes a state merely distinguishes where you are in the flow and may do nothing else. There is always a starting, rest state that usually does nothing and should be set to run-at-startup. If the state requires a wait or a wait is acceptable, then you can transit directly to the next state from the last statement ("Run Program Body.Target (Then Part)" instead of using a condition on the target state. The same is true if there is only one transition out of the state. Otherwise I believe you have to give the ISY a chance to check the other transit-out conditions. You must use conditions to handle every exit path. If there's another / better way to handle this, I'd be most eager to know it. 4) The cond.state lists the condition(s) for all incoming transitions into that state. It ANDs the source state = True with whatever condition takes you from that source state to this target state. This corresponds to a transition arrow on your diagram. If there is more than one incoming transition to a state, they are simply "OR"ed together to create one test. The "Then" clause for a cond.state has a single statement executing that state's body program. You should resist the temptation to put anything else in here. 5) If you find yourself using the same tests repeatedly, consider making a separate True/False program like the .IsNight and .IsMotion programs above. EDIT 8/15/11 to correct a few typos in the first example. Thanks to Illusion for pointing out the errors.
EDIT 2/28/15 to clean up text by restoring some quotation marks and apostrophes that were converted into weird character strings.
EDIT 8/10/15 to fix state diagram.