Do expire timers survive a reboot?

Hi,

simple question: Do items like
Switch myTimer "my dummy Item" { expire="5h,command=OFF" } survive a reboot? Let‘s say from the 5 hours, 3 hours and 42 minutes have already passed. Will myTimer still trigger in 1 hour and 18 minutes after the system has restarted?

If not: Any other way to achieve this?

Use case: I want to be informed periodically when I do need to water my plants. Some plants require watering every 12 days. It is highly likely that within this timeframe I need to reboot my raspi and I do not want to loose the information how much time has been passed since last watering.

Cheers,
Oliver

No, expire timers don’t survive reboots/restarts. In my opinion, they’re best for very short periods ranging from milliseconds to minutes.

If you’re only running openHAB, thus should not be the case. OH can go for months without needing to be restarted.

Trigger your rule using a CRON expression, then use an IF condition to determine if the rule should run any actions.

sigh I expected this. The problem with cron is: I do not necessarily water the plant once the timer fires. Maybe I wait 2 days and then I water it because I am lazy and then I want to restart the timer. That is why the Switch would have been perfect. Using cron I loose flexibility.

What could work is to use a Time is <item> [timeOnly] trigger with the item‘s time being dynamically recalculated once the plant was watered. But that is much more work than the convenient expire timer.

Anyway, it is what it is. :wink:

Which brings me to the next question:

Given a DateTime item. How can I store the following value via ruleDSL inside the item: Today + 12 days?
How can I store these 12 days within another item which reflects a plant and retrieve this information inside a rule? I.e. adding the 12 as a meta data and retrieve it to perform the dynamic recalculation?

I know it is not DSL :wink: but in Blockly it is pretty simple:

which is actually

events.sendCommand(‘MyItem’, zdt.now().plusDays(12));

In javascript.

Blockly will deliver even very simple metadata access in 4.0 … (already done but not yet available)

2 Likes

I see. Thank you!
Maybe I should move away from ruleDSL. That looks quite simple. For sure I need to get the meta data access working otherwise I end up with 1 rule per plant and I want 1 rule for all plants. :slight_smile:

That’s actually why I say to use an IF condition. You’re thinking of CRON as “only run the rule every 12 days”. I’m thinking of CRON as “run the rule every day, and check a condition before doing anything”. The flexibility comes from the IF condition.

Off the top of my head, I would create a number item that counts up each time the rule runs, and then sends a notification when it reaches 12 (and every time after that). This would survive a restart with persisence. Then I’d have a switch that I toggle when I’ve watered the plants, which resets the counter to 0.

Not as simple as an expire timer, but only requires an IF/ELSE statement.

Why not use both? You can leave your existing rules in DSL and start new ones in Blockly. Then you can decide later if you want to completely shift over or just leave things as they are. :wink:

1 Like

Why am I always thinking so complicated? That is also a really nice idea! I will give it a try.

I wouldn’t say you’re overcomplicating. Your focus was on “how do I start a rule at the right time”. Expiring an item enabled you to do just that. Anything else really is more complicated.

When you have a particular perspective, it’s very easy to get stuck on other solutions needing to fit into the same perspective. Looking at it from an outside perspective, it was easy for me to shift your logic from “start the rule at the right time” to “run the code inside of the rule at the right time”. And that was only easy because I’ve gone down this exact same route myself. We all do it…and not just with openHAB rules.

1 Like

In Rules DSL:

MyReminderItem.postUpdate(now.plusDays(12));

I’d use your overall cron with if approach but use another rule to populate a DateTime. Something like this (in Rules DSL). First, create a Switch Item for each plant as well as a DateTime Item named the same as the plant Switch with Reminder appended to the name. Put all the Switches in one Group (I’ll call it “Plants”) and put the reminders in another Group (I’ll call it PlantReminders). Command the Switch to ON when you water the plant.

rule "Plant watered"
when
    Member of Plants received command ON
then
    postUpdate(triggeringItemName, now.plusDays.toString()) // the reminder Item can double as a record of the last time the plant was watered
end

rule "Plant water reminder"
when
    Time cron "0 0 0,16 ? * * *"
then
                  //  Get those Items whose date is before today
    val message = PlantReminders.members.filter[ r | 
                      (r.state as DateTimeType).zonedDateTime.toLocalDate().isAfter(now.toLocalDate().plusDays(12))
                  ]
                  // Get the label of the Items to send the reminder
                  .map[ r | r.label]
                  // Build a String with the list of labels to use in a reminder message
                  .reduce[list, label | list + ', ' + label ]
                  // Replace the last comma with "and", Oxford comma be damned!
                 .replace(',$', 'and')
    
    sendBroadcastNotification('It's time to water ' + message + '!');
end

This will send you a list of all the plants (based on the label of the reminder Item) which it has been at least 12 days since they were watered.

Most of @rpwong suggested would be the same with the difference being a loop. through the members of PlantReminders to add one to their states and the filter modified to look for those states >= 12 instead of a date time comparison. But I like the option to know when was the last time the plant was watered.

Once you have access to Item metadata, you can store the number of days in the Item’s metadata and have a different number of days per plant. In the mean time, you could store those in a separate Number Item (continue the naming scheme).

2 Likes

That’s because you actually know what you’re doing. In my case, Current Russ has to limit things to relatively simple IF statements that Future Russ will be able to comprehend two years from now. Past Russ has made that mistake too many times. :wink:

(And yes, this is due in large part to Past Russ not leaving enough comments for Current Russ and Future Russ)

3 Likes

Don’t be too hard on yourself, current Stuart is only just learning the joys of reading recently Past Stuart’s notes.

1 Like

I’m afraid, I do not fully understand your proposal yet. What exactly is happening in line postUpdate(triggeringItemName, now.plusDays.toString()) // the reminder Item can double as a record of the last time the plant was watered ?

Is this supposed to be the line where the reminder item gets updated to the next watering date?
triggeringItemName is not refering to a reminder item I think.

And also: MyReminderItem.postUpdate(now.plusDays(12)) gives me
Type mismatch: cannot convert from ZonedDateTime to Number; line 1, column 31, length 16

Edit: OK, time conversion is not very intuitive in openHAB. After some searching around I would tweak your code as follows:

rule "Plant watered"
when
    Member of Plants received command ON
then
  // Get the referenced reminder of this plant := plantReminder
  // Get the number of days when this plant has to be watered again := daysCount
  plantReminder.postUpdate(DateTimeType.valueOf(now.toLocalDateTime().plusDays(daysCount).toString()))
end
rule "Plant water reminder"
when
    Time cron "0 0 0,16 ? * * *"
then
    //  Get those Items whose date is before today
    val message = PlantReminders.members.filter[ r | 
        (r.state as DateTimeType).zonedDateTime.toLocalDate().isBefore(now.toLocalDate())
    ]
    // Get the label of the Items to send the reminder
    .map[ r | r.label]
    // Build a String with the list of labels to use in a reminder message
    .reduce[list, label | list + ', ' + label ]
    // Replace the last comma with "and", Oxford comma be damned!
   .replace(',$', 'and')
    
    sendBroadcastNotification('It's time to water ' + message + '!');
end

I changed the logic of the filter in the reminder rule to “isBefore” as this seems more intuitive to me. ^^
I think this should work.

By the way: what you are doing after the “]” bracket with the “.” operator looks very convenient!

Please find my final code here. The first smoke test was positive. Let’s see how it behaves in production. :smiley:

import java.net.URLEncoder

rule "Pflanzen: Pflanze gegossen"
when
    Member of gPflanzen received command ON
then
  // Get the referenced reminder of this plant := plantReminder
  val pflanzeErinnerung = gPflanzenErinnerung.members.findFirst[ i | i.name.toString.contains(triggeringItem.name) ] as DateTimeItem  
  val giesIntervallItem = gPflanzenGiessIntervalle.members.findFirst[ i | i.name.toString.contains(triggeringItem.name) ] as NumberItem
  val giesIntervall = giesIntervallItem.state as Number
  
  // Get the number of days when this plant has to be watered again := daysCount
  logInfo("Pflanzen: Pflanze gegossen", triggeringItem.label + " wurde gegossen. Nächstes Gießdatum: " + DateTimeType.valueOf(now.toLocalDateTime().plusDays(giesIntervall.longValue).toString()))
  pflanzeErinnerung.postUpdate(DateTimeType.valueOf(now.toLocalDateTime().plusDays(giesIntervall.longValue).toString()))  
end

rule "Pflanzen: Erinnerung zum Gießen senden"
when
    Time cron "0 0 17 ? * * *"
then
    val giespflanzen = gPflanzenErinnerung.members.filter[ r | 
        (r.state as DateTimeType).zonedDateTime.toLocalDate().isBefore(now.toLocalDate())] // Welche Pflanzen müssen gegossen werden?
    if(giespflanzen.length > 0) {
        giespflanzen.forEach[ pflanzenErinnerung |
            val pflanzenName = pflanzenErinnerung.name.replace("_Erinnerung", "")            
            var pflanze = gPflanzen.members.findFirst[ p | p.toString.contains(pflanzenName)] as SwitchItem
            pflanze.sendCommand(OFF)        
        ]
        val message = giespflanzen
            .map[ r | r.label]
            .reduce[list, label | list + ', ' + label ]
            .replace(',$', 'and')
        var String msg = "🌼💚🌼 Deine Pflanzen benötigen Pflege. Bitte denk' dran heute folgende Pflanze(n) zu gießen: " + message
        logInfo("Pflanzen: Erinnerung zum Gießen senden", msg)
        val urlmessage = URLEncoder::encode(msg, 'UTF-8')
        sendHttpGetRequest("https://api.callmebot.com/whatsapp.php?source=openHAB&phone=<cell-phone-number>&apikey=<apikey>&text="+urlmessage)
    } else {
        logInfo("Pflanzen: Erinnerung zum Gießen senden", "Alle Pflanzen sind versorgt")
    }
end

And an excerpt from the items:

Group:Switch    gPflanzen
Group:Number    gPflanzenGiessIntervalle
Group:DateTime  gPflanzenErinnerung

Switch Pflanze_ZamioculcasDach "Zamioculcas" (gPflanzen, gSchlafzimmer) ["Pflanze", "Dachgeschoss", "Schlafzimmer"]
Number Pflanze_ZamioculcasDach_Intervall (gPflanzenGiessIntervalle)
DateTime Pflanze_ZamioculcasDach_Erinnerung "Zamioculcas Dachgeschoss" (gPflanzenErinnerung)

What I do not like is the heavy use of reflections (again). So I hope one day we can access metadata from ruleDSL. :slight_smile:

Thanks a lot for your support, all!

Yes, that’s a typo. It should have been

postUpdate(triggeringItemName+'_Reminder', now.plusDays.toString())

That’s because MyReminderItem is apparently a Number Item instead of a DateTime Item. My approach requires the reminder Item to be a DateTime.

Also note that My approach has a separate reminder Item per plant. Your code assumes only on reminder Item shared by all plants. I don’t think that will work. Each time you water a plant it will reset that one reminder based on the most recently watered plant, not the longest ago watered plant.

I don’t understand the question. I just broke a single line of code into several so I could add comments. I could break up (r.state as DateTimeType).zonedDateTime.toLocalDate().isBefore(now.toLocalDate()) the same way into

(r.state as DateTimeType)
.zonedDateTime
.toLocalDate()
.isBefore(now.toLocalDate())

Is that what you are referring to? Or are you asking what the mean? Or something else?

I meant the handling of the text so that all plants to be watered are listed in one message. I would have looped it but you implicitly loop over all items at once and create the message out of it.

That is not true. I have a group of reminder items where each of it is assigned to a distinct plant item and I recall it by name.

See Design Pattern: Working with Groups in Rules

map and reduce are common operations supported by many programming languages including Java (Streams API) and Rules DSL (shown above). Unfortunately it’s not directly supported by Blocky yet (I just opened an issue to discuss adding it).

map is a way to create a new list out of some operation on each member of the old list. You can do something like add 5 to each element, perform some complicated operation, or in this case, just extract the label. The result will be a list of just the labels.

reduce is a way to collapse a list into a single value. It could be a sum of the elements, some complicated algorithm, or in this case concatenating the labels into a comma separated String.

They are very handy short hand operations and on many programming languages, these operations are streamed in parallel so that the reduce is already starting to collapse the values even before the filter is done.

But that’s not what your first version of the code above used or did.

rule "Plant watered"
when
    Member of Plants received command ON
then
  // Get the referenced reminder of this plant := plantReminder
  // Get the number of days when this plant has to be watered again := daysCount
  plantReminder.postUpdate(DateTimeType.valueOf(now.toLocalDateTime().plusDays(daysCount).toString()))
end

You only ever post an update to plantReminder no matter what Plant Item triggered the rule. Only that one plantReminder Item gets updated.

In your second version of the rules you do in fact pull the associated Item from gPflanzenErinnerung using findFirst, which isn’t strictly necessary but isn’t a big deal either.

Yeah, you are right.
I posted lots of code up there which might be a little confusing.

I could shrink it a little using the postUpdate(String, String) method:

rule "Pflanzen: Pflanze gegossen"
when
    Member of gPflanzen received command ON
then
  // Gießintervall der gegossenen Pflanze finden
  val giesIntervallItem = gPflanzenGiessIntervalle.members.findFirst[ i | i.name.toString.contains(triggeringItem.name) ] as NumberItem
  val giesIntervall = giesIntervallItem.state as Number
  
  // Erinnerungs-Item dieser Pflanze auf das nächste Gießdatum setzen
  logInfo("Pflanzen: Pflanze gegossen", triggeringItem.label + " wurde gegossen. Nächstes Gießdatum: " + DateTimeType.valueOf(now.toLocalDateTime().plusDays(giesIntervall.longValue).toString()))
  postUpdate(triggeringItem.name + "_Erinnerung", DateTimeType.valueOf(now.toLocalDateTime().plusDays(giesIntervall.longValue).toString()).toString())  
end