Design Pattern: Encoding and Accessing Values in Rules

Tags: #<Tag:0x00007f745406b060>

Please see Design Pattern: What is a Design Pattern and How Do I Use Them for how to read and use a DP.

Problem Statement

image
Often a user has the desire to be able to store and retrieve a series of constants or other values in a Rule. For example, a user may have an RFID reader and want to store the list of authorized and disallowed IDs in a flexible manner. As another example, one may be sending commands to a gateway and need to include the ID of the target actuator as part of the message. Yet another example may be a set of target temperatures for a series of radiators.

NOTE: The author’s preferred approach is #2 and #3 depending on the nature of the data.

Approach 1: Global Variables

Concept

Create one or more global vals that store the desired information in a way that it is easy to retrieve.

This approach is particularly well suited for situations where

  • the data is simple
  • the data has a one to one relationship with an associated Item
  • the data is static and not likely to change much

Limitations include

  • the data is hard coded
  • it must be recreated at startup
  • it must be kept in sync with Items

Example

Let’s assume we have a number of thermostats and want to have a set of target temps for each one. Each thermostat has its own Item and is a member of the Group Thermostats.

import java.util.Map

val Map<String, Number> presentTargets = newHashMap
val Map<String, Number> awayTargets = newHashMap

// there is a way to initialize the Map statically but I always forget it so prefer a System started Rule
rule "Populate targets"
when
    System started
then
    presentTargets.put("Room1_Thermostat", 65)
    presentTargets.put("Room2_Thermostat", 67)
    presentTargets.put("Room2_Thermostat", 66)

    awayTargets.put("Room1_Thermostat", 55)
    awayTargets.put("Room2_Thermostat", 57)
    awayTargets.put("Room2_Thermostat", 56)
end

rule "Presence changed, update target temps"
when
    Item Presence changed
then
    Thermostats.members.forEach[thermostat |
        val tgt = if(Presence.state == ON) presentTargets.get(thermostat.name) else awayTargets.get(thermostat.name)
        thermostat.sendCommand(tgt)
    ]
end

Approach 2: Data Items

Concept

Apply Design Pattern: Associated Items and store the values in Items.

This approach is well suited for situations where

  • the encoded values need to be adjusted from the UI

Limitations include

  • must have a separate Item for each value which can become unwieldy
  • sitemap/HABpanel can become overwhelming if there are a lot of values

Example

Using the same example from above we create a Group to hold the target temps and a new Item for each target temp.

// This rule is only needed during the first run if you are using persistence with restoreOnStartup on the target temp Items
rule "Initialize the target temps"
when
    System started
then
    // we will initialize them all to the same value
    Thermostats.members.forEach[thermostat |
        val presentTgt = Targets.findFirst[tgt | tgt.name == thermostat.name + "_PresentTarget"]
        val awayTgt = Targets.findFirst[tgt | tgt.name == thermostat.name+"_AwayTarget"]
        presentTgt.postUpdate(65)
        awayTgt.postUpdate(55)
    ]
end

rule "Presence changed, update target temps"
when
    Item Presence changed
then
    Thermostats.members.forEach[thermostat |
        val tgtName = thermostat.name + if(Presence.state == ON) "_PresentTarget" else "_AwayTarget"
        val target = Targets.findFirst[tgt | tgt.name == tgtName]
        thermostat.sendCommand(target.state as Number)
    ]
end

The key takeaway with this approach is that the System started Rule is required to boot strap the value of the data Items. If you are using restoreOnStartup for those Items, which I recommend, you should remove this Rule after the first run.

Appoach 3: Encode Value in Item Name

Concept

Append the value that you care about as part of an Associated Item’s name and parse the value out of the name.

This approach is well suited for situations where

  • each piece of data is static but the list of data may need to change (e.g. authorized and unauthorized RFID IDs)
  • no system started rule required to boot strap the value, value is coded into the Item’s name so only the Items need to be changed to add, remove, modify the values.

Limitations include

  • can only change values by modifying, adding, or removing Items, though this can be seen as an advantage in some circumstances

Example

This example will switch to an RFID reader. We will use Items with the ID encoded in the name to represent each ID card and an Authorized and Unauthorized Group to control access based on the presented ID.

Items

Group:Switch Authorized
Group:Switch Unauthorized

Switch RFID_12345 "Bob's ID" (Authorized) { expire="1m,state=OFF" }
Switch RFID_67890 "Ann's ID" (Authorized) { expire="1m,state=OFF" }
Switch RFID_09876 "Guest ID" (Unauthorized) { expire="1m,state=OFF" }

String RFID { ... } // when the card reader reads a card this Item receives a command with the ID

Rules

rule "RFID triggered"
when
    Item RFID received command
then
    val itemName = "RFID_"+receivedCommand
    val authorized = Authorized.members.findFirst[ id | id.name == itemName ]
    val unauthorized = Unauthorized.members.findFirst[ id | id.name == itemName ]
    
    // Purposefully putting unauthorized first so if an ID is in both Groups the unauthorized Group takes precedence
    if(unauthorized != null) {
        // alert attempted unauthorized access
    }
    else if(authorized != null) {
        // unlock the door
        authorized.sendCommand(ON) // we can do something special for individual IDs if desired
    }
    else {
        // alert attempted unkown access
    }
end

// We can do something special for certain IDs
rule "Bob came home"
when
    Item RFID_12345  received command ON
then
    say("Welcome home Bob!")
end

Approach 4: Scripting Automation Configuration

This approach is mainly suitable for Scripting Automation (e.g. JSR223 Python). When using the Helper Libraries there is a file located in automation/lib/<langauge>/configuration.<language extension>. This file can be imported into your scripts and your modules and makes the perfect place to define data structures like those discussed in Approach 1.

configuration.py

present_targets = {"Room1_Thermostat": 65,
                  "Room2_Thermostat": 67,
                  "Room3_Thermostat": 66}
away_targets = {"Room1_Thermostat": 55,
                "Room2_Thermostat": 57,
                "Room3_Thermostat": 56}

usage:

from core.rules import rule
from core.triggers import when
from configuration import present_targets, away_targets

@rule("Presence changed, update target temps")
@when("Item Presence changed")
def prese_thermo(event):
    for thermostat in ir.getItem("Thermostats").members:
        tgt = present_targets[thermostat.name] if Presence.state == ON else away_targets[thermostat.name]
        events.sendCommand(thermostat, tgt)

Approach 5: Item Metadata

When using Scripting Automation one has access to Item Metadata in Rules. You can statically define metadata on Items or dynamically set and update metadata which is a great way to define constants and other configuration data. See Design Pattern: Using Item Metadata as an Alternative to Several DPs for more details.

Related Design Patterns

Design Pattern How Used
Design Pattern: Associated Items Naming of Items in Approach 2 and 3
Design Pattern: Unbound Item (aka Virtual Item) RFID Items in Approach 3
Design Pattern: Working with Groups in Rules Looping through thermostats in Approach 1 and 2, getting the associated Items from a Group in Approach 3
Design Pattern: Using Item Metadata as an Alternative to Several DPs Approach 5
14 Likes

In Approach 1 shouldn’t that be “presentTargets.get(thermostat.name)”? I’ve been trying to get something similar to this to work for some time based on this example and it turns out that the bracket type was wrong.

Also there’s a missing import at the beginning which I also needed:

import java.util.Map

The import is there, first line on the example code.

The calls to get are indeed wrong.

I don’t usually recommend approach 1 and mainly included it for completeness. I should probably reorder them by preference.

Har, you’re right, when I scrolled the screen down it scrolled the line off the top of the sub-frame and I didn’t notice…

It worked well for me, I wanted a set of static maps defined as variables to form presets for my radiators so thanks for the pointer.