Design Pattern: Simple State Machine (e.g. Time Of Day)

Edit: OH 4 Updates

Problem Statement

Often in home automation there is a need to track certain states that the system is in. Events occur to transition from one state to another state (e.g. at noon we transition to the AFTERNOON state). The collection of all the possible states and the transitions between events is called a “state machine”.

This design pattern illustrates some ways OH can implement very simple state machines.

One of the most common uses of a state machine like this is to track time periods throughout the day, week, or year (e.g. seasons).

In all of the examples below, there is a String Item that stores the state machine’s state.


Simple: Event Always Transitions to Fixed State

In this example, no matter what state the machine is in, when a given event occurs it always transitions to a fixed state.

This approach can be implemented with the simplest of rules, even UI rules. All it requires is writing a rule that triggers based on the event and an action that commands the State Item to the state associated with that event. For example, a time based simple state machine might have the these three rules:

Event Command
Astro dawn DAY
12:00 pm AFTERNOON
Astro dusk EVENING
11:00 pm NIGHT

Note that OH 4.0 introduced a “Time is Item” trigger that will trigger the rule at the time held in a DateTime Item.

Another simple state machine can be to track presence.

Event Command
One or more phones respond to an arping ON
No phones respond to arping OFF

If using a script action you can create just one rule and send the command based on the event that triggered the rule.


Complex: Events and/or Transitions Depend on Conditions

In this case, which events are considered to be transition events differs based on some condition. For example, you might have a different set of time of day states on weekends and holidays as compared to weekdays.

For a time of day example that takes into account ephemeris see Time Based State Machine [4.0.0.0;4.9.9.9]. This implements a time of day state machine using sets of DateTime Items and shows one way to implement something complex using timers for a time based state machine.

When using the UI one can use Conditions to determine whether or not to transition to the new state. For example:

configuration: {}
triggers:
  - id: "1"
    configuration:
      time: 06:00
    type: timer.TimeOfDayTrigger
conditions:
  - inputs: {}
    id: "3"
    configuration:
      offset: 0
    type: ephemeris.WeekdayCondition
actions:
  - inputs: {}
    id: "2"
    configuration:
      itemName: TimeOfDay
      command: MORNING
    type: core.ItemCommandAction

This is the YAML of a simple UI rule that triggers at 6:00 AM and commands TimeOfDay to “MORNING” but only if it’s a Weekday according to Ephemeris.

Related Design Patterns

Design Pattern How It’s Used
Design Pattern: Unbound Item (aka Virtual Item) vTimeOfDay, Irrigation, and other Items are examples of a Virtual Item
Design Pattern: Separation of Behaviors This DP is a specific implementation of Separation of Behaviors
A State Machine Primer with HABlladin, the openHAB Genie - #6 by jswim788 This DP is a simplified implementation of a state machine
Design Pattern: Using Item Metadata as an Alternative to Several DPs The Python versions use metadata to define the times of day.

Edit: A near complete rewrite to match formatting of other DP postings and reflect changes in OH 2.3. Eliminated examples for prior versions of OH.
Edit: Use toString instead of going through the long set of calls to get a millis to convert DateTimeType to DateTime
Edit. Remove call to millis in the switch statement.
Edit: Resorted the headings and added a reference to the openhab-rules-tools implementation.

61 Likes

I tried this exact script (was using your old one, which worked like a treat), and it only switches from Day to Twilight and back.

2016-10-21 16:30:00.195 [INFO ] [openhab.model.script.ToD.rule1] - Setting TimeOfDay to "Twilight"
2016-10-21 16:34:44.832 [INFO ] [hab.model.script.Weather1.rule] - The Weather_Cloudy condition has changed to: false (34)
2016-10-22 05:09:00.146 [INFO ] [openhab.model.script.ToD.rule1] - Setting TimeOfDay to "Day"
2016-10-22 05:09:00.163 [INFO ] [openhab.model.script.ToD.rule1] - Setting TimeOfDay to "Day"
2016-10-22 06:34:44.866 [INFO ] [hab.model.script.Weather1.rule] - The Weather_Cloudy condition has changed to: false (32)
2016-10-22 07:34:44.815 [INFO ] [hab.model.script.Weather1.rule] - The Weather_Cloudy condition has changed to: true (28)
2016-10-22 12:34:45.110 [INFO ] [hab.model.script.Weather1.rule] - The Weather_Cloudy condition has changed to: true (26)
2016-10-22 16:31:00.189 [INFO ] [openhab.model.script.ToD.rule1] - Setting TimeOfDay to "Twilight"
2016-10-22 16:31:00.213 [INFO ] [openhab.model.script.ToD.rule1] - Setting TimeOfDay to "Twilight"
2016-10-22 18:34:45.073 [INFO ] [hab.model.script.Weather1.rule] - The Weather_Cloudy condition has changed to: true (11)

Also, the log entry appears twice, and I have no idea why.

    if(TimeOfDay.state.toString != currPeriod) {
      logInfo("ToD.rule1", "Setting TimeOfDay to \"" + currPeriod + "\"")
      TimeOfDay.sendCommand(currPeriod)
    }

Any hints appreciated.

1 Like

Hmmmm. I think I can explain the two events.

When Astro triggers an event it toggles the switch ON and then immediately OFF. Try changing the Astro event triggers to received update ON or (I’ll update the code above accordingly.

I also see a typo in the code above. There should be no “Twilight” period.

This is what happens when I try to distill my more complicated code into simpler examples. I’ll also correct it above.

Unfortunately this doesn’t explain what you are seeing. My working rule which has an extra time period and an extra Item I use to easily keep track of the previous time of day. I’ve pasted it below. It has been working for me reliably.

rule "Get time period for right now"
when
        System started or
        Time cron "0 0 6 * * ? *" or             // Morning start
        Item Sunrise_Event received update ON or // Day start
        Item Twilight_Event received update ON or // Twilight start
        Item Sunset_Event received update ON or  //  Evening start
        Time cron "0 0 23 * * ? *"               // Night start
then
    Thread::sleep(50) // lets make sure we are just a little past the time transition

    val morning = now.withTimeAtStartOfDay.plusHours(6).millis
    val sunrise = new DateTime((Sunrise_Time.state as DateTimeType).calendar.timeInMillis)
    val twilight = new DateTime((Twilight_Time.state as DateTimeType).calendar.timeInMillis)
    val evening = new DateTime((Sunset_Time.state as DateTimeType).calendar.timeInMillis)
    val night = now.withTimeAtStartOfDay.plusHours(23).millis

    var currPeriod = "Night"
    if(now.isAfter(morning) && now.isBefore(sunrise)) currPeriod = "Morning"
    else if(now.isAfter(sunrise) && now.isBefore(twilight)) currPeriod = "Day"
    else if(now.isAfter(twilight) && now.isBefore(evening)) currPeriod = "Twilight"
    else if(now.isAfter(evening) && now.isBefore(night)) currPeriod = "Evening"

    if(TimeOfDay.state.toString != currPeriod) {
        logInfo(logNameWeather, "Updating Time of Day {}, Previous Time of Day {}", TimeOfDay.state.toString, currPeriod)
        PreviousTimeOfDay.sendCommand(TimeOfDay.state.toString)
        TimeOfDay.sendCommand(currPeriod)
    }

end
4 Likes

Hey @rlkoshak, I really like this solution. I just feel it does however not fall into the category of Design Patterns. Would you agree? I would call it a snippet, similar to the two solutions already posted here. In comparison your solution is quite neat! :wink:

I put it as a Design Pattern because it can be augmented beyond just Time of Day tracking. It could also be used for Day tracking, it can be used to develop a rule cascade (e.g. something that can be used for controlling one’s irrigation zones), and I’m currently working through using this approach to develop a generic state machine (one that isn’t as much work or more than just custom coding for one’s particular problem).

Perhaps if I illustrate a different non-time based example or added more text to show its generic nature.

1 Like

Hmm… checked my log, and this is what it does:

2016-10-23 05:08:00.211 [INFO ] [openhab.model.script.ToD.rule1] - Setting TimeOfDay to "Day"
2016-10-23 05:08:00.221 [INFO ] [openhab.model.script.ToD.rule1] - Setting TimeOfDay to "Day"
2016-10-23 16:31:01.625 [INFO ] [openhab.model.script.ToD.rule1] - Updating Time of Day Day, Previous Time of Day Twilight
2016-10-23 18:01:00.999 [INFO ] [openhab.model.script.ToD.rule1] - Updating Time of Day Day, Previous Time of Day Evening
2016-10-23 22:00:00.218 [INFO ] [openhab.model.script.ToD.rule1] - Updating Time of Day Day, Previous Time of Day Evening
2016-10-24 16:32:01.522 [INFO ] [openhab.model.script.ToD.rule1] - Updating Time of Day to Day, Previous Time of Day was Twilight
2016-10-24 18:02:01.077 [INFO ] [openhab.model.script.ToD.rule1] - Updating Time of Day to Day, Previous Time of Day was Evening

It is always Day… took your code verbatim, and only changed the wording of the log info…

Edit – added:

2016-10-25 20:57:48.626 [INFO ] [openhab.model.script.ToD.rule1] - TimeOfDay morning...: 1477339200000
2016-10-25 20:57:48.921 [INFO ] [openhab.model.script.ToD.rule1] - TimeOfDay sunrise...: 2016-10-25T05:06:00.000+10:00
2016-10-25 20:57:49.130 [INFO ] [openhab.model.script.ToD.rule1] - TimeOfDay twilight..: 2016-10-25T16:33:00.000+10:00
2016-10-25 20:57:49.377 [INFO ] [openhab.model.script.ToD.rule1] - TimeOfDay evening...: 2016-10-25T18:03:00.000+10:00
2016-10-25 20:57:49.582 [INFO ] [openhab.model.script.ToD.rule1] - TimeOfDay night.....: 1477400400000
2016-10-25 20:57:52.078 [INFO ] [openhab.model.script.ToD.rule1] - Updating TimeOfDay: Day, Previous TimeOfDay: Evening

What I have noticed is that sunrise is before morning… morning and night come up as millis, while the others are date/time stamps. Can OH compare these without casting them first?

I have noticed one error in my setup now that I’ve been home to notice (it was a busy week this past week) in that it is not transitioning to Night for me. I do need to look into that but that does not address what you are seeing. Here is my Time Of Day logs for the past few days:

2016-10-21 06:00:00.053 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Evening, Previous Time of Day Morning
2016-10-21 07:17:00.060 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Morning, Previous Time of Day Day
2016-10-21 16:40:00.079 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Day, Previous Time of Day Twilight
2016-10-21 18:10:00.058 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Twilight, Previous Time of Day Evening
2016-10-22 06:52:52.439 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Evening, Previous Time of Day Morning
2016-10-22 07:18:00.062 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Morning, Previous Time of Day Day
2016-10-22 16:39:00.063 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Day, Previous Time of Day Twilight
2016-10-22 18:09:00.058 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Twilight, Previous Time of Day Evening
2016-10-23 06:00:00.093 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Evening, Previous Time of Day Morning
2016-10-23 07:19:00.116 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Morning, Previous Time of Day Day
2016-10-23 16:38:00.060 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Day, Previous Time of Day Twilight
2016-10-23 18:08:00.060 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Twilight, Previous Time of Day Evening
2016-10-24 06:00:00.059 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Evening, Previous Time of Day Morning
2016-10-24 07:20:00.086 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Morning, Previous Time of Day Day
2016-10-24 16:36:00.064 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Day, Previous Time of Day Twilight
2016-10-24 18:06:00.057 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Twilight, Previous Time of Day Evening
2016-10-25 06:00:00.056 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Evening, Previous Time of Day Morning
2016-10-25 07:21:00.058 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Morning, Previous Time of Day Day

That can happen if your sunrise is before 06:00 which it is indeed for me some parts of the year for me, but not at the moment. There might be a bug there, though I as pretty sure I had accounted for that in this rule. Since the actual time the sun comes up moves around (not to mention the daylight savings nonsense) the rule should account for this. It should just skip over Morning entirely if Sunrise is before Morning starts.

That is indeed an inconsistency. They should all be the same. However it works as is because the Joda DateTime class has two versions on .isAfter and .isBefore, one that takes a long (i.e. milliseconds) and one that takes a DateTime (really a parent of DateTime but let’s not bring OO programming and inheritance into this right now). It really isn’t OH that is doing the comparison, it’s the Joda DateTime class.

Updates to make them consistent.

    val morning = new DateTime(now.withTimeAtStartOfDay.plusHours(6).millis)
    val sunrise = new DateTime((Sunrise_Time.state as DateTimeType).calendar.timeInMillis)
    val twilight = new DateTime((Twilight_Time.state as DateTimeType).calendar.timeInMillis)
    val evening = new DateTime((Sunset_Time.state as DateTimeType).calendar.timeInMillis)
    val night = new DateTime(now.withTimeAtStartOfDay.plusHours(23).millis)

I need to think some more on why when I run I’m failing to transition to night and for you it is stuck as Day. What is weird is the “Previous Time of Day” does seem to be changing some.

Are you certain the rule is copied in verbatim? It almost looks like PreviousTimeOfDay is being updated twice and TimeOfDay is not or something like that.

    if(TimeOfDay.state.toString != currPeriod) {
    	logInfo(logNameWeather, "Updating Time of Day {}, Previous Time of Day {}", TimeOfDay.state.toString, currPeriod)
    	PreviousTimeOfDay.sendCommand(TimeOfDay.state.toString)
    	TimeOfDay.sendCommand(currPeriod)
    }

EDIT: Wait, I am seeing weirdness in the timestamps in my logs, though the behavior of my other rules seems to be correct. Investigating…

EDIT 2: My bad, I had the arguments passed to the log statement backward. doh! It should be:L

logInfo(logNameWeather, "Updating Time of Day {}, Previous Time of Day {}", currPeriod, TimeOfDay.state.toString)

So in your logs, “Previous Time of Day” is actually the current and visa versa.

EDIT 3: I do see that indeed you should never see a morning since your sunrise is well before 06:00. And based on the error I see in my log statement I see that it is your PreviousTimeOfDay that is stuck at Day and your TimeOfDay is flipping between Twilight and Evening. That is still very wrong but another data point.

EDIT 4: The lack of the transition to Night may have something to do with the problem described here:

I’ve changed my trigger to use only one cron trigger:

Time cron "0 0 6,23 * * ? *"

We will see if that fixes that one problem tonight.

1 Like

OK, adjusting the cron trigger as described above seems to have addressed the no Night problem I was seeing.

Here is the log from the past day and the rule as it exists.

2016-10-25 07:21:00.058 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Morning, Previous Time of Day Day
2016-10-25 16:35:00.059 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Twilight, Previous Time of Day Day
2016-10-25 18:05:00.061 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Evening, Previous Time of Day Twilight
2016-10-25 23:00:00.058 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Night, Previous Time of Day Evening
2016-10-26 06:00:00.060 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Morning, Previous Time of Day Night
2016-10-26 07:22:00.101 [INFO ] [lipse.smarthome.model.script.weather] - Updating Time of Day Day, Previous Time of Day Morning
rule "Get time period for right now" 
when
	System started or
	Time cron "0 0 6,23 * * ? *" or           // Morning start, Night start
	Item Sunrise_Event received update ON or  // Day start
    Item Twilight_Event received update ON or // Twilight start
	Item Sunset_Event received update ON      // Evening start
then
    Thread::sleep(50) // lets make sure we are just a little past the time transition

    val morning = new DateTime(now.withTimeAtStartOfDay.plusHours(6).millis)
	val sunrise = new DateTime((Sunrise_Time.state as DateTimeType).calendar.timeInMillis)
	val twilight = new DateTime((Twilight_Time.state as DateTimeType).calendar.timeInMillis)
	val evening = new DateTime((Sunset_Time.state as DateTimeType).calendar.timeInMillis)
	val night = new DateTime(now.withTimeAtStartOfDay.plusHours(23).millis)

    var currPeriod = "ERROR"
    if     (now.isAfter(morning)  && now.isBefore(sunrise))  currPeriod = "Morning"
    else if(now.isAfter(sunrise)  && now.isBefore(twilight)) currPeriod = "Day"
    else if(now.isAfter(twilight) && now.isBefore(evening))  currPeriod = "Twilight"
    else if(now.isAfter(evening)  && now.isBefore(night))    currPeriod = "Evening"
    else if(now.isAfter(night))                              currPeriod = "Night"

    if(TimeOfDay.state.toString != currPeriod) {
    	logInfo(logNameWeather, "Updating Time of Day {}, Previous Time of Day {}", currPeriod, TimeOfDay.state.toString)
    	PreviousTimeOfDay.sendCommand(TimeOfDay.state.toString)
    	TimeOfDay.sendCommand(currPeriod)
    }
        
end

Compare your version of the rule to the above. Pay particular attention to the if(TimeOfDay.state.toString != currPeriod) section. Based on your logs I suspect the problem lies there.

1 Like

I added a Complicated Example showing how the design pattern applies to other non-time based sequences, in this case controlling the zones of a sprinkler system. In this case the only time based event is the one that kicks off the watering. The rest of the events occur when the Zones turn off. A separate rule get triggered by the Irrigation Item’s state changes.

2 Likes

… well, it depends… maybe complicated in OH :slight_smile:
No, as always, I appreciate your work, and more so sharing of ideas and patterns… as well as replying to noobs like me…

Good Time of Day Design Pattern example rick… added some extra comments here and there as I learned the rule. Let me know if you want those.

With the latest OH2, I found I needed to encapsulate the information as such:

val long sunset_start = new DateTime((Astro_Sun_Set_Time.state as DateTimeType).calendar.timeInMillis).millis
val long sunrise_start = new DateTime((Astro_Sun_Rise_Time.state as DateTimeType).calendar.timeInMillis).millis

for your sunrise and evening values. Did you have to do this to your rules as well?

Actually I’m my current version, which I thought I had posted here, I don’t use the DateTime objects at all any more and just use the milliseconds. There is no reason to create objects as now.isBefore etc all can handle epoc as well.

Upon looking back I do see that I posted it but only in the Astro 2.0 section.

I tried to follow what you did in the OH2 section, but the only solution that I found that properly set the variables with the milliseconds was that convoluted mess. When I did

(Astro_Sun_Set_Time.state as DateTimeType).calendar.timeInMillis

without the extra stuff around it, it complained about calendar not having a timeInMillis field that it could find. Does it look okay to you?

Also, love the trick of doing the offset by changing the geolocation!

Was Designer complaining or the oh logs?

If Designer, which version?

For the moment, please use Eclipse SmartHome Designer 0.8.

If in the log in not sure what the issue could be. The code above is copied verbatim from my running config.

Your rule calls now more than once. Although every call will only differ some milliseconds, from a design pattern perspective it would be stricter to declare a val at the top of the rule that calls now once and use that value across the rule instead of now.

1 Like

It would but I would consider that a micro optimization which is something I typically do not worry about unless and until I actually experience performance, logic, or timing problems. For a rule that executes five times over the course of the day and even with all the calls to now ends up only taking a hundred milliseconds or so to run I wouldn’t worry.

One could argue that it might make the rule more clear and easier to understand in which case I would consider the change.

However, in this case I do not see how defining a new variable to replace the calls to now would actually clarify anything.

What is the specific purpose of assigning a value and not use

var curr = NULL

instead?

Because I can’t sendCommand(null). It will generate an error. I could use curr = NULL which would set the state of vTimeOfDay to undefined but that would mean that whereever I check vTimeOfDay for some reason I’d need to check if it is NULL in addition to checking if it is “Evening”, for example. By using the String “UNKNOWN” I can avoid those extra checks and since the “UNKNOWN” state doesn’t drive any behavior it will not negatively impact any of my rules.

Honestly, “UNKNOWN” should never actually be sent to to the vTimeOfDay unless there is an error in the rule. I use it mainly to catch errors without breaking my other rules.

2 Likes

I am testing a rule with the clause

rule MyRule
when
    Item TimeOfDay received command "AFTERNOON"
then
    // do stuff
end

but it doesn’t seem to trigger. I know it is possible to use received command ON for switches. Is it possible at all to use a string after the received command? Or is it only possible to use

rule MyRule
when
    Item TimeOfDay received command
then
    switch(receivedCommand) {
        case "AFTERNOON" : // do some stuff
        case "BED" : // do other stuff
    }
end

I use the second method you mentioned. Something like this:

rule MyRule
when
    Item TimeOfDay received command
then
    if(TimeOfDay.state.toString == "AFTERNOON") {
                                                   //Do some stuff                            
    }
end
1 Like