I spent many hours trying to debug the code in the 1st section below; that code was based on a prior version I wrote that worked.
A "groovy.lang.MissingMethodException" error occurred with my new version that was always related to scheduling (see one version of the error in the 2nd section below). I tried MANY different ways to get scheduling to work, some of which are shown in the "initialize" method in the code (I commented out all but one of the attempts). That method should have worked with just a simple "schedule(runTime, handlerX)".
After comparing the code that threw the error to the prior version, line by line, I could find no reason the error occurred. So I started copying and pasting code lines from the broken app to a copy of the old app -- no errors. So I then copied and pasted ALL of the broken app to a new app (renaming the new app) -- and the error went away and the app worked.
In the 3rd section below is the working "clone". The ONLY difference between the broken and working apps is in the app name ("OLD" vs. "NEW").
Is this a known bug? And how do I distinguish between this bug and an actual programming mis-coding that throws a legitimate error?
definition(
name: "Battery Device Status Table OLD",
namespace: "hubitat",
author: "based on 'Battery Notifier' by Bruce Ravenel, modified by John Land via ChatGPT",
description: "Battery Device Status with battery %, event description sorting, and offline reporting",
installOnOpen: true,
category: "Convenience",
iconUrl: "",
iconX2Url: ""
)
/*
PURPOSE: Check the state of battery based devices
FEATURES:
* (2025-08-15) Shows only devices that report a "battery" capability.
* (2025-08-15) Selectively shows devices based on "last battery event" or "last event of any kind".
* (2025-08-15) Configurable reporting interval and reporting time.
* (2025-08-15) Output is in a sortable table for ease of reading.
* (2025-08-15) The parenthetical details in the section headings can be suppressed.
* (2025-08-18) Multiple notification devices supported.
* (2025-08-18) Plain-text table for notification devices (e.g., Pushover, Hubitat)
* (2025-08-18) "[Never]" is red only in "Check Now" view, plain in notifications.
* (2025-08-19) Some more cosmetic changes to the plain text "table" for notification devices (e.g., Pushover, Hubitat)
* (2025-08-19) More cosmetic changes to output tables
* (2025-08-21) Added ability to report only OFFLINE devices
*/
preferences {
page(name: "mainPage")
}
def mainPage() {
dynamicPage(name: "mainPage", title: "Battery Device Status", uninstall: true, install: true) {
// Device selection
def devCount = devs?.findAll { !it.isDisabled() }?.size() ?: 0
def devTitle = "Battery Device Selection"
if (showSectionDetails) devTitle += " (${devCount} selected)"
section(hideable:true, hidden:true, title: devTitle) {
input "devs", "capability.battery",
title: "Select devices (disabled devices will be omitted even if selected)",
submitOnChange:true, multiple:true, required:false
}
// Notification devices
def noticeNames = []
if (notice) {
noticeNames = (notice instanceof Collection) ? notice*.displayName : [notice.displayName]
}
def noticeLabel = noticeNames ? noticeNames.join(", ") : "Select notification device(s)"
def noticeTitle = "Notification Settings"
if (showSectionDetails) noticeTitle += " (${noticeLabel})"
section(hideable:true, hidden:true, title: noticeTitle) {
input "notice", "capability.notification",
title:"Select notification device(s)",
submitOnChange:true, multiple:true, required:true
}
// Report Type, Scheduling & Logging
def reportIntervalLabel = reportIntervalHours ?: 24
def runTimeLabel = runTime ? timeToday(runTime, location.timeZone).format("hh:mm a") : ""
def loggingLabel = enableLogging ? "Enabled" : "Disabled"
def reportTypeLabel = reportType == "any" ? "Last Event (any type)" : (reportType=="offline" ? "Offline Devices Only" : "Last Battery Event Only")
def scheduleTitle = "Report Type, Schedule & Logging"
if (showSectionDetails) {
scheduleTitle += " (Report: ${reportTypeLabel}, Interval: ${reportIntervalLabel}h, Daily check: ${runTimeLabel}, Logging: ${loggingLabel})"
}
section(hideable:true, hidden:true, title: scheduleTitle) {
input "reportType", "enum",
title:"Select which event type to check for reporting",
options:["battery":"Last Battery Event Only","any":"Last Event (any type)","offline":"Offline Devices Only"],
defaultValue:"battery", required:true, submitOnChange:true
input "reportIntervalHours", "number", title:"Reporting interval in hours (default = 24)", defaultValue:24, required:true
input name:"runTime", type:"time", title:"Daily check time", required:true
input "showSectionDetails", "bool", title:"Show extra details in section headers?", defaultValue:true
input "enableLogging", "bool", title:"Enable debug logging?", defaultValue:false
}
// Sort Options
def sortTitle = "Sort Options"
def sortByLabel = "Last Event Time"
def sortOrderLabel = sortOrder == "asc" ? "Ascending" : "Descending"
if (reportType=="offline") {
sortByLabel = "Device Name"
if (showSectionDetails) sortTitle += " (Sort by: ${sortByLabel}, Order: ${sortOrderLabel})"
section(hideable:true, hidden:true, title: sortTitle) {
input "sortOrder","enum",title:"Order",options:["asc":"Ascending","desc":"Descending"], defaultValue:sortOrder ?: "asc", submitOnChange:true
}
} else {
switch(sortBy) {
case "displayName": sortByLabel = "Device Name"; break
case "lastStr": sortByLabel = "Last Event Time"; break
case "level": sortByLabel = "Battery %"; break
case "lastEventStr": sortByLabel = "Event Description"; break
}
if (showSectionDetails) sortTitle += " (Sort by: ${sortByLabel}, Order: ${sortOrderLabel})"
section(hideable:true, hidden:true, title: sortTitle) {
def sortOptions = ["displayName":"Device Name","lastStr":"Last Event Time","level":"Battery %"]
if (reportType == "any") sortOptions["lastEventStr"] = "Event Description"
input "sortBy","enum",title:"Sort by",options:sortOptions, defaultValue:sortBy ?: "lastStr", submitOnChange:true
input "sortOrder","enum",title:"Order",options:["asc":"Ascending","desc":"Descending"], defaultValue:sortOrder ?: "asc", submitOnChange:true
}
}
// Check Now
section(title:"") {
input "check","button", title:"Check Now", state:"check"
if (state.check) {
paragraph handler()
state.check = false
}
}
}
}
def updated() {
unschedule()
unsubscribe()
initialize()
if (enableLogging) log.debug "Updated: ${devs?.size() ?: 0} devices"
}
def installed() { initialize() }
void initialize() {
unschedule()
if (enableLogging) log.debug "Initializing Battery Device Status..."
// if (runTime) schedule(runTime, handlerX)
if (runTime) {
def t = timeToday(runTime, location.timeZone)
def hours = t.format("H", location.timeZone)
def minutes = t.format("m", location.timeZone)
def cronStr = "0 ${minutes} ${hours} * * ?"
if (enableLogging) log.debug "Scheduled daily check at ${t.format("hh:mm a", location.timeZone)} (cron: ${cronStr})"
//schedule("${cronStr}", "handlerX")
//schedule(cronStr.toString(), "handlerX")
//schedule(cronStr, "handlerX")
//schedule(runTime, "handlerX")
schedule(runTime, handlerX)
}
if (enableLogging) log.debug "Initialized with ${devs?.size() ?: 0} devices"
}
void handlerX() {
if (enableLogging) log.debug "Scheduled check running..."
handler(true)
}
// ----------------------
// Main Handler
// ----------------------
String handler(note=false) {
def sortByVal = settings.sortBy ?: "lastStr"
def sortOrderVal = settings.sortOrder ?: "asc"
def selectedEnabledDevices = (devs ?: []).findAll{!it.isDisabled()}
if (!selectedEnabledDevices) return "No battery devices found."
def rightNow = new Date()
// Collect device info
def reportList = selectedEnabledDevices.collect { dev ->
def lastEvent = (reportType=="any") ? dev.events(max:1)?.find{true} : dev.events(max:50)?.find{ it.name=="battery" }
def lastEventDate = lastEvent?.date
def eventDesc = (reportType=="any" && lastEvent) ? "(Event: ${lastEvent.name} ${lastEvent.value})" : ""
def fs = '\u2007' // figure space
def batteryLevel = dev.currentBattery != null ? Math.round(dev.currentBattery).toString().padLeft(3, fs) : "N/A".padLeft(3, fs)
// Offline detection using getStatus() and currentHealthStatus
def devStatus = dev.getStatus()?.toUpperCase()
def isOffline = devStatus in ["OFFLINE", "INACTIVE", "NOT PRESENT"] || (dev.currentHealthStatus?.toLowerCase() == "offline")
// Determine if device should be included in report
def needsNotice = (reportType=="offline" ? isOffline : !lastEventDate || ((rightNow.time - lastEventDate.time)/60000) > ((reportIntervalHours ?: 24) * 60))
String lastStrUI = lastEventDate ? lastEventDate.format("yyyy-MM-dd hh:mm a", location.timeZone) : "<span style='color:red;'>[Never]</span>"
String lastStrNote = lastEventDate ? lastEventDate.format("yyyy-MM-dd hh:mm a", location.timeZone) : "0000-00-00 00:00 xx"
[device:dev, lastDate:lastEventDate, lastStrUI:lastStrUI, lastStrNote:lastStrNote,
lastEventStr:eventDesc, level:batteryLevel, offline:isOffline, needs:needsNotice]
}.findAll{ it.needs }
if (reportType=="offline" && reportList.size()==0) {
// No offline devices
def msg = "No offline devices found."
if (note) {
(notice ?: []).each { n -> n.deviceNotification(msg) }
}
return msg
}
// Sorting
reportList = reportList.sort { entry ->
if (reportType=="offline") {
entry.device.displayName.toLowerCase()
} else {
switch(sortByVal) {
case "displayName": return entry.device.displayName.toLowerCase()
case "lastStr": return entry.lastDate ?: new Date(0)
case "level": return (entry.level.toString().isNumber() ? entry.level.toInteger() : -1)
case "lastEventStr": return entry.lastEventStr?.toLowerCase() ?: ""
default: return entry.lastDate ?: new Date(0)
}
}
}
if (sortOrderVal=="desc") reportList = reportList.reverse()
def totalChecked = selectedEnabledDevices.size()
def notReportedCount = reportList.size()
def headerText = reportType=="offline" ? "${notReportedCount} of ${totalChecked} selected device(s) are OFFLINE:\n\n" :
// ----------------------
// HTML Output (Check Now)
// ----------------------
(notReportedCount>0 ? "${notReportedCount} of ${totalChecked} selected device(s) did not report:\n\n" : "All devices reported (${totalChecked} device${totalChecked!=1?'s':''})")
// HTML Output
def headerHtml = headerText.replace("\n\n","<br><br>")
def tableHtml = ""
if (notReportedCount>0) {
tableHtml = "<table border='1' cellpadding='4' cellspacing='0' style='border-collapse:collapse;width:100%'><thead><tr>"
tableHtml += "<th>Last Event Time</th><th>Battery %</th><th>Device Name</th>"
if (reportType=="any") tableHtml += "<th>Event Description</th>"
tableHtml += "</tr></thead><tbody>"
reportList.each { entry ->
def dev = entry.device
tableHtml += "<tr>"
tableHtml += "<td>${entry.lastStrUI}</td><td>${entry.level}%</td><td><a href='/device/edit/${dev.id}' target='_blank'>${dev.displayName}</a></td>"
if (reportType=="any") tableHtml += "<td>${entry.lastEventStr}</td>"
tableHtml += "</tr>"
}
tableHtml += "</tbody></table>"
}
// ----------------------
// Plain Text Output (Notifications)
// ----------------------
def rows = []
if (notReportedCount>0) {
// Build header
def header = "Last Event Time Battery % Device Name"
if (reportType=="any") header += " Event Description"
rows << header
rows << "-" * header.size()
// Build rows
reportList.each { entry ->
def row = "${entry.lastStrNote} ${entry.level.toString()}% ${entry.device}"
if (reportType=="any") row += " ${entry.lastEventStr}"
rows << row
}
}
def plainMsg = headerText + (rows ? "\n" + rows.join("\n") : "")
// ----------------------
// Decide output
// ----------------------
if (note) {
(notice ?: []).each { n -> n.deviceNotification(plainMsg) }
} else {
return headerHtml + tableHtml
}
}
groovy.lang.MissingMethodException: No signature of method: java.lang.String.call() is applicable for argument types: (java.lang.String, java.lang.String) values: [2025-08-22T10:00:00.000-0700, handlerX]
Possible solutions: wait(), chars(), any(), trim(), grep(), next() on line 146 (method updated)
definition(
name: "Battery Device Status Table NEW",
namespace: "hubitat",
author: "based on 'Battery Notifier' by Bruce Ravenel, modified by John Land via ChatGPT",
description: "Battery Device Status with battery %, event description sorting, and offline reporting",
installOnOpen: true,
category: "Convenience",
iconUrl: "",
iconX2Url: ""
)
/*
PURPOSE: Check the state of battery based devices
FEATURES:
* (2025-08-15) Shows only devices that report a "battery" capability.
* (2025-08-15) Selectively shows devices based on "last battery event" or "last event of any kind".
* (2025-08-15) Configurable reporting interval and reporting time.
* (2025-08-15) Output is in a sortable table for ease of reading.
* (2025-08-15) The parenthetical details in the section headings can be suppressed.
* (2025-08-18) Multiple notification devices supported.
* (2025-08-18) Plain-text table for notification devices (e.g., Pushover, Hubitat)
* (2025-08-18) "[Never]" is red only in "Check Now" view, plain in notifications.
* (2025-08-19) Some more cosmetic changes to the plain text "table" for notification devices (e.g., Pushover, Hubitat)
* (2025-08-19) More cosmetic changes to output tables
* (2025-08-21) Added ability to report only OFFLINE devices
*/
preferences {
page(name: "mainPage")
}
def mainPage() {
dynamicPage(name: "mainPage", title: "Battery Device Status", uninstall: true, install: true) {
// Device selection
def devCount = devs?.findAll { !it.isDisabled() }?.size() ?: 0
def devTitle = "Battery Device Selection"
if (showSectionDetails) devTitle += " (${devCount} selected)"
section(hideable:true, hidden:true, title: devTitle) {
input "devs", "capability.battery",
title: "Select devices (disabled devices will be omitted even if selected)",
submitOnChange:true, multiple:true, required:false
}
// Notification devices
def noticeNames = []
if (notice) {
noticeNames = (notice instanceof Collection) ? notice*.displayName : [notice.displayName]
}
def noticeLabel = noticeNames ? noticeNames.join(", ") : "Select notification device(s)"
def noticeTitle = "Notification Settings"
if (showSectionDetails) noticeTitle += " (${noticeLabel})"
section(hideable:true, hidden:true, title: noticeTitle) {
input "notice", "capability.notification",
title:"Select notification device(s)",
submitOnChange:true, multiple:true, required:true
}
// Report Type, Scheduling & Logging
def reportIntervalLabel = reportIntervalHours ?: 24
def runTimeLabel = runTime ? timeToday(runTime, location.timeZone).format("hh:mm a") : ""
def loggingLabel = enableLogging ? "Enabled" : "Disabled"
def reportTypeLabel = reportType == "any" ? "Last Event (any type)" : (reportType=="offline" ? "Offline Devices Only" : "Last Battery Event Only")
def scheduleTitle = "Report Type, Schedule & Logging"
if (showSectionDetails) {
scheduleTitle += " (Report: ${reportTypeLabel}, Interval: ${reportIntervalLabel}h, Daily check: ${runTimeLabel}, Logging: ${loggingLabel})"
}
section(hideable:true, hidden:true, title: scheduleTitle) {
input "reportType", "enum",
title:"Select which event type to check for reporting",
options:["battery":"Last Battery Event Only","any":"Last Event (any type)","offline":"Offline Devices Only"],
defaultValue:"battery", required:true, submitOnChange:true
input "reportIntervalHours", "number", title:"Reporting interval in hours (default = 24)", defaultValue:24, required:true
input name:"runTime", type:"time", title:"Daily check time", required:true
input "showSectionDetails", "bool", title:"Show extra details in section headers?", defaultValue:true
input "enableLogging", "bool", title:"Enable debug logging?", defaultValue:false
}
// Sort Options
def sortTitle = "Sort Options"
def sortByLabel = "Last Event Time"
def sortOrderLabel = sortOrder == "asc" ? "Ascending" : "Descending"
if (reportType=="offline") {
sortByLabel = "Device Name"
if (showSectionDetails) sortTitle += " (Sort by: ${sortByLabel}, Order: ${sortOrderLabel})"
section(hideable:true, hidden:true, title: sortTitle) {
input "sortOrder","enum",title:"Order",options:["asc":"Ascending","desc":"Descending"], defaultValue:sortOrder ?: "asc", submitOnChange:true
}
} else {
switch(sortBy) {
case "displayName": sortByLabel = "Device Name"; break
case "lastStr": sortByLabel = "Last Event Time"; break
case "level": sortByLabel = "Battery %"; break
case "lastEventStr": sortByLabel = "Event Description"; break
}
if (showSectionDetails) sortTitle += " (Sort by: ${sortByLabel}, Order: ${sortOrderLabel})"
section(hideable:true, hidden:true, title: sortTitle) {
def sortOptions = ["displayName":"Device Name","lastStr":"Last Event Time","level":"Battery %"]
if (reportType == "any") sortOptions["lastEventStr"] = "Event Description"
input "sortBy","enum",title:"Sort by",options:sortOptions, defaultValue:sortBy ?: "lastStr", submitOnChange:true
input "sortOrder","enum",title:"Order",options:["asc":"Ascending","desc":"Descending"], defaultValue:sortOrder ?: "asc", submitOnChange:true
}
}
// Check Now
section(title:"") {
input "check","button", title:"Check Now", state:"check"
if (state.check) {
paragraph handler()
state.check = false
}
}
}
}
def updated() {
unschedule()
unsubscribe()
initialize()
if (enableLogging) log.debug "Updated: ${devs?.size() ?: 0} devices"
}
def installed() { initialize() }
void initialize() {
unschedule()
if (enableLogging) log.debug "Initializing Battery Device Status..."
// if (runTime) schedule(runTime, handlerX)
if (runTime) {
def t = timeToday(runTime, location.timeZone)
def hours = t.format("H", location.timeZone)
def minutes = t.format("m", location.timeZone)
def cronStr = "0 ${minutes} ${hours} * * ?"
if (enableLogging) log.debug "Scheduled daily check at ${t.format("hh:mm a", location.timeZone)} (cron: ${cronStr})"
//schedule("${cronStr}", "handlerX")
//schedule(cronStr.toString(), "handlerX")
//schedule(cronStr, "handlerX")
//schedule(runTime, "handlerX")
schedule(runTime, handlerX)
}
if (enableLogging) log.debug "Initialized with ${devs?.size() ?: 0} devices"
}
void handlerX() {
if (enableLogging) log.debug "Scheduled check running..."
handler(true)
}
// ----------------------
// Main Handler
// ----------------------
String handler(note=false) {
def sortByVal = settings.sortBy ?: "lastStr"
def sortOrderVal = settings.sortOrder ?: "asc"
def selectedEnabledDevices = (devs ?: []).findAll{!it.isDisabled()}
if (!selectedEnabledDevices) return "No battery devices found."
def rightNow = new Date()
// Collect device info
def reportList = selectedEnabledDevices.collect { dev ->
def lastEvent = (reportType=="any") ? dev.events(max:1)?.find{true} : dev.events(max:50)?.find{ it.name=="battery" }
def lastEventDate = lastEvent?.date
def eventDesc = (reportType=="any" && lastEvent) ? "(Event: ${lastEvent.name} ${lastEvent.value})" : ""
def fs = '\u2007' // figure space
def batteryLevel = dev.currentBattery != null ? Math.round(dev.currentBattery).toString().padLeft(3, fs) : "N/A".padLeft(3, fs)
// Offline detection using getStatus() and currentHealthStatus
def devStatus = dev.getStatus()?.toUpperCase()
def isOffline = devStatus in ["OFFLINE", "INACTIVE", "NOT PRESENT"] || (dev.currentHealthStatus?.toLowerCase() == "offline")
// Determine if device should be included in report
def needsNotice = (reportType=="offline" ? isOffline : !lastEventDate || ((rightNow.time - lastEventDate.time)/60000) > ((reportIntervalHours ?: 24) * 60))
String lastStrUI = lastEventDate ? lastEventDate.format("yyyy-MM-dd hh:mm a", location.timeZone) : "<span style='color:red;'>[Never]</span>"
String lastStrNote = lastEventDate ? lastEventDate.format("yyyy-MM-dd hh:mm a", location.timeZone) : "0000-00-00 00:00 xx"
[device:dev, lastDate:lastEventDate, lastStrUI:lastStrUI, lastStrNote:lastStrNote,
lastEventStr:eventDesc, level:batteryLevel, offline:isOffline, needs:needsNotice]
}.findAll{ it.needs }
if (reportType=="offline" && reportList.size()==0) {
// No offline devices
def msg = "No offline devices found."
if (note) {
(notice ?: []).each { n -> n.deviceNotification(msg) }
}
return msg
}
// Sorting
reportList = reportList.sort { entry ->
if (reportType=="offline") {
entry.device.displayName.toLowerCase()
} else {
switch(sortByVal) {
case "displayName": return entry.device.displayName.toLowerCase()
case "lastStr": return entry.lastDate ?: new Date(0)
case "level": return (entry.level.toString().isNumber() ? entry.level.toInteger() : -1)
case "lastEventStr": return entry.lastEventStr?.toLowerCase() ?: ""
default: return entry.lastDate ?: new Date(0)
}
}
}
if (sortOrderVal=="desc") reportList = reportList.reverse()
def totalChecked = selectedEnabledDevices.size()
def notReportedCount = reportList.size()
def headerText = reportType=="offline" ? "${notReportedCount} of ${totalChecked} selected device(s) are OFFLINE:\n\n" :
// ----------------------
// HTML Output (Check Now)
// ----------------------
(notReportedCount>0 ? "${notReportedCount} of ${totalChecked} selected device(s) did not report:\n\n" : "All devices reported (${totalChecked} device${totalChecked!=1?'s':''})")
// HTML Output
def headerHtml = headerText.replace("\n\n","<br><br>")
def tableHtml = ""
if (notReportedCount>0) {
tableHtml = "<table border='1' cellpadding='4' cellspacing='0' style='border-collapse:collapse;width:100%'><thead><tr>"
tableHtml += "<th>Last Event Time</th><th>Battery %</th><th>Device Name</th>"
if (reportType=="any") tableHtml += "<th>Event Description</th>"
tableHtml += "</tr></thead><tbody>"
reportList.each { entry ->
def dev = entry.device
tableHtml += "<tr>"
tableHtml += "<td>${entry.lastStrUI}</td><td>${entry.level}%</td><td><a href='/device/edit/${dev.id}' target='_blank'>${dev.displayName}</a></td>"
if (reportType=="any") tableHtml += "<td>${entry.lastEventStr}</td>"
tableHtml += "</tr>"
}
tableHtml += "</tbody></table>"
}
// ----------------------
// Plain Text Output (Notifications)
// ----------------------
def rows = []
if (notReportedCount>0) {
// Build header
def header = "Last Event Time Battery % Device Name"
if (reportType=="any") header += " Event Description"
rows << header
rows << "-" * header.size()
// Build rows
reportList.each { entry ->
def row = "${entry.lastStrNote} ${entry.level.toString()}% ${entry.device}"
if (reportType=="any") row += " ${entry.lastEventStr}"
rows << row
}
}
def plainMsg = headerText + (rows ? "\n" + rows.join("\n") : "")
// ----------------------
// Decide output
// ----------------------
if (note) {
(notice ?: []).each { n -> n.deviceNotification(plainMsg) }
} else {
return headerHtml + tableHtml
}
}