Very frustrating issue with Groovy code

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
    }
}

Perhaps I am not following completely but it looks like you have a problem on the line below.

		schedule(runTime, handlerX)

In the Error you got for whatever reason it looks like schedule was being called with a timestamp for the variable runTime instead of a cron expression as is expected based on Hubitat documentation.

Now that said if you are saying all you did was copy and paste it into a different app window and it saved then i don't know why that would work.

3 Likes

Adding to the above, the handler method needs to accept an "optional" Map as a parameter. I suspect Groovy will treat your signature as taking an Object instead, which should also work, but for your own sake, the data would probably be easier to work with if the type were consistent (example also in the docs linked to above).

The documented method of specifying the handler name is also as a String, though the platform will normally convert this to one for you if you just provide the method name like an object, so that's probably not related (but I'd go back to that code, which it looks like you commented out).

Neither of these is probably the issue, but fixing either can't hurt. :slight_smile:

3 Likes

Like @mavrrick58 said schedule() is expecting a cron expression. My guess is that the data changed. Looks like runTime is a preference setting which would need to be re-entered when you recreated the app, what did you enter in there the second time (if nothing then the if block wouldn't have executed)?

3 Likes

Actually after looking at the code a bit more, it does look like there is code to try to derive the cron expression, but the variable passed when calling "schedule" is the input value. Try changing your first variable in the schedule call to "cronStr" and see if that fixes it.

1 Like

It's not a cron expression issue.

I literally tried about 10 different ways to write the schedule line, 6 of which are shown below (copied from the OLD code I posted, with comment slashes removed). Other variants I tried were with handlerX not in quotes (like the last example below).

  • schedule("${cronStr}", "handlerX")
  • schedule(cronStr.toString(), "handlerX")
  • schedule(cronStr, "handlerX")
  • schedule(runTime, "handlerX")
  • schedule(runTime, handlerX)

Note that the last line [schedule(runTime, handlerX)] works in the new code, AND in my prior working version of the code that the buggy code was based on, AND is the original code that I borrowed from the app 'Battery Notifier' by Bruce Ravenel.

And yes, ALL I did was copy and paste the old code into a different new app window and saved it after changing "OLD" to "NEW" in line 2. I still have both versions and the OLD version sill throws the error re the schedule method, the NEW version does not.

BTW, as a sanity check, I just copied and pasted the OLD code into a text editor (PSPad) and saved it to disk. I did the same for the NEW code. I then used Beyond Compare to do a line-by-line comparison of the 2 files. The ONLY differences are in line 2, "OLD" vs "NEW" in the name of the apps.

Is the data you posted above the complete driver code. I can't even save it on my hub at all now that i am trying.

The driver says modified with chatGPT. What was modified?

Here is an example of using Schedule from my code. You can also look at the documentation I linked above.

        String pollCron = '0 */'+pollRate+' * ? * *' 
        schedule(pollCron, poll)

Does the code you have that saved and didn't generate that error even work?

2 Likes

I'd suggest adding some logging to your code to see what is happening when. In particular, enabling the your own logging setting or just removing the if (or adding a new log entry just for debugging) that will show you the actual cron string would give you a better idea of what's happening. In the error you showed above, it was defintiely in the wrong format, though the reason why is clear from that particular code, and some of the others you provided should have been right. So, doing things like this will give you (and us) a better idea of what is happening when, what variables are set to at key times, etc.

2 Likes

It is an app, not a driver, and yes, it's the complete code.

I started with the 'Battery Notifier' app by Bruce Ravenel, then used ChatGPT to add a lot of features, such as collapsible sections for device selection and for options, and tables for output. As I said, a prior version worked just fine, as does the NEW copy of the OLD buggy version.

Understood; Bruce Ravenel used similar code: schedule("0 30 9 ? * * *", handlerX). However, the OLD buggy version would not work even with a constructed cron string.

Yes, it works perfectly.

My bad. A few things i saw pushed me toward it being a driver. Sorry for the confusion.

It may be that as you were working on the code something got jumbled in the compiler that needed to be reset.

I have found a few occasions were cron string doesn't mean the same to all systems. There are slight variations i have found. Without troubleshooting the original code it is hard to say why it did or didn't work. That said, as you can see the format I am using and the format you are using now is the same for the schedule() call. The question is perhaps something about how the time input value is being stored allows it to be directly input into the schedule call. It does clearly work with the timestamp value. I modified the logging line so I could see exactly what "runTime" was prior to the call. This is what was shown in the log

Scheduled daily check at runTime: 2025-08-22T18:34:00.000-0400 :06:34 PM (cron: 0 34 18 * * ?)

My guess is that with all of the stuff you were doing something got junked up in the compiler. Or somehow something was changed that you just are not seeing.

1 Like

Which is not unlikely, but a bit scary that the compiler can behave differently when processing the same plain text -- which led to hours of wasted time on my part.

Can't be, unless changing the word "OLD" to "NEW" in the name of the app could possibly have an effect.

BTW, I replicated the problem this morning. I have a working app now, but left that alone. As a test, I copied and pasted the OLD buggy app into a new app window, changed "OLD" to "NEW" in the name, and saved. The OLD app throws an error and will not schedule, the NEW app does not throw an error and does schedule. So it's not a one-off thing.

Thank you for the suggestions! I'm still on the beginning part of the Groovy learning curve. That part of the code was just copied from the "Battery Notifier" app from Bruce Ravenel (see snippet below), and since it worked, I had no clue that it might not be strictly compliant.

String handler(note=false) {
	String s = ""
	def rightNow = new Date()
	devs.each {
		def lastTime = it.events(max: 1).date
		if(lastTime) {
			def minutes = ((rightNow.time - lastTime.time) / 60000).toInteger()
			if(minutes < 0) minutes += 1440
			if(minutes > 1440) s += (note ? "" : "<a href='/device/edit/$it.deviceId' target='_blank'>") + "$it.displayName, "
		} else {
			s += (note ? "" : "<a href='/device/edit/$it.deviceId' target='_blank'>") + "$it.displayName, " + (note ? "" : "</a>")
		}
	}
	if(note) notice.deviceNotification(s ? "[H]${s[0..-3]} did not report" : "All devices reported")
	else return s ? "${s[0..-3]} did not report" : "All devices reported"
}

As part of my hour's long struggle with the buggy version, I did exactly what you suggest, and the cron string appeared in the log to be in a proper format. I removed that debugging code near the end of my efforts as being unnecessary.

I'm struggling to make it fail.... trying various combinations....

For an instance of an App that is producing the error:

  • I assume it is when you click Done in the App that the error is being produced?
  • Can you show a screenshot of the App Info page to see what values are being recorded and any scheduled jobs

Also, are you by any chance working on a Mac? Just wondering if some special characters could be somehow messing with it.... Bit of a long shot.

Also, what platform version are you running? Again, highly unlikely to be the issue...

You probably CAN'T make the posted app (either the OLD or NEW versions) fail if you just copy the posted code and paste it into a new app window -- that's kind of my point. That's how I fixed the problem: after trying to debug the specific instance of the OLD version (which was based on a prior working version), I ultimately copied and pasted the code into a new app window (thus getting a new app ID) and everything just worked. So something in Hubitat just did not like that old app ID and threw the Java error I posted (and others like it) all relating to the single schedule line of code.

I just ran the OLD version and here's the log:

For the OLD version, here's the app info page:

I just ran the NEW version and here's the log:

For the NEW version, here's the app info page:

Comparing the info pages side by side, there may be a clue: the OLD version has table entries called "logging" and "schedule" that the NEW version lacks -- but that might be because the OLD version fails at the schedule line.

I'm on a Windows 10 PC. My Hubitat environment is a C-8 Pro with 2.4.2.152 firmware.

Hmmm... Then I'm as stumped as anyone... bizarre...

Bingo!!

I added a test input called schedule, admittedly with a different type than ENUM (I just copied the runTime one). When I tried hitting Done, I got the error above.

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 name:"schedule", type:"time", title:"Testing schedule preference setting", required:true
            
            input "showSectionDetails", "bool", title:"Show extra details in section headers?", defaultValue:true

            input "enableLogging", "bool", title:"Enable debug logging?", defaultValue:false
        }

Can only suspect you had at one point in time included a switch to allow you to turn the schedule on and off? With that setting persisting in your old App instance, but not being present in any new one's created with code that did not include this. Even if I removed the input from the failing code, the setting remained in the background and continued attempts to schedule the checks in the affected app produced an error.

5 Likes

Thank you! That seems to be the answer!

I did not ever write a section called "schedule" BUT I was using ChatGPT to go through many iterations as I tweaked different features and UI appearances I wanted, so it looks like there was such a section at one time.

So the bug is that Hubitat does not always act on just the code text as currently displayed and can fail to clear out deleted variable names that conflict with a method name.

Again, thank you for your time and effort in helping me understand this problem!

3 Likes

Personally I'd put this in a relatively grey area on whether to label it a bug, preferring to not call it a bug myself.

Inputs can be dynamic in whether they are actually displayed to the user, so it would be hard to say that "if it is not displayed to the user it should no longer exist (be deleted) in the app settings". Similarly, there is no place I can think of to trigger the removal of the setting in apps that use the code if / when it is removed from the code. I would prefer to have that control myself as the developer, so that I could manage any other activities that may need to happen at the same time as when the setting is removed from within the code. I did try to find how this can be done, as I'm pretty sure it can be, but could not find it...

The conflict in naming is one that could perhaps be handled through some education / documentation, but it would be a hard ask to expect all the possible reserved words could be covered off, or that people (developers) could make the association with that error and look for information on any reserved words. Also, this conflict only became a problem due to the use of the scheduling option, which may not be the case for all pieces of code, but I will admit avoiding the name altogether would stop it from being a problem in any circumstance.

All that said, I get it is frustrating, particularly when you are trying to learn. That is why it is good that you chose to post about it here, rather than continue to bang your head against the figurative brick wall.

5 Likes