Design Pattern: State Machine Driven Groups

Please see Design Pattern: What is a Design Pattern and How Do I Use Them to understand the scope and purpose of a design pattern. As with all DPs, the DP is not limited for the use demonstrated in the examples.

Problem Statement

This is going to be one of the more complex design patterns and it depends upon many other design patterns.

First of all, what is a state machine. For the purposes of this DP a state machine is a situation where when certain events occur a new state may be entered. A State Machine Primer with HABlladin, the openHAB Genie and Washing Machine State Machine are two great tutorials to get you started with a state machine. But an ultra simple state machine is actually Design Pattern: Time Of Day.

There are two ways to use the states from a state machine. First, one can change the behaviors by checking the current state of the machine and doing one thing instead of another based on the state. For example,

if(vTimeOfDay.state == "DAY") {
    // do something
}
else if(vTimeOfDay.state == "EVENING") {
    // do something else
}

The second is to do something in reaction to the state changes. For example

rule "It's night time"
when
    Item vTimeOfDay changed to "NIGHT"
then
    // do something
end

Often, the ā€œdo somethingsā€ from above will be to send a bunch of commands or set a scene. the naive implementation would be a Switch statement or if else statements.

switch(vTimeOfDay.state.toString) {
    case "MORNING": {
        ItemOne.sendCommand(ON)
        ItemTwo.sendCommand(ON)
        ItemThree.sendCommand(ON)
    }
    case "DAY": {
        ItemOne.sendCommand(OFF)
        ItemTwo.sendCommand(ON)
        ItemThree.sendCommand(OFF)
    }
    case "AFTERNOON": {
        ItemOne.sendCommand(OFF)
        ItemTwo.sendCommand(OFF)
        ItemThree.sendCommand(ON)
    }
    // and so on
}

Is there a way to avoid this tedious and hard to maintain code?

Concept

Use the name of each state to establish Groups. For example, a Lights_ON_MORNING Group for the lights that are to come on in the morning as calculated by the Time of Day state machine. There might need to be more that one Group, for example Light_OFF_MORNING for the lights that are to turn OFF in the morning.

In your Rule, just send the command to the Group.

Simple Example

The state machine part of this example is the Time of Day DP. In this example, we want to control the lights based on the time of day states. The lights are simple Switches.

Items

Group:Switch:OR(ON, OFF) gLights_ON_MORNING
Group:Switch:OR(ON, OFF) gLights_OFF_MORNING
Group:Switch:OR(ON, OFF) gLights_ON_DAY
Group:Switch:OR(ON, OFF) gLights_OFF_DAY
Group:Switch:OR(ON, OFF) gLights_ON_AFTERNOON
Group:Switch:OR(ON, OFF) gLights_OFF_AFTERNOON
Group:Switch:OR(ON, OFF) gLights_ON_EVENING
Group:Switch:OR(ON, OFF) gLights_OFF_EVENING
Group:Switch:OR(ON, OFF) gLights_ON_NIGHT
Group:Switch:OR(ON, OFF) gLights_OFF_NIGHT
Group:Switch:OR(ON, OFF) gLights_ON_BED
Group:Switch:OR(ON, OFF) gLights_OFF_BED

Switch aFrontLamp "Front Room Lamp" (gLights_ON_MORNING, gLights_OFF_DAY, gLights_ON_AFTERNOON, gLights_ON_EVENING, gLights_OFF_NIGHT, gLights_OFF_BED)

Switch aFamilyLamp "Family Room Lamp" (gLights_ON_MORNING, gLights_OFF_DAY, gLights_ON_AFTERNOON, gLights_ON_EVENING, gLights_OFF_NIGHT, gLights_OFF_BED)

Switch aPorchLight "Front Porch" (gLights_OFF_MORNING, gLights_OFF_DAY, gLights_ON_AFTERNOON, gLights_ON_EVENING, gLights_OFF_NIGHT, gLights_OFF_BED)

Switch aMBR_Lights "Master Bedroom Lights" (gLights_ON_EVENING)

Python

from core.rules import rule
from core.triggers import when

@rule("Set lights based on Time of Day")
@when("Item vTimeOfDay changed")
def lights(event):
    events.sendCommand("gLights_OFF_{}".format(items["vTimeOfDay"]), "OFF")
    events.sendCommand("gLights_ON_{}".format(items["vTimeOfDay"]), "ON")

Rules DSL

// Theory of operation: Turn off the light that are members of gLights_OFF_<TOD> and
// then turn on the lights that are members of gLights_ON_<TOD>. Reset the overrides.
rule "Set lights based on Time of Day"
when
    Item vTimeOfDay changed
then
    sendCommand("gLights_OFF_"+vTimeOfDay.state.toString, "OFF")
    sendCommand("gLights_ON_"+vTimeOfDay.state.toString, "ON")
end

Theory of Operation

If you look at the way the Items and Groups are defined, itā€™s a table.

- aFrontLamp aFamilyLamp aPorchLight aMBR_Lights
MORNING ON ON OFF NA
DAY OFF OFF OFF NA
AFTERNOON ON ON ON NA
EVENING ON ON ON ON
NIGHT OFF OFF OFF NA
BED OFF OFF OFF NA

I strongly recommend establishing a table like this when trying to establish what you want to happen during the various states. If you have a lot of Items to control it might make more sense to put the Items as the rows and the states as the columns.

Armed with a table like this it is just a matter of adding the lights Items to the right Group. So, for example, we want the front lamp to turn on in the morning so we add it to gLights_ON_MORNING. It should be off during the day so we add it to gLights_OFF_DAY.

The Rule is ultra simple, just two lines of real code. When the time of day changes, we send OFF to the OFF Group for the current time of day state and we send ON to the Group for the current time of day state.

But with a configuration like this, changing the behaviors of the lights is a simple matter of changing Group membership. Need to add a new light? Just make sure to add it to the right Groups. Notice that aMBR_Lights is only a member of one of the Groups. Thatā€™s fine too. There is nothing that says that the light needs to be controlled for all the times of day.

Complex Example

What if there is a mix of Item types? It is reasonable to expect that there are Dimmers or Color Items involved in a lighting example.

Items

We will again be using Design Pattern: Associated Items, only this time we will be using it to encode Dimmer and Color values for each light for each time of day. We will use the pattern <Light Name>_<Time of Day> for the Items that store the state to be used for that light at that time of day.

Group:Switch:OR(ON, OFF) gLights
Group gSettings

Switch aFrontLamp "Front Room Lamp" (gLights)
Switch aFrontLamp_MORNING (gSettings)
Switch aFrontLamp_DAY (gSettings)
Switch aFrontLamp_AFTERNOON (gSettings)
Switch aFrontLamp_EVENING (gSettings)
Switch aFrontLamp_NIGHT (gSettings)
Switch aFrontLamp_BED (gSettings)

Dimmer aFamilyLamp "Family Room Lamp" (gLights)
Dimmer aFamilyLamp_MORNING (gSettings)
Dimmer aFamilyLamp_DAY (gSettings)
Dimmer aFamilyLamp_AFTERNOON (gSettings)
Dimmer aFamilyLamp_EVENING (gSettings)
Dimmer aFamilyLamp_NIGHT (gSettings)
Dimmer aFamilyLamp_BED (gSettings)

Color aPorchLight "Front Porch" (gLights)
Color aPorchLight_MORNING (gSettings)
Color aPorchLight_DAY (gSettings)
Color aPorchLight_AFTERNOON (gSettings)
Color aPorchLight_EVENING (gSettings)
Color aPorchLight_NIGHT (gSettings)
Color aPorchLight_BED (gSettings)

Populating the <Light Name>_<Time of Day> Items is beyond the scope of this DP. See Design Pattern: Encoding and Accessing Values in Rules for some approaches.

Python

from core.rules import rule
from core.triggers import when

@rule("Set lights based on Time of Day")
@when("Item vTimeOfDay changed")
def lights(event):
    for light in ir.getItem("gLights").members:
        setting = items["{}_{}".format(light.name, items["vTimeOfDay"])]
        events.sendCommand(light.name, setting)

Rules DSL

This rule will leverages the Associated Items DP.

rule "Set lights based on Time of Day"
when
    Item vTimeOfDay changed
then
    gLights.members.forEach[ light | 
        val setting = gSettings.members.findFirst[ s | s.name = light.name+"_"+vTimeOfDay.state.toString ]
        if(setting !== null) light.sendCommand(setting.state)
    ]
end

Theory of Operation

Each Item has an associated setting Item for each time of day where it needs to change state. When the time of day changes, we loop through all of the lights, get the associated setting Item for that light and time of day and send that setting as a command to the light.

Advantages and Disadvantages

Advantages:

  • Vastly reduced lines of Rules code and Rules complexity.
  • Can modify behaviors for Items without modifying Rules.
  • Can add new states without modifying Rules.

Disadvantages:

  • Many more Groups and Items required.
  • There is not way to configure this using a nice table format potentially leading to reconfiguration errors.

Related Design Patterns

Design Pattern How Itā€™s Used
Doing it smarter - help with rule code rollershutter automation An example where the state from the state machine is used to drive different behaviors instead of being used as an event to cause something to happen
A State Machine Primer with HABlladin, the openHAB Genie Example state machine tutorial
Washing Machine State Machine An example state machine tutorial
Design Pattern: Time Of Day An example state machine tutorial, used to drive the examples
Design Pattern: Associated Items Used to sendCommands and get the states from associated Items based on Item names.
Design Pattern: Encoding and Accessing Values in Rules How to initially populated the settings Items in the complex example
9 Likes

Very nice concept. I learned so much from your design patterns. I do something similar. Just one difference. Instead of all the extra items I use a map file. This file is stuctured as follows:

GardenLightNight=5
GardenLightDay=50
GardenLightEvening=40
DownstairsLightNight=10
DownstairsLightDay=70
DownstairsLightEvening=40

When I want my rules to set the light to a certain level I simply use

rule "Set lights based on Time of Day"
when
    Item LichtScene changed
then
DimmerGroup.members.forEach[light |
	val SearchValue = light.name.replace('_Dimmer','') + LichtScene.state.toString
	val Number LightValue = new DecimalType(transform("MAP", "lights.map", SearchValue))
	sendCommand(light, LightValue)
]
end

In this example LichtScene is my item I use to track the time of day. DimmerGroup is the group of light dimmers I want to adjust. All the items in that group have names like BedroomLight_Dimmer, hence the need to remove the ā€œ_Dimmerā€ part of the name to search the map file. In this way I can find the appropriate light setting for the specific time of day.

The advantage is that it is really easy to keep track of all the different lights and settings. I suppose by using JSON it would be even more powerfull, but i found JSON files too difficult to edit.

2 Likes

Thatā€™s a fantastic approach! Thanks for posting!

When/if you move to the new rule engine, you can use metadata to store this information. In OH3, we should be able to modify it through a UI too!

You beat me to it, Rich :slightly_smiling_face:! This is how all of my lighting and sound automations work, and some others too. One rule for everything, with lux, timers, TimeOfDay, etc. I call it the Mother of the Mother of All Lighting Rules. Just take your example a little furtherā€¦ all triggering Items (motion, contact, locks, etc.) separated into groups by area, and those groups as members of a single Area_Triggers group. Trigger on ā€˜Member of Area_Triggersā€™ and use Associated Items to call actions on the corresponding Area_Actions groups. Most of the logic turns to the group functions, rather than having to build complex logic in rules.

One of these days Iā€™ll get around to posting that, but planning on putting it into a template and wanted the Jython addon to be available first. But after your post, I should probably just post it now as an advanced example of this DP! I think this rule could be a bit of a game changer for new users getting their automation setup quickly.

I can add it to the OP if you want or it can be in the comments here or itā€™s own posting. I love to see more complex examples as it shows how the DPs scale. And the fact that Iā€™m not the only one to come up with this approach further validates it as a DP. :smiley:

Iā€™ve had my lighting run like this almost since the beginning. This was when I stopped worrying and started to love the bombā€¦er the Rules DSL. But as you are well aware, the approach is equally at home in JSR223. Having a template would be awesome for sure.

I donā€™t know why I waited so long to post this as a DP.

For textual rules that will probably only be in one but more likely two years after the official launch. I have no programming background whatsoever. So I am dependant on examples on the forum that I can copy and modify. If it wasnā€™t for all the great people posting questions and examples to learn from I would never have gotten my house running. That is why I love the DPs. It will take quite some time until there is an equal amount of examples to learn from.

For the UI based rules, that depends on the UI of course. Looking forward to seeing that. Would have loved to have UI based rules when I started automating my home some years back.

Here is the post!

2 Likes