Receiving a mail with battery status and alerts on long last seen devices, in recursive groups

Hi, this is my first post on my solutions. Excuse my english for I’m French.

Here’s a rule to send / receive a mail, at 7AM and 7PM, with battery status (< 30% in this example) and alerts on devices which were not seen for a certain amount of time (over 60 minutes in this example).

rule "Controle capteurs"

    when

        Time cron "0 0 7,19 ? * * *"

    then

        VarTest=""
	
        /////////////// Long last seen items ////////////////////

        VarTest=VarTest + "\n__________________________________________________________________________________\n\nFollowing items have not been seen for a long time : \n__________________________________________________________________________________"

        G_Devices_Updates?.members.sortBy[ name ].forEach[ profile |
            VarTest = VarTest + "\n" + "\n" + "----------  " + profile.label + "  ----------"  
     
            // get group object
            val gProfile = G_Devices_Updates.members.filter[ p | p.name == profile.name ].head as GroupItem

            // now loop through all items in the profile
            gProfile.members.filter(item|((item.state != NULL) && (Duration.between((item.state as DateTimeType).getZonedDateTime(), ZonedDateTime.now()).toMinutes()) > 60 )).sortBy[ label ].forEach[item | VarTest = VarTest + "\n" + item.label + " - " + (Duration.between((item.state as DateTimeType).getZonedDateTime(), ZonedDateTime.now()).toMinutes()) + " mn"]

            VarTest=VarTest + "\n"
        
            gProfile.members.filter(item|(item.state == NULL)).sortBy[ label ].forEach[item | VarTest = VarTest + "\n" + item.label + " - Jamais vu !"]
        ]
        
        logInfo("My Rule", VarTest)


        /////////////// Weak batteries ////////////////////


        VarTest=VarTest + "\n\n\n__________________________________________________________________________________\n\nFollowing batteries are weak : \n__________________________________________________________________________________"

   	
        G_Batteries?.members.sortBy[ name ].forEach[ profile |
            VarTest = VarTest + "\n" + "\n" + "----------  " + profile.label + "  ----------"  

            // get group object
            val gProfile2 = G_Batteries.members.filter[ p | p.name == profile.name ].head as GroupItem

            // now loop through all items in the profile
            gProfile2.members.filter(item|((item.state != NULL) && (item.state < 30 ))).sortBy[ label ].forEach[item | VarTest = VarTest + "\n" + item.label + " - Charge : " + item.state + "%"]

            VarTest= VarTest + "\n"
        
            gProfile2.members.filter(item|(item.state == NULL)).sortBy[ label ].forEach[item | VarTest = VarTest + "\n" + item.label + " - Jamais vu !"]
        ]

        logInfo("My Rule", VarTest)
   

        /////////////// Send mail ////////////////////

       val mailActions = getActions("mail","mail:smtp:googlesmtp")  
       mailActions.sendHtmlMail("xxxxxxx@xxxxx.com", "Capteurs non actifs", VarTest) 

   
end

The rule goes through included groups in a master group. Example for Batteries group :

/////////////////////////////    Niveaux batteries   /////////////////////////////////

Group        G_Batteries                   "Etat batteries"            <battery>         
Group        G_Sensor_Batteries            "Batteries capteurs"        <battery>         (G_Batteries)
Group        G_Contact_Batteries           "Batteries contacts"        <battery>         (G_Batteries)
Group        G_Motion_Batteries            "Batteries motion sensors"   <battery>        (G_Batteries)
Group        G_Switch_Batteries            "Batteries switches"        <battery>         (G_Batteries)
Group        G_Thermostat_Batteries        "Batteries thermostats"     <battery>         (G_Batteries)

With Items in includes groups :slight_smile:

Number    Escalier_Motion_Batt
          "Niveau Capteur Présence escalier [%d %%]"
          <battery>
          (G_Motion_Batteries)
          {channel="mqtt:topic:zigbee2mqtt:Motion_Escalier:battery"}

Don’t hesitate to comment or help to improve code.

2 Likes

You asked for it :smiling_imp: Don’t worry, it looks pretty good actually.

  • The biggest addition I would make is to use a map/reduce instead of a forEach and maybe break it up a bit to make it easier to read.

  • Also be consistent in the use of [ ] and ( ).

  • Finally, you could use Item tags instead of Group memberships as an alternative (I won’t show that here but can if asked)

  • You’ve already got the profile Group Item, there is no need to get it again using a filter.

For example, the “Long last seen” might become:

G_Devices_Updates?.members.sortBy[ name ].forEach[ profile |
    VarTest = VarTest  + "\n" + "\n" + "----------  " + profile.label + "  ----------"
    VarTest = VarTest + profile.members + '\n'
                               .filter[ item | 
                                   val iTime = (item.state as DateTimeType).getZonedDateTime()
                                   val delta = Duration.between(iTime, now).toMinutes()
                                   !(item.state instanceof UnDefType) && delta > 60
                               ]
                               .sortBy[ label ]
                               .map[ label ]
                               .reduce[ msg, lbl |
                                   val iTime = (item.state as DateTimeType).getZonedDateTime()
                                   val delta = Duration.between(iTime, now).toMinutes()
                                   msg = msg + lbl + ' - ' + delta + ' mn' + '\n'
                               ]
    VarTest = VarTest + profile.members
                               .filter[ item | item.state instanceof UnDefType ]
                               .sortBy[ label ]
                               .map[ label ]
                               .reduce[ msg, lbl | msg = msg + '\n' + lbl + ' - Jamais vu!' ]
]

I just typed in the above, there are likely typos.

Here’s a rule that I got off the forum a while ago that handles battery watching.

→ Batteries ← is a group in .items tagged to battery related items

var Number			lowBatteryThreshold = 20					// Used for monitoring battery levels


rule "Battery Monitor CRON 4x Daily"
	when
		Time cron "0 35 3  ? * * *"	or				// 3:35  AM Daily	
        Time cron "0 11 8  ? * * *"	or				// 8:11  AM Daily
        Time cron "0 11 14 ? * * *"	or				// 2:11  PM Daily
		Time cron "0 31 22 ? * * *"					// 10:31 PM Daily

	then

		if (systemStarted.state != ON && gInternet.state == ON && gPort25.state == ON && OH_Uptime_HumanReadable.state != NULL && OH_Uptime_HumanReadable.state !== null && OH_Uptime_HumanReadable.state.toString() != 'UNDEF') {

 			logInfo("BATTERY","-----------------------------------------------------------------------------")
			logInfo("BATTERY","Checking zWave and HUE Batteries Levels via CRON Rule.")
			logInfo("BATTERY","-----------------------------------------------------------------------------")
			var MailBinding = getThingStatusInfo("mail:smtp:4228b22a33")

			if (!Batteries.allMembers.filter( [ GenericItem battery | battery.state != NULL && battery.state !== null && battery.state.toString() != 'UNDEF' && (battery.state as Number) <= (lowBatteryThreshold) ] ).empty) {
		
				val String report = Batteries.allMembers.filter( [ (state as Number) <= (lowBatteryThreshold) ] ).sortBy( [ state as DecimalType ] ).map[ name + ": " + state.format("%d%%") ].join("\n")
	
				if ((MailBinding !== null) && (MailBinding.getStatus().toString() == 'ONLINE')) {	
	
					val	mailActions = getActions("mail","mail:smtp:4228b22a33")
					val String subjectemailbattery	=	"openHAB3 - Low Battery Alert!"
					val batterymessage = "Battery Levels:\n\n" + report + "\n\n** End of Report **"	
					mailActions.sendMail(JaygMail, subjectemailbattery, batterymessage)
					logInfo("EMAIL",subjectemailbattery)
					logInfo("BATTERY","Battery Report eMail Sent to Jays gMail.")
				}	
			}
		}
end

Best, Jay

Thanks for your answers :slight_smile:

Jay,
fact is my master group is NULL only the secondary groups have values:
image

image

And if any of my item has a NULL state for any reason, my rule still trigger the alert, example :

----------  Batteries contacts  ----------

Niveau Capteur Contact Fenêtre Chambre Titouan - NULL
Niveau Capteur Contact Velux Chambre Titouan - NULL
Niveau Capteur Contact Velux Salle de bain - NULL
Niveau Capteur Contact Velux Salle de jeu - NULL

----------  Batteries motion sensors  ----------


----------  Batteries capteurs  ----------
Niveau capteur Chambre Mëlyne - Charge : 62%
Niveau capteur Chambre Parents - Charge : 62%
Niveau capteur Congélateur - Charge : 37%
Niveau capteur Frigo cuisine - Charge : 62%
Niveau capteur Salle d'eau - Charge : 68%
Niveau capteur Salon - Charge : 62%
Niveau capteur chambre Titouan - Charge : 40%

Niveau capteur Extérieur - NULL

----------  Batteries switches  ----------
Niveau switch 4 boutons cuisine - Charge : 0%
Niveau switch 4 boutons salon - Charge : 0%

Niveau switch 2 boutons dressing cuisine - NULL
Niveau switch 2 boutons salon cuisine - NULL
Niveau switch carré Alarme - NULL
Niveau switch cube - NULL

Rich : I’m trying to run your rule without any success, I’m stuck at first

profile.members

having in log :

Script execution of rule with UID 'test-1' failed: 'members' is not a member of 'org.openhab.core.items.Item'

I still have to learn about maps and tags, still have a long path ahead of me :slight_smile:

Try (profile as GroupItem).members

So, how is that defined?
This Group Item -

has no aggregation function defined, so it is expected to just lie there and stay at NULL.
Aggregation functions -