My Garadget + MQTT + OpenHAB Setup

Tags: #<Tag:0x00007fc3fdd4d000> #<Tag:0x00007fc3fdd4cf38>

After receiving my working Garadget (a smart garage opener), I’ve managed to set it to MQTT mode (no cloud), and named mine “garage”.

If you noticed any bugs or would suggest an improvement / more efficient way of doing things, I’d love to hear it.

UPDATE 2019-08-27: I’ve just upgraded to 2.5M2 and the mqtt bugs affecting this implementation in 2.5M1 have been fixed. I have therefore updated the things/rule to a much simpler version.

Requirements:

  • Garadget with MQTT feature Enabled
  • An mqtt broker already set up and Garadget pointed to it
  • OpenHAB 2.5M2 (my set up won’t work with earlier versions)
  • OpenHAB MQTT binding
  • OpenHAB JSON Transformation, MAP Transformation
  • This may be obvious to most here, but for a beginner, I’ll add this information: I have an MQTT bridge Thing created in a separate .things file, and my mqtt bridge is called “mosquitto”, hence “mqtt:broker:mosquitto”. Adjust accordingly to match your mqtt bridge name.
    FYI, this is my mqtt.things file containing my bridge definition:
Bridge mqtt:broker:mosquitto [ host="x.x.x.x", secure="false" ]

Notes:

  • I have my openhab linked with Google Home via myopenhab.org so I can open/close using voice command

garadget.things file:

Thing mqtt:topic:mosquitto:garage "Garage Door" (mqtt:broker:mosquitto) @ "Garage" {
    Channels:
        Type rollershutter : control "Control"		[ 
            stateTopic="garadget/garage/status", 
            transformationPattern="JSONPATH:$.status∩MAP:garadget.map", 
            commandTopic="garadget/garage/command",
            transformationPatternOut="MAP:garadget.map"
            ]
        Type string : time "Time"                   [ stateTopic="garadget/garage/status", transformationPattern="JSONPATH:$.time" ]
        Type string : status "Status"				[ stateTopic="garadget/garage/status", transformationPattern="JSONPATH:$.status" ]
        Type number : reflection "Reflection"		[ stateTopic="garadget/garage/status", transformationPattern="JSONPATH:$.sensor" ]
        Type number : rssi "Wifi Signal Strength"	[ stateTopic="garadget/garage/status", transformationPattern="JSONPATH:$.signal" ]
        Type number : brightness "Light Brightness"	[ stateTopic="garadget/garage/status", transformationPattern="JSONPATH:$.bright" ]
        Type number : mtt "Door Moving Time"        [ stateTopic="garadget/garage/config", transformationPattern="JSONPATH:$.mtt" ]
}

garadget.items

Rollershutter Garage_Door "Garage Door"  <rollershutter> (UI) ["Blinds"] { autoupdate="false", channel="mqtt:topic:mosquitto:garage:control" }

String Garage_Time "Time" { channel="mqtt:topic:mosquitto:garage:time" }
String Garage_Status   "Status"    <garagedoor>  (UI) { channel="mqtt:topic:mosquitto:garage:status" }
Number Garage_RSSI "RSSI [%d dBm]" <wifi>  { channel="mqtt:topic:mosquitto:garage:rssi" }
Number Garage_Reflection "Reflection Level [%d]" {channel="mqtt:topic:mosquitto:garage:reflection" }
Number Garage_Brightness "Brightness Level [%d]" { channel="mqtt:topic:mosquitto:garage:brightness" }
Number Garage_MovingTime "Door Moving Time [%d ms]" { channel="mqtt:topic:mosquitto:garage:mtt" }

garadget.map

// incoming transformations
closed=100
close=100
closing=50
open=0
stopped=stop
stop=STOP
opening=50

// outgoing transformation
0=open
100=close

STOP=stop

garadget.rules

// A rule file is no longer needed
2 Likes

Does garadget not reliably report status or does it not use retained messages for its status? If it does, why do you need to poll for the status every minute? If you do have to poll the MQTT status then garaget’s MQTT implementation is broken. The whole point is for the device to publish it’s state changes rather than your needing to poll for them.

If garaget is publishing state changes when they occur but isn’t using retained messages, then you can change your polling Rule to run at System started. If it is using retained messages, then you don’t need that Rule at all. OH will get the last state as soon as the MQTT binding comes up.

You could simplify your Rules if you create another .map file. You can then replace the switch statements with val command = transform("MAP", "control.map", receivedCommand.toString) (for the first Rule as an example.

Thanks for posting! It’s a good tutorial!

EDIT: Corrected my missreading of the cron expression, thanks for pointing out the error @opus! :slight_smile:

1 Like

IMHO the poll is triggered every minute (at 0 seconds). All other statement I do agree with.

You are correct. I saw the 0 as a *.

Thanks for your feedback.

You could simplify your Rules if you create another .map file. You can then replace the switch statements with val command = transform("MAP", "control.map", receivedCommand.toString) (for the first Rule as an example.

Great idea, and thanks for pointing out a way that I didn’t know before. However in this case, the trade off would be having to maintain yet another file (the map file) vs longer code. I personally prefer the in-code processing. However, I’ll keep this in mind, it might come in handy in the future.

Garadget does publish its status as a retained MQTT message. However, I thought that Garadget won’t update the status when the door was opened via other means (e.g. RF remote) but after reading your comment, I tested this and found that it indeed updated the status. So for the purpose of detecting an open garage, the cron rule is not necessary. On the other hand, to monitor changes in the light level, which is included in the status message, this is, AFAIK, the only way to “poll” it.

Another reason for polling the status, is to get an updated “garage time” (how long it has remained in the current state). Although this can be calculated independently perhaps.

So for those who don’t need to know the light level or the “garage time”, the cron rule can be removed.

1 Like

After much frustration with trying to make MQTT work with Garadget, I will just patiently wait for 2.5M2.

It would be great to have a full ‘ui-only’ binding for Garadget, but I would be happy to just get rollershutter and MQTT working reliably.

Most of the bugs that have been fixed in the MQTT binding were fixed before 2.5 M1. There are very few new bugs that have or will be fixed for 2.5 M2. So don’t wait. Upgrade to 2.5 M1 now.

My main problem with 2.5M1 is the mqtt actions don’t work reliably in code / rules. This is why I had to resort to creating a dummy item in order to publish mqtt messages.

I have a Garadget in a box waiting for me to set up the same stuff. Your configuration will save me a bunch of time! Thanks for sharing it!

I hope the MQTT implementation is not as worrying as it sounded.

Thanks. I am warming up to to idea of just using the particle cloud (which is what garadget uses) rather than trying to do local via MQTT. I did upgrade and the binding in 2.5 M1 works great when using the cloud. Given the widespread use of particle, I am a bit less worried about connectivity issues. I am especially concerned because security-wise, it is my garage door!

I wanted to share some observations about the Garadget Binding and help others who use this device.

The items file example described in the binding https://www.openhab.org/addons/bindings/garadget1/ is a great starting point.

Since I have two doors instead of one, I used that to get it to work for two doors:

garadget.items

Group Garadget
Group UI

String door1               "Garage Door [%s]"              <rollershutter> (Garadget,UI) { garadget="<[330028001447373033383530#door1]" }

// A Contact item supports open and closed, but a Garadget doorStatus_status can be: 
// closed, open, closing, opening, stopped
// (as documented here: https://github.com/Garadget/firmware#door-states-status)
String doorStatus_status_door1  "Door Status [%s]"                      <garagedoor> (Garadget,doorStatus,UI) { garadget="<[330028001447373033383530#doorStatus_status]" }
String doorStatus_time_door1    "Last Change [%s ago]"                  <clock> (Garadget,doorStatus,UI) { garadget="<[330028001447373033383530#doorStatus_time]" }
// Send the doorState item commands like ZERO, HUNDRED, UP, DOWN, ON, OFF, STOP, or "open", "closed" or "stop".
Rollershutter doorState_door1   "Control"                       <rollershutter> (Garadget,UI) { garadget=">[330028001447373033383530#setState],<[330028001447373033383530#doorStatus_status]" }
Number doorStatus_sensor_door1  "Reflection [%d %%]"                      <sun> (Garadget,doorStatus,UI) { garadget="<[330028001447373033383530#doorStatus_sensor]" }
Number doorConfig_srt_door1     "Threshold [%d %%]"                   <battery> (Garadget,doorStatus,UI) { garadget="<[330028001447373033383530#doorConfig_srt]" }
Number doorStatus_signal_door1  "Signal [%d dB]"                      <battery> (Garadget,doorStatus,UI) { garadget="<[330028001447373033383530#doorStatus_signal]" }
String last_app_door1           "Last App [%s]"                                 (Garadget) { garadget="<[330028001447373033383530#last_app]" }
String last_ip_address_door1    "Last IP Address [%s]"                          (Garadget) { garadget="<[330028001447373033383530#last_ip_address]" }
DateTime last_heard_door1       "Last Heard [%1$tm/%1$td %1$tH:%1$tM]"  <clock> (Garadget) { garadget="<[330028001447373033383530#last_heard]" }
Number product_id_door1         "Product ID [%d]"                               (Garadget) { garadget="<[330028001447373033383530#product_id]" }
Switch connected_door1          "Connected [%s]"                                (Garadget) { garadget="<[330028001447373033383530#connected]" }

String doorStatus_door1         "Door Status [%s]"                              (Garadget) { garadget="<[330028001447373033383530#doorStatus]" }

String doorConfig_door1         "Door Config [%s]"                              (Garadget) { garadget="<[330028001447373033383530#doorConfig]" }
Number doorConfig_ver_door1     "Version [%.1f]"                                (Garadget) { garadget="<[330028001447373033383530#doorConfig_ver]" }
Number doorConfig_rdt_door1     "Sensor Scan Interval [%d ms]"                  (Garadget) { garadget="<[330028001447373033383530#doorConfig_rdt]" }
Number doorConfig_mtt_door1     "Door Moving Time [%d ms]"                      (Garadget) { garadget="<[330028001447373033383530#doorConfig_mtt]" }
Number doorConfig_rlt_door1     "Button Press Time [%d ms]"                     (Garadget) { garadget="<[330028001447373033383530#doorConfig_rlt]" }
Number doorConfig_rlp_door1     "Delay betw Presses [%d ms]"                    (Garadget) { garadget="<[330028001447373033383530#doorConfig_rlp]" }
Number doorConfig_srr_door1     "Sensor reads [%d]"                             (Garadget) { garadget="<[330028001447373033383530#doorConfig_srr]" }
Number doorConfig_aot_door1     "Open Timeout Alert [%d min]"                   (Garadget) { garadget="<[330028001447373033383530#doorConfig_aot]" }
Number doorConfig_ans_door1     "Night time alert start [%d min from midnight]" (Garadget) { garadget="<[330028001447373033383530#doorConfig_ans]" }
Number doorConfig_ane_door1     "Night time alert end [%d min from midnight]"   (Garadget) { garadget="<[330028001447373033383530#doorConfig_ane]" }

String netConfig_door1          "Net Config [%s]"                               (Garadget) { garadget="<[330028001447373033383530#netConfig]" }
String netConfig_ip_door1       "IP Address [%s]"                               (Garadget) { garadget="<[330028001447373033383530#netConfig_ip]" }
String netConfig_snet_door1     "Subnet [%s]"                                   (Garadget) { garadget="<[330028001447373033383530#netConfig_snet]" }
String netConfig_gway_door1     "Gateway [%s]"                                  (Garadget) { garadget="<[330028001447373033383530#netConfig_gway]" }
String netConfig_mac_door1      "MAC address [%s]"                              (Garadget) { garadget="<[330028001447373033383530#netConfig_mac]" }
String netConfig_ssid_door1     "SSID [%s]"                                     (Garadget) { garadget="<[330028001447373033383530#netConfig_ssid]" }

// send the setConfig item commands like "ans=1200|ane=420" to set night time alert to 8pm-7am, for example.
String setConfig_door1          "Garage Door 1 Config [%s]"              (Garadget) { garadget=">[330028001447373033383530#setConfig],<[330028001447373033383530#doorConfig]" }

Switch doorStatus_status_door1_timer_10 (doorStatusTimers){expire="10m,command=OFF"}
Switch doorStatus_status_door1_timer_30 (doorStatusTimers){expire="20m,command=OFF"}

String door2               "Garage Door [%s]"              <rollershutter> (Garadget,doorStatus,UI) { garadget="<[2b0027001447373033383530#door2]" }

// A Contact item supports open and closed, but a Garadget doorStatus_status can be: 
// closed, open, closing, opening, stopped
// (as documented here: https://github.com/Garadget/firmware#door-states-status)
String doorStatus_status_door2  "Door Status [%s]"                      <garagedoor> (Garadget,doorStatus,UI) { garadget="<[2b0027001447373033383530#doorStatus_status]" }
String doorStatus_time_door2    "Last Change [%s ago]"                  <clock> (Garadget,doorStatus,UI) { garadget="<[2b0027001447373033383530#doorStatus_time]" }
// Send the doorState item commands like ZERO, HUNDRED, UP, DOWN, ON, OFF, STOP, or "open", "closed" or "stop".
Rollershutter doorState_door2   "Control"                       <rollershutter> (Garadget,doorStatus,UI) { garadget=">[2b0027001447373033383530#setState],<[2b0027001447373033383530#doorStatus_status]" }
Number doorStatus_sensor_door2  "Reflection [%d %%]"                      <sun> (Garadget,doorStatus,UI) { garadget="<[2b0027001447373033383530#doorStatus_sensor]" }
Number doorConfig_srt_door2     "Threshold [%d %%]"                   <battery> (Garadget,doorStatus,UI) { garadget="<[2b0027001447373033383530#doorConfig_srt]" }
Number doorStatus_signal_door2  "Signal [%d dB]"                      <battery> (Garadget,doorStatus,UI) { garadget="<[2b0027001447373033383530#doorStatus_signal]" }
String last_app_door2          "Last App [%s]"                                 (Garadget) { garadget="<[2b0027001447373033383530#last_app]" }
String last_ip_address_door2    "Last IP Address [%s]"                          (Garadget) { garadget="<[2b0027001447373033383530#last_ip_address]" }
DateTime last_heard_door2       "Last Heard [%1$tm/%1$td %1$tH:%1$tM]"  <clock> (Garadget) { garadget="<[2b0027001447373033383530#last_heard]" }
Number product_id_door2         "Product ID [%d]"                               (Garadget) { garadget="<[2b0027001447373033383530#product_id]" }
Switch connected_door2          "Connected [%s]"                                (Garadget) { garadget="<[2b0027001447373033383530#connected]" }

String doorStatus_door2         "Door Status [%s]"                              (Garadget) { garadget="<[2b0027001447373033383530#doorStatus]" }

String doorConfig_door2         "Door Config [%s]"                              (Garadget) { garadget="<[2b0027001447373033383530#doorConfig]" }
Number doorConfig_ver_door2     "Version [%.1f]"                                (Garadget) { garadget="<[2b0027001447373033383530#doorConfig_ver]" }
Number doorConfig_rdt_door2     "Sensor Scan Interval [%d ms]"                  (Garadget) { garadget="<[2b0027001447373033383530#doorConfig_rdt]" }
Number doorConfig_mtt_door2     "Door Moving Time [%d ms]"                      (Garadget) { garadget="<[2b0027001447373033383530#doorConfig_mtt]" }
Number doorConfig_rlt_door2     "Button Press Time [%d ms]"                     (Garadget) { garadget="<[2b0027001447373033383530#doorConfig_rlt]" }
Number doorConfig_rlp_door2     "Delay betw Presses [%d ms]"                    (Garadget) { garadget="<[2b0027001447373033383530#doorConfig_rlp]" }
Number doorConfig_srr_door2     "Sensor reads [%d]"                             (Garadget) { garadget="<[2b0027001447373033383530#doorConfig_srr]" }
Number doorConfig_aot_door2     "Open Timeout Alert [%d min]"                   (Garadget) { garadget="<[2b0027001447373033383530#doorConfig_aot]" }
Number doorConfig_ans_door2     "Night time alert start [%d min from midnight]" (Garadget) { garadget="<[2b0027001447373033383530#doorConfig_ans]" }
Number doorConfig_ane_door2     "Night time alert end [%d min from midnight]"   (Garadget) { garadget="<[2b0027001447373033383530#doorConfig_ane]" }

String netConfig_door2          "Net Config [%s]"                               (Garadget) { garadget="<[2b0027001447373033383530#netConfig]" }
String netConfig_ip_door2       "IP Address [%s]"                               (Garadget) { garadget="<[2b0027001447373033383530#netConfig_ip]" }
String netConfig_snet_door2     "Subnet [%s]"                                   (Garadget) { garadget="<[2b0027001447373033383530#netConfig_snet]" }
String netConfig_gway_door2     "Gateway [%s]"                                  (Garadget) { garadget="<[2b0027001447373033383530#netConfig_gway]" }
String netConfig_mac_door2      "MAC address [%s]"                              (Garadget) { garadget="<[2b0027001447373033383530#netConfig_mac]" }
String netConfig_ssid_door2     "SSID [%s]"                                     (Garadget) { garadget="<[2b0027001447373033383530#netConfig_ssid]" }

// send the setConfig item commands like "ans=1200|ane=420" to set night time alert to 8pm-7am, for example.
String setConfig_door2          "Garage Door 2 Config [%s]"              (Garadget) { garadget=">[2b0027001447373033383530#setConfig],<[2b0027001447373033383530#doorConfig]" }

Switch doorStatus_status_door2_timer_10 (doorStatusTimers){expire="10m,command=OFF"}
Switch doorStatus_status_door2_timer_30 (doorStatusTimers){expire="20m,command=OFF"}

The door status reported by the binding can vary depending on how you open the door and I think also the brand of opener.

It will go one of two ways:

  1. from opening to open, or
  2. from closed to open.

Someone more experienced can probably write this more efficiently, but the below rules worked for me in both conditions.

For my example, I will send an email (specifically, an email-to-SMS text for Verizon) if either door is still open after 10 minutes, and send another notice if still open after 30 minutes.

Even though the Garadget app lets you assign one email address, it does not let you assign more than one (or a different one to each door), nor does it allow you to use a email-to-SMS text address (at least it did not accept mine).

So, by using this rule format with the email action, the binding will let you send notification to as many addresses or phone numbers you wish. Note that using this method is a bit slower than if you use Pushover and/or the Gardget app notification. In my case, the difference was less than one minute longer. So, if you are going for 10 minutes, you might want to set your timer for 9 minutes instead.

garadget.rules

var Timer door1OpenTimer = null
var Timer door2OpenTimer = null
val int timeoutMinutes = 30

rule "Garage Door 1 Left Open for 10 minutes first"
when
    Item doorStatus_status_door1 changed from closed to open
then
    if (door1OpenTimer === null) {
        door1OpenTimer = createTimer(now.plusMinutes(10)) [|
        door1OpenTimer = null
        if (doorStatus_status_door1.state.toString =="open"){
            logInfo("Garage Door 1", "The garage door for the Camry has been left open for 10 minutes. Message texted to owner.")
            sendMail("5127775689@vtext.com", " ", "The garage door for the Camry has been left open for 10 minutes.")
            }
        ]
    }
end

rule "Garage Door 1 Left Open for 10 minutes second"
when
    Item doorStatus_status_door1 changed from opening to open
then
    if (door1OpenTimer === null) {
        door1OpenTimer = createTimer(now.plusMinutes(10)) [|
        door1OpenTimer = null
        if (doorStatus_status_door1.state.toString =="open"){
            logInfo("Garage Door 1", "The garage door for the Camry has been left open for 10 minutes. Message texted to owner.")
            sendMail("5127775689@vtext.com", " ", "The garage door for the Camry has been left open for 10 minutes.")
            }
        ]
    }
end

rule "Garage Door 1 Left Open for 30 minutes first"
when
    Item doorStatus_status_door1 changed from closed to open
then
    if (door1OpenTimer === null) {
        door1OpenTimer = createTimer(now.plusMinutes(timeoutMinutes)) [|
        door1OpenTimer = null
        if (doorStatus_status_door1.state.toString =="open"){
            logInfo("Garage Door 1", "The garage door for the Camry has been left open for 30 minutes. Message texted to owner.")
            sendMail("5127775689@vtext.com", " ", "The garage door for the Camry has been left open for 30 minutes.")
            }
        ]
    }
end

rule "Garage Door 1 Left Open for 30 minutes second"
when
    Item doorStatus_status_door1 changed from opening to open
then
    if (door1OpenTimer === null) {
        door1OpenTimer = createTimer(now.plusMinutes(timeoutMinutes)) [|
        door1OpenTimer = null
        if (doorStatus_status_door1.state.toString =="open"){
            logInfo("Garage Door 1", "The garage door for the Camry has been left open for 30 minutes. Message texted to owner.")
            sendMail("5127775689@vtext.com", " ", "The garage door for the Camry has been left open for 30 minutes.")
            }
        ]
    }
end

rule "Garage Door 2 Left Open for 10 minutes first"
when
    Item doorStatus_status_door2 changed from closed to open
then
    if (door2OpenTimer === null) {
        door2OpenTimer = createTimer(now.plusMinutes(10)) [|
        door2OpenTimer = null
        if (doorStatus_status_door2.state.toString =="open"){
            logInfo("Garage Door 2", "The garage door for the Corolla has been left open for 10 minutes. Message texted to owner.")
            sendMail("5125550123@vtext.com", " ", "The garage door for the Corolla has been left open for 10 minutes.")
            }
        ]
    }
end

rule "Garage Door 2 Left Open for 10 minutes second"
when
    Item doorStatus_status_door2 changed from opening to open
then
    if (door2OpenTimer === null) {
        door2OpenTimer = createTimer(now.plusMinutes(10)) [|
        door2OpenTimer = null
        if (doorStatus_status_door2.state.toString =="open"){
            logInfo("Garage Door 2", "The garage door for the Corolla has been left open for 10 minutes. Message texted to owner.")
            sendMail("5125550123@vtext.com", " ", "The garage door for the Corolla has been left open for 10 minutes.")
            }
        ]
    }
end

rule "Garage Door 2 Left Open for 30 minutes first"
when
    Item doorStatus_status_door2 changed from closed to open
then
    if (door2OpenTimer === null) {
        door2OpenTimer = createTimer(now.plusMinutes(timeoutMinutes)) [|
        door2OpenTimer = null
        if (doorStatus_status_door2.state.toString =="open"){
            logInfo("Garage Door 2", "The garage door for the Corolla has been left open for 30 minutes. Message texted to owner.")
            sendMail("5125550123@vtext.com", " ", "The garage door for the Corolla has been left open for 30 minutes.")
            }
        ]
    }
end

rule "Garage Door 2 Left Open for 30 minutes second"
when
    Item doorStatus_status_door2 changed from opening to open
then
    if (door2OpenTimer === null) {
        door2OpenTimer = createTimer(now.plusMinutes(timeoutMinutes)) [|
        door2OpenTimer = null
        if (doorStatus_status_door2.state.toString =="open"){
            logInfo("Garage Door 2", "The garage door for the Corolla has been left open for 30 minutes. Message texted to owner.")
            sendMail("5125550123@vtext.com", " ", "The garage door for the Corolla has been left open for 30 minutes.")
            }
        ]
    }
end

Being new to openHAB, I welcome any improvements you can offer to this sample code!

This should be implementable in a single Rule.

  • You can have more than one trigger for a Rule
  • You are not required to have a from in the trigger
  • In this case, we can handle the open or closed in the same Rule
  • Inside the Rule, you can figure out which Item triggered the Rule using triggeringItem
  • Design Pattern: Expire Binding Based Timers make for simpler code so I prefer to use them. Then we can also use Design Pattern: Associated Items, but I’ll show it using regular Timers because it will be easier to handle the two different times that way.
  • This would be a perfect use for Design Pattern: Looping Timers
  • Put your Items into a Group and you can simplify the triggers. Then you can add all the rest of your doors and windows to that Group and reuse this same Rule for reminders on those.
import org.eclipse.smarthome.model.script.ScriptServiceUtil
import java.util.Map

val Map<String, Timers> timers = newHashMap

rule "A garage door opened"
when
    Member of doorStatus changed to open or
    Member of doorStatus changed to closed
then
    // Get the timer
    val timer = timers.get(triggeringItem.name)

    // A door changed to ON and there isn't already a timer (shouldn't happen where timer !== null)
    if(triggerinItem.state.toString == "open" && timer === null) {
        var time = 10 // we use this as a flag to see if this is the first or second time the timer runs

        timers.put(createTimer(now.plusMinutes(time.intValue), [ |

            // The Timer would have been canceled by now if the door closed, no need to check

            // Get the car name and email address assocaited with triggeringItem
            val carName = transform("MAP", "garagedoors.map", triggeringItem.name) // see https://community.openhab.org/t/design-pattern-human-readable-names-in-messages/34117
            val phone = transform("MAP", "phones.map", triggeringItem.name)

            // Send the alert
            logInfo("Garage Door 1", "The garage door for the " + carName + " has been left open for " + time + " minutes. Message texted to owner.")
            sendmail(phone, " ", "The garage door for the " + carName + " has been left open for " + time + " minutes")

            // if time == 10, this is the first time we ran, change time to 30 so next time we know it's the second
            // time the timer was run and we can send the appropriate alert
            if(time == 10) { 
                time = 30
                timers.get(triggeringItem.name).reschedule(now.plusMinutes(time.intValue))
            }
            else {
                timers.put(triggeringItem.name, null) // we are done after the second alert
            }

        ])
    } // changed to open
    else {
        timers.get(triggeringItem.name)?.cancel // the ? means only call cancel if the get is not null
        timers.put(triggeringItem.name, null)
    } // changed to closed
end

This is better. But what if we used Expire based Timers after all?

import org.eclipse.smarthome.model.script.ScriptServiceUtil

rule "A garage door changed to open or closed"
when
    Member of doorStatus changed to open or
    Member of doorStatus changed to closed
then
    val timer1 = ScriptServiceUtil.getItemRegistry.getItem(triggeringItem.name+"_timer_10")
    val timer2 = ScriptServiceUtil.getItemRegistry.getItem(triggeringItem.name+"_timer_30")
    
    if(triggeringItem.state.toString == "closed") {
        timer1.postUpdate(OFF)
        tiemr2.postUpdate(OFF)
    }

    else if(triggeringItem.state.toString == "open" && timer1 == OFF && timer2 == OFF) {
        timer1.sendCommand(ON)
    }
    // othewise ignore the event, or log a warning as this should never happen
end

rule "A timer expired"
when
    Member of doorStatusTimers received command ON or
    Member of doorStatusTimers received command OFF
then
    // Get the appropriate information for this timer
    val carName = transform("MAP", "garagedoors.map", triggeringItem.name) 
    val phone = transform("MAP", "phones.map", triggeringItem.name)
    val time = triggeringItem.name.split("_").get(4)

    // Send the alert
    logInfo("Garage Door", "The garage door for the " + carName + " has been left open for " + time + " minutes. Message texted to owner.")
    sendmail(phone, " ", "The garage door for the " + carName + " has been left open for " + time + " minutes")

    // Schedule the next timer
    if(time == "10") {
        sendCommand(triggeringItem.name.replace("10", "30"))
    }
end    

It’s fewer lines of code and it is significantly simpler.

If you are willing to fudge on your requirements, the Rules can be made even simpler. For example, if you just want an alert every 10 minute or every 30 minutes until it closes, I can drop the lines of code in this last example by half.

For more ways to avoid duplicated code see Design Pattern: DRY, How Not to Repeat Yourself in Rules DSL.

1 Like

Wow, thanks Rich! I will need to study all you have done here and it is clear I have much to learn. I appreciate your thoughtful advice!

If the two timers are set by door1 and then door2 is opened five minutes later, it appears that the timer1 and timer2 values would be overwritten by door2, causing the door1 timers to be lost, no?

No because each door has it’s own Timer. In the first example, we put that timer into a Map keyed on the name of the door’s Item. That’s what a Map does, it stores a bunch of values with a corresponding key.

In the Expire example, each door has it’s own Timer Item and it is names with the same name as the door with “_timer_10” or “_timer_30”.

In short, each door has it’s own timers.

Thanks Rich. I updated garadget.items in my first post to add the group names doorStatus and doorStatusTimers, and added the timer items.

Here is what the ‘near final’ version looks like, but the last rule: “A timer expired” does not yet work for two reasons, described below.

garadget.rules

import org.eclipse.smarthome.model.script.ScriptServiceUtil

rule "A garage door changed to open or closed"
when
    Member of doorStatus changed to open or
    Member of doorStatus changed to closed 
then
    val carName = transform("MAP", "garagedoors.map", triggeringItem.name)
    logDebug("Garage Door", "The garage door status for " + carName + " changed. Starting timer.")
    val timer1 = ScriptServiceUtil.getItemRegistry.getItem(triggeringItem.name+"_timer_10")
    val timer2 = ScriptServiceUtil.getItemRegistry.getItem(triggeringItem.name+"_timer_30")
    
   // If system is restarted, need to check for null state and if needed change default to OFF \n
   if( timer1.state === NULL){
      timer1.postUpdate(OFF)
      }
   if( timer2.state === NULL){
      timer2.postUpdate(OFF)
      }
    
    // If one of the doors is closed or gets closed, turn off its timer \n  
    if(triggeringItem.state.toString == "closed") {
        timer1.postUpdate(OFF)
        timer2.postUpdate(OFF)
        logDebug("Garage Door", "The garage door for " + carName + " is (or has just been) closed. Timer is canceled.")
    }    
    logDebug("Garage Door", "The trigger is " + triggeringItem.state.toString + ".")
    
    // Check the state of both timers when the door's state is open. If neither timer is on, turned on timer1 \n
    if(triggeringItem.state.toString == "open" && timer1.state == OFF && timer2.state == OFF){
        logDebug("Garage Door", "Trigger to open timer received for " + carName + ". Next, the timer command should work.")
        sendCommand(timer1, ON)
        logInfo("Garage Door", "The garage door for " + carName + " was opened and timer1 is not already on. Starting timer1.")
    }  
logDebug("Garage Door", "The value for timer1 is " + timer1 + " and timer2 is " + timer2 +". The trigger is " + triggeringItem.state.toString + ".")

// otherwise ignore the event, or log a warning as this should never happen\n
end

rule "A timer expired"
when 
    Member of doorStatusTimers received command ON or
    Member of doorStatusTimers received command OFF
then
    // Get the appropriate information for this timer\n
    val carName = transform("MAP", "garagedoors.map", triggeringItem.name) 
    val phone = transform("MAP", "phones.map", triggeringItem.name)
    val time = triggeringItem.name.split("_").get(4)
    // Send the alert\n
    logInfo("Garage Door", "val time is " + time + " , and the garage door for the " + carName + " has been left open for " + time + " minutes. Message texted to owner.")
    sendMail(phone, " ", "The garage door for the " + carName + " has been left open for " + time + " minutes")
    // Schedule the next timer\n
    if(time == "10") {
        sendCommand(triggeringItem.name, triggeringItem.name.replace("10", "30"))
        logInfo("Garage Door", "The garage door has not been been closed after ten minutes, so timer is now set for 20 more minutes.")
        }
end  

The first issue:
When a door is opened, timer1 is created, it triggers the rule “A timer expired” (because command ON is received), which sends an email alert - not when the timer expires - but when the timer gets created. I think in part because we have:

val time = triggeringItem.name.split("_").get(4)

When the first timer is created, val time == “10”.

The second issue is this if statement from the same rule, using val time based on the item name, not the expiration of the timer:

    if(time == "10") {
        sendCommand(triggeringItem.name.replace("10", "30"))
        logInfo("Garage Door", "The garage door has not been been closed after ten minutes, so timer is now set for 20 more minutes.")
        }

the sendCommand statement generated this error:

Rule 'A timer expired': An error occurred during the script execution: index=1, size=1

So I changed it from

sendCommand(triggeringItem.name.replace("10", "30"))

to:

sendCommand(triggeringItem.name, triggeringItem.name.replace("10", "30"))

This eliminated the error, but I am getting this bus event error:

Cannot convert 'doorStatus_status_door1_timer_30' to a command type which item 'doorStatus_status_door1_timer_10' accepts: [OnOffType, RefreshType].

Also, even though I have this in my items file:

Switch doorStatus_status_door1_timer_10 (doorStatusTimers){expire="10m,command=OFF"}

The timer does not ever expire. I am working on fixes for all of this. Any advice?

garagedoors.map

doorStatus_status_door1=Camry
doorStatus_status_door2=Corolla
doorStatus_status_door1_timer_10=Camry
doorStatus_status_door1_timer_30=Camry
doorStatus_status_door2_timer_10=Corolla
doorStatus_status_door2_timer_30=Corolla

garadget.map

open=UP
close=DOWN
stop=STOP
ON=open
UP=open
0=open
OFF=close
DOWN=close
100=close
STOP=stopped

phones.map

doorStatus_status_door1= 5127775689@vtext.com
doorStatus_status_door2=5125550123@vtext.com
doorStatus_status_door1_timer_10=5127775689@vtext.com
doorStatus_status_door1_timer_30=5127775689@vtext.com
doorStatus_status_door2_timer_10=5125550123@vtext.com
doorStatus_status_door2_timer_30=5125550123@vtext.com

The timer expired Rule should only ever be triggered on received command OFF. That was a dumb mistake in my original version. Sorry about that.

Another oops. The command isn’t actually being sent. sendCommand needs two arguments, the name of the Item and the actual command. That line is only providing the name of the Item. I think

sendCommand(triggeringItem.name.replace("10", "30"), "ON")

is correct.

You are getting an error because you are trying to send the command “doorStatus_status_door1_timer_30” to a Switch Item. Obviously, that is a nonsense command to send to a Switch, which can only accept “ON” or “OFF” as a command.

Thanks Rich, it works correctly with those changes. I appreciate your help very much. It is encouraging and helps to keep an old man like me learning!

Here is the final solution.

garadget.rules

import org.eclipse.smarthome.model.script.ScriptServiceUtil

// Besides the import ScriptServiceUtil, these rules require the Garadget and Expire Bindings, and the Javascript and Map Transformations \n
// Script can be used to send specifically-timed, multiple email or text notifications, and tailored based on how long a garage door has been \n
// left open. This script is for operating two garage doors. \n

rule "A garage door changed to open or closed" 
when
    Member of doorStatus changed to open or
    Member of doorStatus changed to closed 
then
    val carName = transform("MAP", "garagedoors.map", triggeringItem.name)
    logDebug("Garage Door", "The garage door status for " + carName + " changed. Starting timer.")
    val timer1 = ScriptServiceUtil.getItemRegistry.getItem(triggeringItem.name+"_timer_10")
    val timer2 = ScriptServiceUtil.getItemRegistry.getItem(triggeringItem.name+"_timer_30")
    
   // If system is restarted, need to check for null state and if needed change default to OFF \n
   if( timer1.state === NULL){
      timer1.postUpdate(OFF)
      }
   if( timer2.state === NULL){
      timer2.postUpdate(OFF)
      }
    
    // If one of the doors is closed or gets closed, turn off its timer \n  
    if(triggeringItem.state.toString == "closed") {
        timer1.postUpdate(OFF)
        timer2.postUpdate(OFF)
        logDebug("Garage Door", "The garage door for " + carName + " is (or has just been) closed. Timer is canceled.")
    }    
    logDebug("Garage Door", "The trigger is " + triggeringItem.state.toString + ".")
    
    // Check the state of both timers when the door's state is open. If neither timer is on, turn on timer1 \n
    if(triggeringItem.state.toString == "open" && timer1.state == OFF && timer2.state == OFF){
        logDebug("Garage Door", "Trigger to open timer received for " + carName + ". Next, the timer command should work.")
        sendCommand(timer1, ON)
        logInfo("Garage Door", "The garage door for " + carName + " was opened and timer1 is not already on. Starting timer1.")
    }  
    logDebug("Garage Door", "The value for timer1 is " + timer1 + " and timer2 is " + timer2 +". The trigger is " + triggeringItem.state.toString + ".")

// otherwise ignore the event, or log a warning as this should never happen\n
end

rule "A timer expired"
when   
    Member of doorStatusTimers received command OFF
then
    // Get the appropriate information for this timer\n
    val carName = transform("MAP", "garagedoors.map", triggeringItem.name) 
    val phone = transform("MAP", "phones.map", triggeringItem.name)
    val time = triggeringItem.name.split("_").get(4)
    // Send the alert\n
    logDebug("Garage Door", "val time is " + time + ", and the garage door for the " + carName + " has been left open for " + time + " minutes. Message texted to owner.")
    sendMail(phone, " ", "The garage door for the " + carName + " has been left open for " + time + " minutes")
    // Schedule the next timer\n
    if(time == "10") {
        sendCommand(triggeringItem.name.replace("10", "30"), "ON")
        logInfo("Garage Door", "The garage door has not been been closed after ten minutes, so timer is now set for 20 more minutes.")
        }
end

Thanks to the release of 2.5M2, my garadget setup is now much simpler. I have updated the original post above.