Timer within state monitoring rule not functioning as desired

I have a fairly simple rule to monitor my washing machine. I’m not interested closely tracking the state, I just want a notification when the cycle is complete.
I have a power monitoring outlet that I’m using to read the power draw. In standby it draws < 2W, so I simply set the operating threshold to 3W. The rule is as follows:

rule "Set Washing Machine State"
when 
  Item OutletWasherInwall_WasherWatts changed
then 
  if ((WasherStatus.state == "POWEROFF") && (OutletWasherInwall_WasherWatts.state >= 3)) { //
    WasherStatus.sendCommand("WASHING")
  }
  else if ((WasherStatus.state == "WASHING") && (OutletWasherInwall_WasherWatts.state < 3)) { //
    washer_timer = createTimer(now.plusMinutes(1)) [
      if (OutletWasherInwall_WasherWatts.state < 3) { //
        WasherStatus.sendCommand("POWEROFF")
        sendBroadcastNotification("Washing Machine is DONE")
        washer_timer?.cancel()
        washer_timer = null
        }
      else {
        washer_timer?.cancel()
        washer_timer = null        
      }
    ]
  }
end

The 1m timer is intended to prevent an early notification of cycle complete, as the cycle changes between wash, rinse, spin, etc. The problem is, the timer cancel does not seem to function properly. If the power draw changes by even 0.1W and the status is POWEROFF, I will get the “Washing Machine is DONE” notification. In fact, often I will get two simultaneously

I think you need some

(OutletWasherInwall_WasherWatts.state as Number).intValue

Or better at beginning set it as variable.

var washwatt = (OutletWasherInwall_WasherWatts.state as Number).intValue 

Greets

1 Like

Your rule will start one timer for every measurement < 3 as long as the state is WASHING. It will not cancel a running timer (but simply overwrite the pointer to the timer).
So, instead, you’ll have to check whether the timer was already started.

rule "Set Washing Machine State"
when
    Item OutletWasherInwall_WasherWatts changed
then
    if(WasherStatus.state == "POWEROFF" && OutletWasherInwall_WasherWatts.state >= 3) { // start washing
        WasherStatus.postUpdate("WASHING")
    } else if(WasherStatus.state == "WASHING" && OutletWasherInwall_WasherWatts.state < 3 && washer_timer === null) { // maybe washing is ending
        washer_timer = createTimer(now.plusMinutes(1), [ // less than 3 for at least one minute
            WasherStatus.postUpdate("POWEROFF")
            sendBroadcastNotification("Washing Machine is DONE")
            washer_timer?.cancel()
            washer_timer = null
        ])
    } else {
        washer_timer?.cancel
        washer_timer = null
    }
end

Now the timer will not be created multiple times and will only be executed, if the consumption is less 3 for a minute.
If the consumption is over 3, the timer has to be cancelled, as the washin is going on.

It’s unlikely that sendCommand is correct for OutletWasherInwall_WasherWatts (sendCommand() is for sending a command to other rules and/or to Things, postUpdate() is for changing the state of an Item).

2 Likes

It’s funny how a seemingly “simple” problem can be so “tricky”!

I deleted my code above because it isn’t right. I especially realised that after seeing your version. But then it is still not right either.

  • Imagine the state is now WASHING, then WasherWatts is 2, and timer == null, so it would create a timer.
  • Now timer is not null
  • before the timer fired WasherWatts changed to 2.5. so now state == WASHING, timer != null, watts < 3
  • Now it won’t go into if state == WASHING && Watts < 3 && timer == null because… timer is not null now
  • It goes into else clause, timer gets cancelled.
  • If no further events, notification never get sent (but this is unlikely, assuming watts would fluctuate regardless)
  • Another watts change under 3W, it goes back into the second clause
  • A timer created - now not null
  • Another watts change under 3W, the third clause is executed… timer got cancelled again
  • Notification never sent

The notification will only get sent if no events were received after the timer was created.

So here is hopefully a correct version but I won’t be surprised if it’s still not right lol.

EDIT:
So I went and TESTED this too, and it turned out there were more errors, so here’s the hopefully working version

Lessons (mainly for me)

  • You need to declare washer_timer outside the rule otherwise it will be undefined when you tried to access it to test it
  • You need to declare it as Timer (var Timer washer_timer…), otherwise calling cancel will not work because RulesDSL doesn’t know its type.
  • Casting the item state to NumberItem then getting intValue is not actually needed, surprisingly. It works fine with the code below
  • Checking for null needs three equal signs ===
  • Apparently you can call cancel without parentheses
var Timer washer_timer = null

rule "Set Washing Machine State"
when
    Item OutletWasherInwall_WasherWatts changed
then
    if(WasherStatus.state != "WASHING" && OutletWasherInwall_WasherWatts.state >= 3) { // start washing
        WasherStatus.postUpdate("WASHING")
        logInfo("Washer", "Washing Machine is ON")
    } else if(WasherStatus.state == "WASHING" && OutletWasherInwall_WasherWatts.state < 3) {
        if (washer_timer === null) {
            logInfo("Washer", "Starting a timer")
            washer_timer = createTimer(now.plusMinutes(1), [ | // less than 3 for at least one minute
                logInfo("Washer", "Washing Machine is DONE")
                washer_timer = null
                WasherStatus.postUpdate("POWEROFF")
                sendBroadcastNotification("Washing Machine is DONE")
            ])
        }
    } else {
        logInfo("Washer", "Stopping a timer")
        washer_timer?.cancel
        washer_timer = null
    }
end
2 Likes

Here’s a Ruby version. Not much better

rule "Set Washing Machine State" do
  changed OutletWasherInwall_WasherWatts
  run do |event|
    logger.info "OutletWasherInwall_WasherWatts changed to #{event.state}"
    if event.state >= 3
      logger.info("WasherStatus changed to WASHING") if WasherStatus.ensure.update("WASHING")
      logger.info("Timer cancelled") if timers.cancel(WasherStatus)
    elsif WasherStatus.state == "WASHING" && !timers.include?(WasherStatus)
      logger.info("Timer started")
      after 1.minute, id: WasherStatus do
        logger.info("WasherStatus changed to IDLE")
        WasherStatus.update("IDLE")
      end
    end
  end
end

Explanation:

  • Ruby has a global timers object that keeps track of your timers. You can check the existence of an active timer with timers.include?(timer_id), you can cancel a timer using timers.cancel(timer_id).
  • after creates a timer. You can give it an id so you can keep track of it.
  • The id for after can be an actual object, e.g. your item name. It can also be a string if you prefer
  • You can also get the return value of after and manipulate the timer as usual. The return value is the exact same openHAB Timer object you have in RulesDSL. So you can call all the same methods that an openHAB Timer object has. But using the timer manager as demonstrated here is probably easier.
  • Item.ensure.update will only post an update if the item isn’t already in the same state. If it’s already in the same state, it returns nil. This way you can act on the return value, e.g.
    logger.info("It wasn't X, but now it's updated to X") if ItemName.ensure.update("X")
    
  • Ruby has a “modifier if” syntax where you put the if at the end of the statement you want to execute. Example: a = 5 if a > 5 this only sets a = 5 if a is currently > 5.

To learn more about Ruby: File: Ruby Basics — openHAB JRuby

1 Like

Yes, you’re absolutely right. I should not post code in a hurry… :slight_smile:

This is why my Threashold Alert rule template is so complicated. But it can be used for this case. Once installed a rule can be instantiated with the following settings. Any property not listed should remain the default.

  1. create a Group and put OutletWasherInwall_WasherWatts into a Group or you can manually edit the trigger of the rule that will be created to only trigger on this Item instead of member of group.
  2. Create a rule to be called. Unfortunately Rules DSL doesn’t support being called from another rule with arguments so it must be any other language. In JS this rule’s action would look something like the following. If you create a file based rule, there are no triggers.
var currStatus = items.WasherStatus.state;
if(!isAlerting && currStatus  == "POWEROFF") {
  WasherStatus.postUpdate("WASHING");
}
else if(isAlerting && currStatus == "WASHING") {
  actions.NotificationAction.sendBroadcastNotification("Washing Machine is DONE");
}
  1. Create a new rule in the UI and choose Threshold Alert as the template.
    a. alertRule: select the rule created in 2
    b. endAlertRule: select the rule created in 2
    c. thresholdState: 3
    d. operator: <
    e. defaultAlertDelay: PT1M
    f. group: Group created in 1

The rule created from the template will call the rule you created in step 2 when the reading of OutletWasherInWall_Washer_Watts remains below 3 for one minute with isAlerting set to true. When the value goes to or above 3 the rule gets called with isAlerting set to false and it will not call the rule again until it goes below 3 for one minute.

Or use the cache, though that wasn’t working in Rules DSL until very recently. I don’t know if that got backported to 4 or if it’s only available in 5.

One case of Rules DSL broken typing system.

Another example of Rules DSL’s broken typing system.

That’s a Rules DSL convention. If you want to call a function that doesn’t take any arguments, the parens are optional. If the function starts with “get” you can omit that too. So MyDateTimeType.getZonedDateTime() can be rendered MyDateTypeType.zonedDateTime.

If one didn’t want to use a rule template and code this themselves, in JS OHRT has a number of classes to help manage timers.

In a JS file using rule builder the rule would look something like:

var {TimerMgr} = require('openhab_rules_tools');

rules.when().item("OutletWasherInwall_WasherWatts").changed()
     .then( (event) => {
       const tMgr = cache.private.get("timerMgr", () => TimerMgr());
       const status = items.WasherStatus;
       const watts = parseFloat(events.newState); // I don't know if events.newState has a numeric version

       if(status.state != "WASHING" && watts >= 3) {
         status.postUpdate("WASHING");
         console.info("Washing Machine is ON");
       }
       else if(status.state == "WASHING" && watts < 3) {
         tMgr.check("mytimer",
                    () => {  
                      logInfo("Washer", "Washing Machine is DONE");
                      status.postUpdate("POWEROFF"),
                      sendBroadcastNotification("Washing Machine is DONE");
                    },
                    "PT1M", false, null, "Washing Machine Timer");
       }
       else {
         console.info("Stopping washing machine timer");
         tMgr.cancel("mytimer");
       }
     }).build("Set Washing Machine State", "Send an alert when the washing machine stops", [], "washingMachineAlert");

Or just using a straight timer in the cache since TimerMgr doesn’t do a whole lot for you in cases where there’s only one timer created by the rule instead of, for example, one timer per Item that can trigger the rule with a member of trigger):

rules.when().item("OutletWasherInwall_WasherWatts").changed()
     .then( (event) => {
       const status = items.WasherStatus;
       const watts = parseFloat(events.newState); // I don't know if events.newState has a numeric version

       if(status.state != "WASHING" && watts >= 3) {
         status.postUpdate("WASHING");
         console.info("Washing Machine is ON");
       }
       else if(status.state == "WASHING" && watts < 3 && !cache.private.exists("timer")) {
         cache.private.put("timer", actions.ScriptExecution.createTimer(time.toZDT("PT1M"), () => {
           logInfo("Washer", "Washing Machine is DONE");
           status.postUpdate("POWEROFF"),
           sendBroadcastNotification("Washing Machine is DONE");
           cache.private.remove("timer");
         });
       }
       else {
         console.info("Stopping washing machine timer");
         cache.private.get("timer")?.cancel();
         cache.private.remove("timer");
       }
     }).build("Set Washing Machine State", "Send an alert when the washing machine stops", [], "washingMachineAlert");

In Blockly it could look something like this:


Obviously "MyItem needs to be replaced with your actual Item name.

1 Like

This has been the most substantial and detailed reply I’ve ever had from the OpenHAB community to a question I’ve posted. For one, it’s a testament to the incredible community we have. But I also get the best responses from timer questions! I need to ponder the “why” of that…
For the time being, I have the code from @jimtng in place and it seems to be working (I will watch it closely for a few days). Thank you as well for informing me about Ruby; I didn’t know anything about it and I now have a new rabbit hole to explore! I also learn from anything that @rlkoshak includes. @Udo_Hartmann, I believe yours would work as well and elements of your code will be repurposed into other rules. All of this is going into my personal notes. Again, thank you all!

2 Likes

If you have any questions about Ruby / JRuby in relation to openhab, post it and tag me. I’ll be happy to help. You can start from here: