Design Pattern: Encoding and Accessing Values in Rules

Edit: Added link to item_init library.

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

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.

Approach 1: 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.

There is a Python library, init_items, located at https://github.com/rkoshak/openhab-rules-tools that lets you define Item metadata used to initialize the Item at OH startup or in response to a command to the Item InitItems which will be automatically created if it doesn’t already exist. For example:

// Set Presence to OFF at boot every time
Switch Presence { init="OFF"[override="True"] }

// Initialize MyLamp to 123,45,67 if it's NULL or UNDEF at start
Color MyLamp { init="123,45,67" }

// Initialize TerhermostSetpoint to 70 but only if it's NULL or UNDEF, remove
// the init metadata after start.
Number ThermostatSetpoint { init="70"[override="False", clear="True"] }

Approach 2: 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 3: 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.

Approach 4: 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 5: 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)

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
17 Likes
Taking A Rule To The Next Level
How to store additional info in ITEM?
Create a datetime item
[SOLVED] Dummy Item with predefined value / state
Setting light themes in rules
Help with Regex Placeholder
Man/Auto mode switch
Rule higher than 40 degrees notification sent
Rollershutter Group positioning
How to calculate the time of a specific azimuth
Design Pattern: State Machine Driven Groups
Counter for rollershutter shading
No Global Variables? Time to use Node-Red
Grouping items in rules - customer data structures like pojo
Design Pattern: Gate Keeper
Unchanging string in items
Adding custom Java code
[SOLVED] Help with a light timer rule
[SOLVED] How to compare a value/variable against a bunch of numbers?
Hue lights misbehaving
Roadmap to Happiness - What is missing in the core framework
Arrays as Global Variables
Please test the new Expire Binding
Http1 binding using a base url
Setting up a cron based sprinkler
Possible to use transform in a rule?
Way to save system settings
Reduce the size of a rule
BiDirectional HashMap in OH?
GDPR Compliance and NEW WEBSITE!
Absolute value of a decimal
[SOLVED] Dummy Item with predefined value / state
[SOLVED] Presence detection with iCloud binding
Squeezebox Player WIP (Help Appreciated)
Virtual Item - String - Default Value
How I have automated my lights
Shelly 1 - Set temperature via http
Hue scene follow a switch
OH3 - How to get the type of an item
Position estimator for shutters
MainUI Equipment Questions: Add URL? Compare value to Point?
OH3 - Help with rules rest api broker for Sony TV integration
How to concatenate parameters in command that I should send out
Exec binding: Controlling two bluetooth Eqiva EQ3 radiator valves simultaneously
Rule / Switch not reliable
Add new attribute to an Item

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.