Thing Status Reporting [4.0.0.0;4.9.9.9]

logo

A rule that triggers when a Thing changes its status and calls a user defined rule with the Thing and information about the Thing. The conditions of the called script are enforced so events can be filtered there (e.g. only process changes from ONLINE to some other status).

The following properties are passed to the called rule:

Property Contents
thingID The UID of the Thing that changed status
oldStatus The status the Thing was in before the change
oldDetail Some Thing statuses have additional detail. For example, a Thing that has been disabled would have a status of UNINITIALIZED with detail DISABLED. If there is no detail, it will be ā€˜NONEā€™.
newStatus The status the Thing changed to.
newDetail The detail the Thing changed to, or NONE.
thing The actual JS Scripting Thing Object (see Thing - Documentation)

The original Java Thing Object can be reached through thing.rawThing. The Most of the properties besides the thing are included to make writing the rule called by this one easier to write in Blockly.

Additional conditions can be placed on this rule or on the called rule to further limit when it runs (e.g. donā€™t run at night).

Feel free to customize this rule as much as desired to meet your needs. You could even replace the code that calls your rule and put that processing inline.

An important note, this version works significantly differently from the older OH 3.x version of the rule template. But it works much closer to how I envisioned the original one to work in the first place but didnā€™t know how to configure the trigger properly.

Limitations:

Iā€™ve seen situations where this rule will throw a bunch of exceptions during startup as it gets ponded with Things coming into existence and initializing. Once runlevel 100 is reached the rule works as expected. If you see this you can do the following:

  1. Create a Runlevel Number Item
  2. Create a rule that triggers at system runlevel 40
  3. Add an Item Action and update the Runlevel Item to 40
  4. Repeat 2-3 for runlevels 50-100.
  5. Add a condition to this rule to not run unless the runlevel is >=100.

Iā€™m looking into a rule template that will enable/disable rules based on the runlevel reached so stay tuned to the marketplace. See Delay Start [4.0.0.0;4.9.9.9].

Language: JS Scripting (ECMAScript 2021 with the openhab-js helper library injection enabled)

Dependencies:

  • OH 4.0.0 +
  • JS Scripting add-on installed
  • openhab-js 4.1.0 or later
  • openhab_rules_tools 2.0.1 or later

Changelog

Version 0.2

  • throws an exception of openhab-js or openhab_rules_tools are not new enough

Version 0.1

  • initial release

Sponsorship

If you want to send a tip my way or sponsor my work you can through Sponsor @rkoshak on GitHub or PayPal. It wonā€™t change what I contribute to OH but it might keep me in coffee or let me buy hardware to test out new things.

Resources

https://raw.githubusercontent.com/rkoshak/openhab-rules-tools/main/rule-templates/thingStatus/thing_status2.yaml

3 Likes

Did you see this discussion: rework GenericEventTrigger and GenericEventCondition by ccutrer Ā· Pull Request #3299 Ā· openhab/openhab-core Ā· GitHub?

I have but didnā€™t really read it closely to understand what it says. But on a closer read, none of the changes seem to invalidate what Iā€™m trying to do here. They are just some changes to the names of the configuration properties and the way the matching filter works (if I read that issue correctly). I fully expect Iā€™ll need to adjust most if not all of my rule templates over the coming months so that sort of thing doesnā€™t bother me. This one was a simple one that will let me experiment with the marketplace and such.

I donā€™t plan on adding the ā€œpublishedā€ tag until we get closer to release so changes I have to make shouldnā€™t be too disruptive.

However, if Iā€™m misunderstanding the PR and I canā€™t actually use the GenericEventTrigger to trigger a rule on ThingStatusChangedEvents at all Iā€™ll have more concerns.

Ideally, this whole rule template would best be replaced with something like a Member of trigger except for Things. So I look at this as a stop gap (perhaps a long term one :wink: ).

Thanks for the heads up!

Edit: since the change was merged Iā€™ve now updated the rule template to use the new property names and a glob for the topic instead of the regex.

1 Like

I have installed thing status reporting from the marketplace but I am missing openhab_rules_tools and I canā€™t work out how to install it? Am I missing something obvious?

Script execution of rule with UID '8c4b7b49f7' failed: org.graalvm.polyglot.PolyglotException: TypeError: Cannot load CommonJS module: 'openhab_rules_tools'

If you are on openHABian it can be installed through openhabian-config.

If not, from $OH_CONF/automation/js run the command npm install openhab_rules_tools

1 Like

Thanks that worked.

I canā€™t quite see how i can access the context in my rule. Is there some documentation around iā€™m missing?

What rulesā€™s language are you using?

In JS Scripting and Rules DSL you just reference the variable name: thingID.

In Nashorn JS youā€™d use context.thingID.

I donā€™t know about jRuby.

In Blockly youā€™d use the ā€œget context attributeā€ block which can be found under the Rule and Process category.

Iā€™m using DSL. I tried:

        var thingLabel = thing.label

but i get:

The name 'thing' cannot be resolved to an item or type; 

Itā€™s been a very long time since I tried it in Rules DSL and now that I think about it I donā€™t know that I ever really did try it in Rules DSL. I may have just assumed it gets passed into the rule the same as everything else that gets passed into the rule. I ran a couple of tests and as far as I can tell, you cannot pass arguments into a Rules DSL rule. If it is possible, I donā€™t know how to do it.

@rlkoshak any chance you could demonstrate how to use this template with a rule (or script?) that did an announce to a user with the thing name (I think this is thing.label?) and status its changed to - I assume I can have one rule/script to manage this?

Here is the rule I have that gets called by the above.

configuration: {}
triggers: []
conditions:
  - inputs: {}
    id: "1"
    label: Thing is Zigbee coordinator, Zwave controller, or Garage Camera
    configuration:
      type: application/javascript
      script: >
        var key = ruleUID+'_thingToItem';

        if(!cache.shared.exists(key)) {
          cache.shared.put(key, { 'zwave:serial_zstick:zw_controller'      : 'Zwave_Status',
                                  'zigbee:coordinator_ember:zg_coordinator': 'Zigbee_Status',
                                  'ipcamera:generic:garage_camera'         : 'GarageCamera_Raw',
                                  'mqtt:topic:broker:family'               : 'FamilyRoomWaveplus_Status',
                                  'mqtt:topic:broker:basement_waveplus'    : 'BasementAirthingsWavePlus_Status'
                                });
        }


        cache.shared.get(key)[thingID] !== undefined;
    type: script.ScriptCondition
actions:
  - inputs: {}
    id: "2"
    label: Process Thing status
    description: Log zigbee/zwave, set Garage Camera status
    configuration:
      type: application/javascript
      script: >
        console.debug('Thing ' + thingID + ' changed from ' + oldStatus + ' (' +
        oldDetail + ') to ' 
                      + newStatus + '(' + newDetail + ')');

        var itemName = cache.shared.get(ruleUID+'_thingToItem')[thingID];

        var newState = (newStatus == 'ONLINE') ? 'ON' : 'OFF';


        console.debug(cache.shared.get(ruleUID+'_thingToItem'));

        console.debug('Thing status update: ' + itemName + ' ' + newState);

        items[itemName].postUpdate(newState);
    type: script.ScriptAction

The condition checks to see if the thingID is one of the few Things that I want to have processed by the rule and it creates a mapping between the Thing and the status Item I use to represent that Thing in the semantic model. Without the condition the rule will process all Thing events.

The. action script does some debug level logging but all it really does is set the mapped Item to ON for ONLINE and OFF for all other Thing statuses.

I donā€™t really use voice announcements but I can follow the docs for the say action.

Correct.

newStatus and newDetail represent the status and description the Things just changed to.

So a simple script action that just uses say to report the changes would look something like:

var msg = 'Thing ' + thing.label + ' changed to ' + newStatus + ", " + newDetail;
actions.Voice.say(msg);``

All the properties in the table above get passed into the called rule. In JS Scripting in the UI, they just appear as a variable. In .js files Iā€™m not sure how they appear but assume the same. In Blockly youā€™d use Run and Process ā†’ get context attribute to access the passed in data.

image

You might need to use an inline script block to pull the thing.label as I donā€™t see other ways to do that through the blocks, though I could just be missing that.

Rules DSL, as far as I can tell, does not have access to the passed in variables. I do not know how to access the passed in variables in the other rules languages.

Thanks @rlkoshak - as usual I should be careful what I wish for - this is way above what I can really comprehend or adapt to my simple set up - but if I am reading this correctly - are you getting to the point where you are mapping thing status to update individual items for status in a nutshell?

At a high level this rule creates a mapping between Thing ID and Item name and if the Thing changed to ONLINE it updates the mapped Item to ON and OFF for all other states. Everything else is logging.

The rule you see above is the YAML for a managed rule (i.e. a rule created through the UI). Managed rules have some metadata (name, description, etc.) and three main sections (triggers, conditions, and actions).

Triggers should be self explanatory.

At a high level, the condition does the following:

       var key = ruleUID+'_thingToItem';

That sets a variable with a unique value that comes from the ruleUID which is an implicit variable that exists in side of JS Scripting script actions and script conditions. When working with the shared cache it is important to ensure your keys are unique and unlikely to be overridden by some other rule.

        if(!cache.shared.exists(key)) {
          cache.shared.put(key, { 'zwave:serial_zstick:zw_controller'      : 'Zwave_Status',
                                  'zigbee:coordinator_ember:zg_coordinator': 'Zigbee_Status',
                                  'ipcamera:generic:garage_camera'         : 'GarageCamera_Raw',
                                  'mqtt:topic:broker:family'               : 'FamilyRoomWaveplus_Status',
                                  'mqtt:topic:broker:basement_waveplus'    : 'BasementAirthingsWavePlus_Status'
                                });
        }

This line checks to see if an entry in the shared cache for key exists and if it doesnā€™t it populates it with a mapping between Thing UID and Item names. There are other ways to do this but since I have so few Things this was expedient. This mapping is used in the script action too, hence the use of the shared cache.

Finally this line tests to see if the thingID, which is a variable passed into the rule from the rule template, exists in the map. The last line of a condition needs to evaluate to true or false. If false the script action(s) will not be run. If true the script action(s) will be run.

In the UI it looks like this:

or with just the text

var key = ruleUID+'_thingToItem';
if(!cache.shared.exists(key)) {
  cache.shared.put(key, { 'zwave:serial_zstick:zw_controller'      : 'Zwave_Status',
                          'zigbee:coordinator_ember:zg_coordinator': 'Zigbee_Status',
                          'ipcamera:generic:garage_camera'         : 'GarageCamera_Raw',
                          'mqtt:topic:broker:family'               : 'FamilyRoomWaveplus_Status',
                          'mqtt:topic:broker:basement_waveplus'    : 'BasementAirthingsWavePlus_Status'
                        });
}

cache.shared.get(key)[thingID] !== undefined;

Donā€™t let the YAML formatting confuse or scare you. The YAML is convenient for sharing a complete rule on the forum but you rarely if ever actually work with it. Instead you work with each part of the rule independently.

In Rules DSL or a file based rule, the condition would be implemented as part of the Ruleā€™s body as an if since there are no conditions supported in file based rules typically (jRuby and JS Scriptings rule builder are exceptions).

In the action we have the following:

        console.debug('Thing ' + thingID + ' changed from ' + oldStatus + ' (' + oldDetail + ') to ' 
                      + newStatus + '(' + newDetail + ')');

Log out the variables that the rule template passed into this rule that I care about.

var itemName = cache.shared.get(ruleUID+'_thingToItem')[thingID];

Pull the mapping between Thing IDs and Item names from the cache and get the Item name that corresponds with the Thing ID. Remember this was created the first time the rule ran in the condition above. The shared cache is how scripts, be they part of the same rule or part of different rules, share data between themselves. Previously a ā€œglobal variableā€ was used for this purpose but that approach should be considered deprecated.

var newState = (newStatus == 'ONLINE') ? 'ON' : 'OFF';

This line uses the JS ternary operator to determine the state that the Status Item should be in. If the Thing changed to ONLINE it should be ON, and OFF for all other states.

console.debug(cache.shared.get(ruleUID+'_thingToItem'));

This logs out the name of the Item that maps to the Thing. This line probably isnā€™t needed anymore.

console.debug('Thing status update: ' + itemName + ' ' + newState);

This logs out the Item name and what that Item will be updated to.

items[itemName].postUpdate(newState);

Finally we update the Item with the newState.

Again, while actually working with the code it will look like this:

console.debug('Thing ' + thingID + ' changed from ' + oldStatus + ' (' + oldDetail + ') to ' 
              + newStatus + '(' + newDetail + ')');

var itemName = cache.shared.get(ruleUID+'_thingToItem')[thingID];
var newState = (newStatus == 'ONLINE') ? 'ON' : 'OFF';

console.debug(cache.shared.get(ruleUID+'_thingToItem'));
console.debug('Thing status update: ' + itemName + ' ' + newState);
items[itemName].postUpdate(newState);

Thanks for the comprehensive explanation.

Thanks
Andrew