Send an email that lists the time an item state has changed every 2 hours

Hello,
I have a contact on my entry door that writes to the OH log when it changes state →
2024-08-17 21:42:20.778 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item ‘GF_Entry_Door’ changed from CLOSED to OPEN
I would like to send an email that lists the time that the item state has changed to OPEN every 2 hours.
I already have a rule that sends email every time the item state has changed to OPEN but would like to have a list every 2 hours over getting emails every time it changes.

rule “Main door is opened 01”
when
Item GF_Entry_Door changed from CLOSED to OPEN

then
logInfo(“RULES”, “Main door was opened”)
end

rule “Main door is opened 02”
when
Item GF_Entry_Door changed

then
if(now.getHour() > 16 && now.getHour() < 8) return;
if(GF_Entry_Door.state == OPEN) return;
val mailActions = getActions(“mail”,“mail:smtp:samplesmtp”)
mailActions.sendHtmlMail(“my_email@gmail.com”, “OH Alert - Main door log”, “Entry door was opened”)
logInfo(“RULES”, “eMail of main door status change was sent” )
end

You could keep an array of the times the door changed from closed to open (and open to close to know the open duration), and then every 2 hours, email that list and empty it.

Or you could persist the state changes and query the persistence service every 2 hours, looping through the results. Probably using getAllStatesSince.

Hi, can you share examples how to do that ?

My rulesdsl is rusty, and coding in it is just a pain compared to JRuby which is my preference for rule scripting.

Here’s how I would do it in JRuby:

@state_changes = []

REPORT_PERIOD = "8am".."4pm"

SMTP = "mail:smtp:samplesmtp"
EMAIL = "my_email@gmail.com"

rule "Keep track of door states" do
  changed GF_Entry_Door, from: CLOSED, to: OPEN
  changed GF_Entry_Door, from: OPEN, to: CLOSED
  between REPORT_PERIOD
  run do |event|
    if event.open?
      @state_changes << { opened: ZonedDateTime.now }
      things[SMTP].send_html_mail(EMAIL, "OH Alert - Main door event", "Entry door was opened")
      logger.info "eMail of main door status change was sent"
    else
      @state_changes.last[:closed] = ZonedDateTime.now
    end
  end
end

rule "Report door state changes" do
  every 2.hours
  between REPORT_PERIOD
  run do
    changes = @state_changes.slice!(0..) # move the array into `changes` and empty it

    changes = changes.map do |change|
      opened, closed = change.values_at(:opened, :closed)
      if closed
        duration = closed - opened
        closed = closed.to_time.strftime("%H:%M:%S")
      else
        duration = ZonedDateTime.now - opened
        closed = "still open"
      end
      opened = opened.to_time.strftime("%H:%M:%S")
      "<li>#{opened} - #{closed} (#{duration.to_minutes} minutes)</li>"
    end

    msg = <<~HTML
      <h1>Main door log</h1>

      <ul>
        #{changes.join}
        #{changes.empty? ? "<li>No changes. Current door state: #{GF_Entry_Door.state}</li>" : ""}
      </ul>
    HTML

    things[SMTP].send_html_mail(EMAIL, "OH Alert - Main door log", msg)
  end
end

Untested, so there may be bugs, but I hope you get the general idea of how to implement it.

The variable @state_changes will reset (empty) when you save/reload the script, so once it’s working, don’t touch it :slight_smile:

Another way of doing it is using persistence.

Thanks, i will test it.
In which folder should i put the JRuby rule ?

See: File: USAGE — openHAB JRuby

The JRuby Scripting addon will load scripts from automation/ruby in the user configuration directory

Since you only care about when the door changed to the current state I think using persistence or a separate Item would be less complicated.

Using Items in Rules DSL (assumes the Item doesn’t update except when it changes):

  • create a DateTime Item linked to the same Channel as the door sensor Channel and apply the timestamp profile
rule “Main door is opened 01”
when
    Item GF_Entry_Door changed from CLOSED to OPEN
then
    logInfo(“RULES”, “Main door was opened”)
end

rule “Main door is opened 02”
when
    Time cron "0 0 0/2 ? * * *"
then
    if(now.getHour() > 16 && now.getHour() < 8) return;
    if(GF_Entry_Door.state == OPEN) return;
    val mailActions = getActions(“mail”,“mail:smtp:samplesmtp”)
mailActions.sendHtmlMail(“my_email@gmail.com”, “OH Alert - Main door log”, “Entry door was opened at ” + GF_Entry_Door_Timestamp.state.toString)
    logInfo(“RULES”, “eMail of main door status change was sent” )
end

If the Item does update and you cannot use the timestamp profile you can use your OPEN rule.

rule “Main door is opened 01”
when
    Item GF_Entry_Door changed from CLOSED to OPEN
then
    logInfo(“RULES”, “Main door was opened”)
    GF_Entry_Door_Timestamp.postUpdate(now)
end

You have a few more Items with this approach but much less complexity in the rules.

You could also use the cache which would be slightly less complicated than keeping track of the timestamps manually.

rule “Main door is opened 01”
when
    Item GF_Entry_Door changed from CLOSED to OPEN
then
    logInfo(“RULES”, “Main door was opened”)
    sharedCache.put("GF_Entry_Door_Timestamp", now)
end

rule “Main door is opened 02”
when
    Time cron "0 0 0/2 ? * * *"
then
    if(now.getHour() > 16 && now.getHour() < 8) return;
    if(GF_Entry_Door.state == OPEN) return;
    val mailActions = getActions(“mail”,“mail:smtp:samplesmtp”)
mailActions.sendHtmlMail(“my_email@gmail.com”, “OH Alert - Main door log”, “Entry door was opened at ” + sharedCache.get("GF_Entry_Door_Timestamp"))
    logInfo(“RULES”, “eMail of main door status change was sent” )
end

The persistence approach gets a bit complicated depending on the database used and strategies used to save values. I don’t think we can just get all the entries in the past two hours becuase the door may have opened more than two hours ago.

Thankfully there is now a lastChange persistence extension which should work so long as:

  1. the persistence strategy is such that everyChange is used
  2. the timing is OK (i.e. the door state doesn’t change exactly when the rule runs); lastChange returns null if the current state of the Item is different from the most recent value in the database
rule “Main door is opened 01”
when
    Item GF_Entry_Door changed from CLOSED to OPEN
then
    logInfo(“RULES”, “Main door was opened”)
end

rule “Main door is opened 02”
when
    Time cron "0 0 0/2 ? * * *"
then
    if(now.getHour() > 16 && now.getHour() < 8) return;
    if(GF_Entry_Door.state == OPEN) return;

    var openedAt = GF_Entry_Door.lastChange
    if(openedAt === null) {
        logError("door open", "Timing error")
        return;
    }

    val mailActions = getActions(“mail”,“mail:smtp:samplesmtp”)
mailActions.sendHtmlMail(“my_email@gmail.com”, “OH Alert - Main door log”, “Entry door was opened at ” + openedAt)
    logInfo(“RULES”, “eMail of main door status change was sent” )
end

Having done all that, there is another option if you change your requirements just a little bit. You can install and use Threshold Alert and Open Reminder [4.0.0.0;4.9.9.9] to get an alert every two hours that a door is left open.

  1. Make sure you have JS Scripting installed
  2. Install OHRT; if you are on openHABian this can be done from openhabian-config, if not run npm install openhab_rules_tools from the $OH_CONF/automation/js folder.
  3. Navigate to Add-on Store → Automation → Rule Templates → Threshold Alert and Open Reminder and add the rule tempalte
  4. Add the Door Item to a Group (we’ll call if AllDoors)
  5. Create a rule to send the alert. All the tracking logic is handled by the rule template so all you need to do is send the email. The rule ID will be the name of the .rules file and the number of the rule in that file. For example, if this is the third rule in a file named “foo.rules” the rule ID will be foo-3.
rule "Main door is opened"
when
    // no triggers, this rule will be called from threshold alert
then
    val mailActions = getActions(“mail”,“mail:smtp:samplesmtp”)
mailActions.sendHtmlMail(“my_email@gmail.com”, “OH Alert - Main door log”, “Entry door has been open for a long time”)
logInfo(“RULES”, “eMail of main door status change was sent” )
end
  1. Navigate to Settigns → Rules → +
  2. Enter the rule metadata (name, ID, etc)
  3. Select Threshold Alert under “Templates”
  4. Fill out the properties as follows (if not listed here leave the property at the default):
    a. Triggering Group: AllDoors
    b. Threshold State: OPEN
    c. Alert Delay: PT5M
    d. Reminder Period: PT2H
    e. Alert Rule: Select the rule created above
    f. Do not disturb start time: 16:00
    g. Do not disturb end time: 08:00

The rule that gets created will work as follows:

  • When any member of AllDoors remains in the OPEN state for five minutes, it will call the Alert Rule.
  • If the door closes before the five minutes nothing happens.
  • If the door remians open for two hours after that initial alert, the Alert Rule is called again.
  • If the call to the alert rule would happen between 16:00 and 08:00, the call to the alert rule is delayed until 08:00. Of course, if the door closes between 16:00 and 08:00, no call to the alert rule will occur.

It’s really pretty straight forward. I was overly explicit in the steps above so it looks like a lot more work than it really is.