Multiple Actions In Top Level Rule

UPDATE (SOLVED, MOSTLY)

Yes, you can have multiple actions at the top level and yes they do act the same as several independent rules. HOWEVER…

In my testing, adding an ‘interval’ rule alongside my other rules was the primary cause of my problems. I suspect there is something I do not understand about every or specific, but somehow it appeared to cause some of my other actions to not trigger or trigger inconsistently.

In the end I left the every action in it’s own dedicated rule and combined all the others, this met my needs of reducing the number of rules I was creating for the sake of rate limiting.

If anyone wanders into this and would like some of my final test rules that illustrated the cases and proof of behavior. Comment and I can include.

ORIGINAL QUESTION (left for reference for all the noise below)

Looking for some assistance or advice if someone understands better than me. Here is my situation…

I have a SmartApp written that can be installed multiple times in an environment. Each time it is installed it creates a set of 4 rules. It is designed to be installed ‘per-room’ in my home, so when I roll it out the limitations (Rate Limits and Guardrails | SmartThings Developers) of 50/100 pretty quickly.

So, I want to combine the 4 rules into fewer rules (ideally 1 rule). Since rules can consist of an array of ‘Actions’ at the top level, it had hoped that simply putting my existing 4 rules, which each consisted of a single ‘Action’ into a single rule with 4 actions at the top level, things would just work, but here’s what I’m seeing…

An example rule in my setup may look like this for example (pseudo-code obviously):

{
  "name": "the rule name, whatever",
  "actions": [
    1: if between 8AM and 8PM... turn some lights on to some level (this rule works as expected)
    2. if between 8PM and 12AM... turn some other lights on to some other level (this rule never fires)
    3. at 8PM... turn of lights from rule1 and turn on lights from rule2 (this rule triggers)
    4. if no action for X minutes (no time restrictions), turn off all lights from rules 1 and 2 (this rule fires if in the time-range of rule1)
}

So, basically, the time-range restrictions of the first rule seem to be applying to all rules in the array of actions. If I comment out the first rule, then all rules work within the time-range of the other rule, but still not outside the time ranges. This further supports my theory above.

Does this seem expect to others? Is this to do with the way that all time ranges act as ‘PreConditions’?

Tagging @nayelyz

Hello @lanfear,
Yes, what you mention about the array of actions is correct. Several rules can be added to the mentioned array and all of them are evaluated at the same time, just as if they were different rules. I think the issue you are facing may happen because of this:

A between condition with a time reference (between 8AM and 8PM, for example) never gets triggered by its own. Here there are some examples (pseudo-code):

  1. In this case, whenever motion is detected the rule will evaluate whether this occurred during the desired time range.
"if": {
  "and": [
    { between 8AM and 8PM },
	{ motion detected }
  ]
}
  1. In this case, the rule is triggered any time someone change the switchLevel value.
"if": {
  { switchLevel between 50 and 55 }
}

Whereas a between condition with a time reference should be evaluated every single minute to see if it becomes true and this will constantly consume a lot of unnecessary memory.

In case you decide to use an every action instead of if, take into account that up to 2 level every actions are allowed.

I would suggest that, since there are no features of Scenes in Rules, you evaluate configuring Routines that execute Scenes directly in the app, for this kind of scenario.

2 Likes

I’m surprised they haven’t documented the Scenes execution as it has been round for quite a while. Perhaps they are still considering changing them a bit. I don’t use a lot of Scenes so it isn’t a big deal to me if they change things and Rules need rewriting. Rules do get refined even when Routines have been using them for months. For example, ‘remains’ is now implemented a level up from where it was.

1st, thank you much for taking time to digest and give me info and answers. The info on processing complexity is useful and interesting.

2nd, I apologize if in what I’m asking below you have already directly answered, I have tried to grok your statements and I think I am still seeing something unexpected. If I am wrong, please tell me so, my feelings will not be hurt =)

3rd, apologies for the length, at this point, I need to paste in some concrete examples to allow for explicit confirm/deny.

So… below are 2 examples directly generated by my app using minimal configured options so they are as simple as possible. Surely there is still noise in these I can try to boil down absolute minimum layers for simplicity, but I do not have a harness or code manipulated to allow simpler rule generation or submission, and I am leaning toward hard examples at this point.

I anonymized the device-ids and a couple other specifics, but the rules are otherwise in-tact and submit successfully to the rules API. An easier way to pick them apart is probably an online JSON tool like: JSON Parser Online to parse JSON

Example 1

  1. WORKS: action[0] fires in time-range, action[1] fires all the time when motion ceases. NOTE: this disproved my original theory a little as I see the action not associated with a time-range firing always
{
  "name": "app-<myappidhere>-rule",
  "actions": [
    {
      "if": {
        "and": [
          {
            "between": {
              "value": {
                "time": {
                  "reference": "Now"
                }
              },
              "start": {
                "time": {
                  "reference": "Noon",
                  "offset": {
                    "value": {
                      "integer": -300
                    },
                    "unit": "Minute"
                  }
                }
              },
              "end": {
                "time": {
                  "reference": "Noon",
                  "offset": {
                    "value": {
                      "integer": 360
                    },
                    "unit": "Minute"
                  }
                }
              }
            }
          },
          {
            "or": [
              {
                "equals": {
                  "left": {
                    "device": {
                      "devices": [
                        "<motion-device-id-here>"
                      ],
                      "component": "main",
                      "capability": "motionSensor",
                      "attribute": "motion"
                    }
                  },
                  "right": {
                    "string": "active"
                  }
                }
              }
            ]
          },
          {
            "equals": {
              "left": {
                "device": {
                  "devices": [
                    "<switch-device-id-here>"
                  ],
                  "component": "main",
                  "capability": "switch",
                  "attribute": "switch"
                }
              },
              "right": {
                "string": "off"
              }
            }
          }
        ],
        "then": [
          {
            "command": {
              "devices": [
                "<switch-device-id-here>"
              ],
              "commands": [
                {
                  "component": "main",
                  "capability": "switchLevel",
                  "command": "setLevel",
                  "arguments": [
                    {
                      "integer": 50
                    },
                    {
                      "integer": 20
                    }
                  ]
                }
              ]
            }
          }
        ]
      }
    },
    {
      "if": {
        "then": [
          {
            "command": {
              "devices": [
                "<switch-device-id-here>",
                "<switch-device-id-here>"
              ],
              "commands": [
                {
                  "component": "main",
                  "capability": "switch",
                  "command": "off"
                }
              ]
            }
          }
        ],
        "and": [
          {
            "equals": {
              "left": {
                "device": {
                  "devices": [
                    "<motion-device-id-here>"
                  ],
                  "component": "main",
                  "capability": "motionSensor",
                  "attribute": "motion"
                }
              },
              "right": {
                "string": "inactive"
              }
            }
          }
        ]
      }
    }
  ]
}

EXAMPLE 2

  1. FAILS:
  • action[0] fires in time-range
  • action[1] never fires, regardless of time-range
  • action[2] never fires (has no time range associated with it)
  • action[3] seems to work, harder to test but saw it fire in at least one test
{
  "name": "app-<myappidhere>-rule",
  "actions": [
    {
      "if": {
        "and": [
          {
            "between": {
              "value": {
                "time": {
                  "reference": "Now"
                }
              },
              "start": {
                "time": {
                  "reference": "Noon",
                  "offset": {
                    "value": {
                      "integer": -300
                    },
                    "unit": "Minute"
                  }
                }
              },
              "end": {
                "time": {
                  "reference": "Noon",
                  "offset": {
                    "value": {
                      "integer": 360
                    },
                    "unit": "Minute"
                  }
                }
              }
            }
          },
          {
            "or": [
              {
                "equals": {
                  "left": {
                    "device": {
                      "devices": [
                        "<motion-device-id-here>"
                      ],
                      "component": "main",
                      "capability": "motionSensor",
                      "attribute": "motion"
                    }
                  },
                  "right": {
                    "string": "active"
                  }
                }
              }
            ]
          },
          {
            "equals": {
              "left": {
                "device": {
                  "devices": [
                    "<switch-device-id-here>"
                  ],
                  "component": "main",
                  "capability": "switch",
                  "attribute": "switch"
                }
              },
              "right": {
                "string": "off"
              }
            }
          }
        ],
        "then": [
          {
            "command": {
              "devices": [
                "<switch-device-id-here>"
              ],
              "commands": [
                {
                  "component": "main",
                  "capability": "switchLevel",
                  "command": "setLevel",
                  "arguments": [
                    {
                      "integer": 50
                    },
                    {
                      "integer": 20
                    }
                  ]
                }
              ]
            }
          }
        ]
      }
    },
    {
      "if": {
        "and": [
          {
            "between": {
              "value": {
                "time": {
                  "reference": "Now"
                }
              },
              "start": {
                "time": {
                  "reference": "Noon",
                  "offset": {
                    "value": {
                      "integer": 360
                    },
                    "unit": "Minute"
                  }
                }
              },
              "end": {
                "time": {
                  "reference": "Noon",
                  "offset": {
                    "value": {
                      "integer": -300
                    },
                    "unit": "Minute"
                  }
                }
              }
            }
          },
          {
            "or": [
              {
                "equals": {
                  "left": {
                    "device": {
                      "devices": [
                        "<motion-device-id-here>"
                      ],
                      "component": "main",
                      "capability": "motionSensor",
                      "attribute": "motion"
                    }
                  },
                  "right": {
                    "string": "active"
                  }
                }
              }
            ]
          },
          {
            "equals": {
              "left": {
                "device": {
                  "devices": [
                    "<switch-device-id-here>"
                  ],
                  "component": "main",
                  "capability": "switch",
                  "attribute": "switch"
                }
              },
              "right": {
                "string": "off"
              }
            }
          }
        ],
        "then": [
          {
            "command": {
              "devices": [
                "<switch-device-id-here>"
              ],
              "commands": [
                {
                  "component": "main",
                  "capability": "switchLevel",
                  "command": "setLevel",
                  "arguments": [
                    {
                      "integer": 15
                    },
                    {
                      "integer": 20
                    }
                  ]
                }
              ]
            }
          }
        ]
      }
    },
    {
      "if": {
        "then": [
          {
            "command": {
              "devices": [
                "<switch-device-id-here>",
                "<switch-device-id-here>"
              ],
              "commands": [
                {
                  "component": "main",
                  "capability": "switch",
                  "command": "off"
                }
              ]
            }
          }
        ],
        "and": [
          {
            "equals": {
              "left": {
                "device": {
                  "devices": [
                    "<motion-device-id-here>"
                  ],
                  "component": "main",
                  "capability": "motionSensor",
                  "attribute": "motion"
                }
              },
              "right": {
                "string": "inactive"
              }
            }
          }
        ]
      }
    },
    {
      "every": {
        "specific": {
          "reference": "Noon",
          "offset": {
            "value": {
              "integer": 360
            },
            "unit": "Minute"
          }
        },
        "actions": [
          {
            "if": {
              "or": [
                {
                  "equals": {
                    "left": {
                      "device": {
                        "devices": [
                          "<switch-device-id-here>"
                        ],
                        "component": "main",
                        "capability": "switch",
                        "attribute": "switch"
                      }
                    },
                    "right": {
                      "string": "on"
                    }
                  }
                }
              ],
              "then": [
                {
                  "command": {
                    "devices": [
                      "<switch-device-id-here>"
                    ],
                    "commands": [
                      {
                        "component": "main",
                        "capability": "switch",
                        "command": "off"
                      }
                    ]
                  }
                },
                {
                  "command": {
                    "devices": [
                      "<switch-device-id-here>"
                    ],
                    "commands": [
                      {
                        "component": "main",
                        "capability": "switchLevel",
                        "command": "setLevel",
                        "arguments": [
                          {
                            "integer": 15
                          },
                          {
                            "integer": 20
                          }
                        ]
                      }
                    ]
                  }
                }
              ]
            }
          }
        ]
      }
    }
  ]
}

In summary, rule 1 is intended to be:

- [0]: between 7AM and 6:30PM (iirc), turn on lights with motion, setting dim levels for some
- [1]: _anytime_ turn off lights with no action

In summary, rule 2 is intended to be:

- [0]: between 7AM and 6:30PM, turn on lights with motion, setting dim levels for some (day)
- [1]: between 6:30PM and 7AM, turn on (different) lights with motion, setting dim levels for some (night)
- [2]: _anytime_ turn off lights with no motion
- [3]: at 6:30PM, transition lights from 'day' to 'night'

I don’t think this conflicts with your original assertion. Rules 0,1 are conditioned every minute to confirm in range. Rule 3 is checked ‘every’ minute to see if condition matches’ but rule 2 should not be associated with the other time constraints.

If you think my rules are defining something else just call me out.

Again thank everyone for any time you put into helping me here. In worst case scenario I can probably fall back to creating 2 independent rules, it’s still less than the original 4.

Originally my setup consisted of some 50ish ‘routines’ that worked but were hard to manage in terms of syncing transition from day-night, to day-night time ranges across 3 different routines. But I had it working.

When Lutron ‘upgraded’ to the new SDK framework for their device handler, they screwed something up and began randomly sequencing the dimming events with the ‘on/off’ events. I opened an issue and Samsung replied in a timely manner, blaming the Lutron integration. Lutron never replied to my attempts to open an issue with them and Samsung could not give me updates on the vendor (understandably).

So, after 2 months of having my ‘nightlights’ turn on at 100% and blinding the family, I had to do something. After some quick tests showed the ‘rules’ did not exhibit the same behaviors as the ‘Routines’ (surprisingly)… I ended up here.

It’s possible I can go back to where I started, but the manual work at this point is kinda daunting if it’s even fixed. I have deleted the routines.

I’m instinctively concerned about the time range in [1]. When the rule is evaluated Noon is going to be Noon on that day. Yet you seem to need it to be one day for the start and the next day for the end. To me it looks like the end is before the start. I’m not saying the rule doesn’t assume what you must have meant, webCoRE style, but does it? If it does I actually see that as a bug.

[2] Seems to be garbled. It has an empty if block and then an and in the then. The more I look at it the stranger it looks to me.

Again, speedy and thoughtful response taking time to look at my data, thank you.

You may be correct… Here is what I know…

AFAIK, as a standalone rule this works as it is running all over my house unless it is somehow entirely circumstantial. At first declaring it like this seemed odd to me also, but then I was able to rationalize it as…

The comparison you want to make is relative to the current day. Meaning, when I am calculating is ‘now’ between time X (start) and time Y (end), the instant that the day ticks to the new date, the result of is ‘now’ (midnight, 00:00) < time offset from noon -300 (i.e. 07:00AM), the answer is ‘yes’.

Counter to that, is ‘now’ > the start using that same logic… I suppose ‘no’. I will experiment with tuning that range to be a ‘net positive’ and post results.

I will inspect this much more closely, as an issue here would possibly explain my key issue. i have been hung up on time ranges and may have missed something stupid here. Again, I have evidence of the rule working standalone, but that does not mean it is not flawed in structure and somehow now causing an issue.

Thanks.

As I create Rules via the CLI, I am able to use YAML instead of JSON. It does make things so much easier to read.

I’ve largely stuck to single action Rules so far. Many of my Rules could be three individual if blocks, for example, and be a lot easier to read. However I tend to stick to a single nested block where I can, else I split the Rules up. I can’t remember why, I just remember being a bit suspicious of things. I actually prefer individual Rules if possible, but I am wary that there isn’t any convincing information on Guardrails for Rules. Realistically any overall limit less than 500 would be laughable and 1000 wouldn’t be too high.

I was largely suspicious of the ‘between’ because it didn’t seem to work for you. I know webCoRE will, in certain circumstances, assume end times are later than start times, but that is actually not a good idea if the times are dynamic. For example, you might want something to happen between sunset and 8pm. When sunset falls after 8pm you don’t want it to assume 8pm the next day, you want the something to not happen. So I am hoping Rules doesn’t make the same calls.

I can confirm these are quite real: Rate Limits and Guardrails | Developer Documentation | SmartThings. This is the origin of my current situation where after installing this app that creates 2-4 individual rules about 15 times in my home I started getting a 429 - Too Many Requests back when saving rules.

This confused me at first as it is usually used as a calls/duration throttling, but as documented there, for rules specifically it applies specifically to count. Which is what I saw. Remove a couple and I could add a couple others, but then I would get 429 again.

I re-inspected what I posted here and my original data and am seeing the same thing when put into an online JSON parser. I believe it is valid, albeit maybe confusing. What I am seeing is…

an if rule where the logical ordering of the props then and ‘command’ and are inverted because of the way i construct the object. But ordering of props in a JSON object is irrelevant. There is also only a single action in the and condition since I produced the simplest rule option from my setup for example. (there are options in my app to build out multiple checks at that level). In the simplest case there is only one condition. I hope, an and or or with only a single condition in the array will evaluate to the value of the single condition if there is only one and it is valid.

again, thank you for the dialog, i appreciate your thoughts and am not intending to just deflect blame here.

Ah yes, of course, I had a bit of a brain fart there. I should know better as I’m always saying not to think of Rules like scripts even when they are in a script like order.

Yes, it was the actual numbers I was unsure of. For example, that page states that users can be associated with 50 Locations. That is not the number quoted everywhere else.

Also it seems to be possible to create 200 Routines, which are created as Rules, and that number is inconsistent.

So it is hard to know what to believe.

1 Like

Finally… Progress!

TLDR; it has something to do with my ‘every’, ‘interval’ rule.

I boiled down some of my rules to bare minimums and hard-coded the JSON and did some tests.

  • 2 rules with independent, non-overlapping ‘between’ ranges do work!
  • same 2 rules with an additional ‘whenever motion sensor goes inactive’ that has no ‘between’ ranges also works
  • as soon as i add the ‘every’…‘interval’ rule, things start to not trigger.

i will post further with final details as i work through things now that I have finally learned something. eventually i will update the original post with some focused resolved information.

I posted update with final ‘answer’ of sorts in the original post at the top to cut through some of the noise in here. Thanks for all the help community.