deconz-rest-plugin: Rules triggering logic

I’ve mentioned this in several other issues, but maybe it deserves an issue on its own: the way the deCONZ REST API plugin triggers rules differs significantly from the way the Hue bridge triggers rules. On the Hue bridge, rules are triggered when the last condition becomes true, on deCONZ while all conditions are true.

This causes many issues using rules created for the Hue bridge in deCONZ. While not unsolvable, re-writing the rules for deCONZ almost triples their footprint (as in number of rules and conditions needed to achieve the same logic).

As an example, on the Hue bridge I use a series of rules to control the lights in a room, based on:

  • The virtual master switch of the room, a CLIPGenericFlag sensor say /sensors/220;
  • The light sensor in the Hue motion sensor in that room, a ZLLLightLevel/ZHALight sensor, say /sensors/222;
  • A CLIPGenericFlag sensor to indicate night mode, say /sensors/2.

On the Hue bridge, I would use the following conditions, in a rule that recalls the daytime scene for the room (I actually use /sensors/222/state/dark in the second condition, but that’s another story):

  "conditions": [
    {
      "address": "/sensors/220/state/flag",
      "operator": "eq",
      "value": true
    },
    {
      "address": "/sensors/222/state/lightlevel",
      "operator": "lt",
      "value": 12000
    },
    {
      "address": "/sensors/102/state/flag",
      "operator": "eq",
      "value": false
    }
  ]

On the Hue bridge this rule fires when:

  1. The virtual switch is switched on while light level < 12000 and night mode is off;
  2. The light level drops below 12000 while the virtual switch is on and night mode is off;
  3. Night mode is switched off while the virtual switch is on and light level < 12000.

On the deCONZ this rule fires continuously while all three conditions hold, causing a scene recall every second. With a couple of rooms setup like this, the ZigBee network is swamped. To remedy this, I’d need three rules, each with an added dx condition. So instead of one rule with three conditions, I need three rules with a total of twelve conditions.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Comments: 33 (10 by maintainers)

Commits related to this issue

Most upvoted comments

Time for some Friday rule hacking 😃

YES, let’s do this!

Can you confirm my assumptions when the rule should trigger?

I’m afraid I cannot. Sorry, I’m afraid you need to “unthink” the current rules triggering logic in deCONZ and take a fresh look. Your first example is, in fact, artificial; the other two are not - I’ve been using both extensively.

The Hue documentation is very vague, so my understanding is based on reverse-engineering and lots of trial and error.

The most confusing part (if you will: paradigm shift) is that each element (object) in the conditions array serves one of two distinct purposes:

  • It is either a trigger, causing the rule to be evaluated; or
  • It is a condition to be tested when the rule is evaluated.

The rule actions are only executed when a trigger happens and all conditions hold. No need to consider lasttriggered. When evaluating a rule, only one element serves as trigger; the other elements serve as conditions.

A trigger is always the result of a change, causing the element to become true. What change, depends on the operator:

  • dx: when lastupdated changed just now;
  • ddx: when localtime reached lastupdated+ value;
  • in: when localtime reached the first time from value;
  • not in: when localtime reached the last time from value;
  • eq: when the attribute changed from another value to value;
  • lt: when the attribute changed from a value >= value to a value < value;
  • gt: when the attribute changed from a value <= value to a value > value;
  • stable: never;
  • not stable: never.

When a trigger occurs, the conditions (i.e. the other elements in the conditions array) are evaluated. Again, the logic depends on the operator:

  • dx: always evaluates to false;
  • ddx: always evaluates to false;
  • in: evaluates to true iff localtime is within the time range in value;
  • not in: evaluates to true iff localtime is outside the range in value;
  • eq: evaluaties to true iff attribute = value;
  • lt: evaluates to true iff attribute < value;
  • gt: evaluates to true iff attribute > value;
  • stable: evaluates to true iff localtime >= lastupdated + value;
  • not stable: evaluates to true iff localtime < lastudated + value.

So dx and ddx only ever serve as trigger - consequently, you should only specify one of these operators in a rule. Also, when a rule contains one of these operators, you don’t need to consider any triggers from other elements, because the dx or ddx element evaluates to false anyways. stable and not stable only serve as conditions - a rule with only elements with these operators is never even evaluated. The other operators might serve as trigger or as condition, but only one at a time.

Note the subtle differences: a rule with only an lt element executes when the attribute value becomes less then value (the becoming less is the trigger). A rule with both a lt and a dx element executes when the attribute is less than value after the change, regardless of the previous value (lastupdated is the trigger). For example: a rule with status lt 2 executes when setting status from 2 to 1, but not when setting it from 0 to 1 or 1 to 0. A rule with status lt 2, lastupdated dx executes when setting status from 2 to 1, from 0 to 1, from 1 to 0, and even from 1 to 1. Likewise, a rule with dark eq true executes when lightlevel changes from above tholddark to below tholddark (becoming dark is the trigger), but not when it changes from one value below tholddark to another value below tholddark.

The conditions of all rules linked to a trigger are to be evaluated before any action is executed. In particular the change of lastupdated that caused a dx or ddx trigger should evaluate to false for the changes caused by rule actions (see the Hue tap toggle example).

Hope this helps - it’s not easy to explain.

On the Hue bridge, rules are triggered when the last condition becomes true, on deCONZ while all conditions are true.

I agree and suggest to adapt the behaviour of the fire then last condition becomes true. The sooner the better to not cause side effects when implementing it too late.

Great examples! The Hue Tap toggle makes it more clear to me.

When the rules are evaluated after event fired, at the beginning a snapshot of current state, of only related resources will be made. Evaluation will be made only with this snapshot. This (should) support rules like the Hue Tap toggle.

To properly support operators lt, gtand similar, the resource item events will extended with previous value before set/change.

Your first use case is equivalent to:

{
    "address": "/sensors/8/state/lastupdated",
    "operator": "dx"
}

Is it? This is may be not the best example, but if I PUT state open twice to a CLIPOpenClose sensor the lastupdated changes while state/open doesn’t.

I think the Hue bridge only allows dx on lastupdated? At least that’s all I’ve ever seen and used.

Hue allows dx on other attributes. A rule I used for ‘arriving home after sunset’ with a CLIPPresence sensor. It is executed when presence becomes true:

conditions": [
                {
                    "address": "/sensors/1/state/daylight",
                    "operator": "eq",
                    "value": "false"
                },
                {
                    "address": "/sensors/8/state/presence",
                    "operator": "dx"
                },
                {
                    "address": "/sensors/8/state/presence",
                    "operator": "eq",
                    "value": "true"
                },
                {
                    "address": "/groups/1/state/any_on",
                    "operator": "eq",
                    "value": "false"
                }
            ]

I love the extensions the deCONZ offers on top of the Hue API (like web sockets), but I would like apps (and scripts and homebridge plugins 😉 using the Hue api to work on deCONZ.

Web sockets are my main reason for switching to deCONZ 👍. I understand your point of view, however the Scene API is also quite different compared to Hue api. I’m also a user of your homebridge plugin. It is not my intention to give you extra work with this proposal. Although, recalling scenes using a HomeKit switch in homebridge-hue would be nice 😉 Would result in one API call, instead of one for every light in a HomeKit scene.

This is tricky, the Hue API doesn’t distinguish between triggers and conditions, unlike e.g. HomeKit. When a condition changes value, it might be a trigger (when the other conditions hold). When it doesn’t change value, it’s just a condition (regardless whether it’s refreshed).

Best give some examples.

On the Hue bridge a rule with

  "conditions": [
    {
      "address": "/sensors/2/state/flag",
      "operator": "eq",
      "value": "true"
    }
  ]

fires only when state.flag changes to true, i.e. when PUTting {"flag": true} while state.flag is false. It does not fire when when PUTting {"flag": true} while state.flag was already true. For that, you’d use:

  "conditions": [
    {
      "address": "/sensors/2/state/flag",
      "operator": "eq",
      "value": "true"
    },
    {
      "address": "/sensors/2/state/lastupdated",
      "operator": "dx"
    }
  ]

which fires when state.lastupdated changes while state.flag is true. state.flag is updated before state.lastupdated, so this rules fires whenever PUTting {"flag": true}, regardless of the (previous) value of state.flag. Same for ddx variants.

In other words: including a condition for state.lastupdated forces any update (“refresh”) of the state to be (considered as) a trigger.

As rule with

  "conditions": [
    {
      "address": "/sensors/1/state/daylight",
      "operator": "eq",
      "value": "false"
    },
    {
      "address": "/sensors/2/state/flag",
      "operator": "eq",
      "value": "true"
    }
  ]

fires:

  1. At sunset (when state.daylight changes to false) while state.flag is true; or
  2. When state.flag changes to true while state.daylight is false, i.e. when PUTting {"flag": true} while state.flag is false and state.daylight is false.

A rule with

  "conditions": [
    {
      "address": "/sensors/1/state/daylight",
      "operator": "eq",
      "value": "false"
    },
    {
      "address": "/sensors/2/state/flag",
      "operator": "eq",
      "value": "true"
    },
    {
      "address": "/sensors/2/state/lastupdated",
      "operator": "dx"
    }
  ]

fires when PUTting {"flag": true} while state.daylight is false, irrespective of the (previous) value of state.flag, but not on sunset.

A rule with

  "conditions": [
    {
      "address": "/config/localtime",
      "operator": "in",
      "value": "T07:00:00/T23:00:00"
    },
    {
      "address": "/sensors/2/state/flag",
      "operator": "eq",
      "value": "true"
    }
  ]

Fires:

  1. At 07.00.00 while state.flag is true; or
  2. When state.flag becomes true between 7:00 and 23:00, i.e. when PUTting {"flag": true} while state.flag is false and config.localtime is between T07:00:00 and T23:00:00.

In other words: only the first time in a range is (considered as) a trigger.

A rule with

  "conditions": [
    {
      "address": "/sensors/3/state/lightlevel",
      "operator": "lt",
      "value": "12000"
    }
  ]

fires when state.lightlevel changes from 13000 to 11000, but also when it changes from 11000 to 10000. A rule with

  "conditions": [
    {
      "address": "/sensors/3/state/dark",
      "operator": "eq",
      "value": "true"
    }
  ]

fires only when state.lightlevel drops below 12000 (assuming that’s the value of config.tholddark).