Suggestions for improved user experience with timers

That seems to be a pretty subjective feeling. Rules are there to implement behaviors. What constitutes “interesting” is completely subjective. But since the beginning, rule have been and always were intended to implement behaviors, no matter how complex or simple they may be. When event X occurs and all you want to do is send a command to Item Y, a rule is what you’d use even though it’s just one command.

In my mind it doesn’t make OH simpler or easier to use to create alternative ways to do things like this. All it really does is just move the problem. Users now have to figure out whether or not their problem becomes interesting enough that this new way to do timers will not work or not. And if they need to grow their system later they may have to abandon their new timers for rules anyway.

I completely agree that a transformation is absolutely not the place for any sort of timers.

It sounds like you probably have lots of room for consolidation. There are tons of approaches that could be used but without seeing the rules I couldn’t recommend one. If using JS or Python for the rules language I’d probably set some custom Item metadata on the Item with the delay and the ID of the rule to call and have just 1 rule to implement the delay.

In fact, I’ve already written such a rule that might work. See https://github.com/rkoshak/openhab-rules-tools/tree/main/debounce. Though that will require proxy Items. NOTE: when OH 3.2 is released rules like this will be findable and installable just like add-ons.

But you still have to implement the “thing to do after the delay” which will still require “interesting” code which will require a rule. So all you’ve really done is eliminate one line of code from each of your rules and replaced it with a brand new concept for implementing timers. I’m not yet convinced that’s a fair trade. (I could be convinced. I’m not completely against the idea, just skeptical based on my years of experiencing helping users on this forum.)

Which is one way to define a rule. You can also have rules defined in text files. Any new concept for timers (with one exception) would have to work with both.

In UI rules there are several different types of Actions one can choose from, one of which is a Script Action. The same goes for Conditions where one can choose a Script Condition among several different types of conditions.

But what you call a “script” is part of a rule. It doesn’t exist independently. You can have a rule without a “script” but you cannot have a “script” without a rule. If you are parsing “rules” then you are by definition also parsing scripts unless you say “rules except scripts”. The stuff you see under “Scripts” in MainUI are a special type of rule that consists of only a single Script Action with the tag “Script” applied. It’s still a rule though.

And if you are ignoring Script Actions and Script Conditions then you are missing a huge area where Items are referenced and used. I don’t think you can just ignore them and have a tool that is complete. And as I said there will still be cases where you can’t identify when I Item is used by a given rule, which is probably OK. This is a case where good enough is good enough.

Finally, that syntax/structure you show only exists in MainUI. The actual syntax and structure used by rules varies based on the language and how it’s defined. But for this conversation we can probably focus on the JSON returned by the REST API as all the other text based ways to define a rule can be acquired in this form.

For example:

{
  "status": {
    "status": "IDLE",
    "statusDetail": "NONE"
  },
  "editable": true,
  "triggers": [
    {
      "id": "1",
      "configuration": {
        "groupName": "Debounce"
      },
      "type": "core.GroupStateChangeTrigger"
    }
  ],
  "conditions": [
    {
      "inputs": {},
      "id": "3",
      "configuration": {
        "type": "application/javascript",
        "script": "event.itemState.class != UnDefType.class"
      },
      "type": "script.ScriptCondition"
    }
  ],
  "actions": [
    {
      "inputs": {},
      "id": "2",
      "configuration": {
        "type": "application/javascript",
        "script": "var logger = Java.type(\"org.slf4j.LoggerFactory\").getLogger(\"org.openhab.model.script.Rules.Debounce\");\n\n// Get Metadata query stuff\nthis.FrameworkUtil = (this.FrameworkUtil === undefined) ? Java.type(\"org.osgi.framework.FrameworkUtil\") : this.FrameworkUtil;\nthis._bundle = (this._bundle === undefined) ? FrameworkUtil.getBundle(scriptExtension.class) : this._bundle;\nthis.bundle_context = (this.bundle_context === undefined) ? this._bundle.getBundleContext() : this.bundle_context;\nthis.MetadataRegistry_Ref = (this.MetadataRegistry_Ref === undefined) ? bundle_context.getServiceReference(\"org.openhab.core.items.MetadataRegistry\") : this.MetadataRegistry_Ref;\nthis.MetadataRegistry = (this.MetadataRegistry === undefined) ? bundle_context.getService(MetadataRegistry_Ref) : this.MetadataRegistry;\nthis.Metadata = (this.Metadata === undefined) ? Java.type(\"org.openhab.core.items.Metadata\") : this.Metadata;\nthis.MetadataKey = (this.MetadataKey === undefined) ? Java.type(\"org.openhab.core.items.MetadataKey\") : this.MetadataKey;\n\n// Load TimerMgr\nthis.OPENHAB_CONF = (this.OPENHAB_CONF === undefined) ? java.lang.System.getenv(\"OPENHAB_CONF\") : this.OPENHAB_CONF;\nload(this.OPENHAB_CONF+'/automation/lib/javascript/community/timerMgr.js');\n\n/**\n * Get and check the item metadata.\n * @return {dict} The metadata parsed and validated\n */\nvar checkMetadata = function(itemName) {\n  var USAGE = \"Debounce metadata should follow debounce=ProxyItem[command=true, timeout='2s', state='ON,OFF'].\"\n  var cfg = MetadataRegistry.get(new MetadataKey(\"debounce\", itemName));\n  if(cfg === null) {\n    throw itemName + \" does not have debounce metadata! \" + USAGE;\n  }\n  \n  if(cfg.value === undefined || cfg.value === null) {\n    throw itemName + \" does not have a proxy Item defined! \" + USAGE;\n  }\n  if(items[cfg.value === undefined]) {\n    throw \"Proxy Item \" + cfg.value + \" does not exist! \" + USAGE;\n  }\n  if(cfg.configuration[\"timeout\"] == undefined || cfg.configuration[\"timeout\"] === null) {\n    throw itemName + \" does not have a timeout parameter defined! \" + USAGE;\n  }\n  var dict = {\"proxy\": cfg.value,\n              \"timeout\": cfg.configuration[\"timeout\"],\n              \"command\": \"command\" in cfg.configuration && cfg.configuration[\"command\"].toLowerCase() == \"true\",\n              };\n              \n  var stateStr = cfg.configuration[\"state\"];\n  if(stateStr === undefined || stateStr === null) {\n    throw itemName + \" does not have proper debounce metadata \" + cfg.toString();\n  }\n  var split = stateStr.split(\",\");\n  dict[\"states\"] = [];\n  for(st in split) {\n    dict[\"states\"].push(split[st]);\n  }\n  return dict;\n}\n\n/**\n * Called when the debounce timer expires, transfers the current state to the \n * proxy Item.\n * @param {string} state the state to transfer to the proxy Item\n * @param {string} name of the proxy Item\n * @param {Boolean} when true, the state is sent as a command\n */\nvar end_debounce_generator = function(state, proxy, isCommand) {\n    return function() {\n        logger.debug(\"End debounce for \" + proxy + \", new state = \" + state + \", curr state = \" + items[proxy] + \", command = \" + isCommand);\n        if(isCommand && items[proxy] != state) {\n          logger.debug(\"Sending command \" + state + \" to \" + proxy);\n          events.sendCommand(proxy, state);\n        }\n        else if (items[proxy] != state) {\n          logger.debug(\"Posting update \" + state + \" to \" + proxy);\n          events.postUpdate(proxy, state);\n        }\n      }\n}\n\nthis.timers = (this.timers === undefined) ? new TimerMgr() : this.timers;\nvar cfg = checkMetadata(event.itemName);\n\nif(cfg[\"states\"].length == 0 || \n  (cfg[\"states\"].length > 0 && cfg[\"states\"].indexOf(event.itemState.toString()) >= 0)) {\n  logger.debug(\"Debouncing \" + event.itemName + \" with proxy = \" + cfg[\"proxy\"] \n               + \" timeout = \" + cfg[\"timeout\"] + \" and states = \" + cfg[\"states\"]);\n  this.timers.check(event.itemName, cfg[\"timeout\"], \n                    end_debounce_generator(event.itemState, cfg[\"proxy\"], cfg[\"command\"]));    \n}\nelse {\n  logger.debug(event.itemName + \" changed to \" + event.itemState + \" which is not debouncing\");\n  end_debounce_generator(event.itemState, cfg[\"proxy\"], cfg[\"command\"])();\n}\n"
      },
      "type": "script.ScriptAction"
    }
  ],
  "configuration": {},
  "configDescriptions": [],
  "uid": "debounce",
  "name": "Debounce",
  "tags": [
    "rules_tools"
  ],
  "visibility": "VISIBLE",
  "description": "Waits a configured time before passing an Item's state to a proxy"
}

That’s the JSON representation of that Debounce rule I linked to above.

Here is what a “Script” looks like:

{
  "status": {
    "status": "IDLE",
    "statusDetail": "NONE"
  },
  "editable": true,
  "triggers": [],
  "conditions": [],
  "actions": [
    {
      "inputs": {},
      "id": "script",
      "configuration": {
        "type": "application/javascript",
        "script": "var logger = Java.type(\"org.slf4j.LoggerFactory\").getLogger(\"org.openhab.model.script.Rules.Expamples\");\nlogger.info(\"About to test createTimer\");\nvar ScriptExecution = Java.type(\"org.openhab.core.model.script.actions.ScriptExecution\");\nvar runme = function(){ logger.info(\"Timer expired!\"); }\nvar ZonedDateTime = Java.type(\"java.time.ZonedDateTime\");\nvar now = ZonedDateTime.now();\nvar timer = ScriptExecution.createTimer(now.plusSeconds(1), runme);\n"
      },
      "type": "script.ScriptAction"
    }
  ],
  "configuration": {},
  "configDescriptions": [],
  "uid": "timerexample",
  "name": "createTimer example",
  "tags": [
    "Script"
  ],
  "visibility": "VISIBLE",
  "description": "Shows how to create a timer"
}

Notice it’s the identical structure. It just doesn’t have triggers or conditions, the single script action and the tag “Script”.

While it’s not yet a tool built into OH, there is a script that does a lot of the work of parsing through the OH entities to figure out where they are used. See [script] find items or other objects in OH environment

It would be indeed awesome if that were turned into a Developer Tool in MainUI.

When/if you ever file an issue, separate the two. It’s confusing and not really appropriate to try to deal with both at the same time in the same issue.

And I still stand by the idea that adding basically the equivalent to Thread.sleep as a separate UI Action is a good idea. So focus on that. But note that this must be very simple. All that such a UI action should do is block execution of the rule for the defined amount of time. Then you can do some simple time sequencing in a UI rule without needing to resort to “code”. This action doesn’t need to know or care about any context. All it needs to do is block execution for the configured amount of time. With that you can select it as one of the actions from the UI.

Implementing something like this will required changes to OH core as well as MainUI.

There is already an issue open for a Debounce profile which is basically what we are talking about implementing. [profiles] Proposing debouncing profile · Issue #2172 · openhab/openhab-core · GitHub

I think both a Delay Action one can choose from in the UI Rules and a Debounce profile (or what ever you want to call it, but there is already an issue for Debounce) are good ideas.

The finding which Items are used where and such are also good ideas but need to be treated in a separate issue.