I have a need to use the date / time of a scheduled job in a piston. What’s the best way to accomplish this?
+620ms
║Setting up scheduled job for Sat, Jul 18 2026 @ 7:10:00 AM EDT (in 599987ms), with 4 more jobs pending
I have a need to use the date / time of a scheduled job in a piston. What’s the best way to accomplish this?
+620ms
║Setting up scheduled job for Sat, Jul 18 2026 @ 7:10:00 AM EDT (in 599987ms), with 4 more jobs pending
That is an issue in apps code in general, not just Webcore. Hubitat has given no access to any method in the underlying Quartz Scheduler, to pull anything out of it using some sort of getScheduledJobs() method.
So, Webcore has no access to that either. Every time a job runs, you need to store the next run time in a variable. This will get tricky if it is a timer schedule with restrictions for certain days or months, as to calculate the next run time you will have to take any restrictions into account. Use the webcore time variables when checking restrictions, and the time functions in expressions for calculating the next run times to store.
Edit: I just wrote a piston to store next run time for a daily timer that runs on Mondays and Saturdays, but restrictions get ugly fast. You have to progress the timer through the week and check if the day restriction applies:
I had AI write this driver below, it got it correct the first try.
The idea is to add it to device code and create a virtual device. Add that device to Webcore, and then when a schedule fires, you use the device and the cron string that matches your schedule (AI can give you that), use the setCronString command from webcore to set the cron, then wait one second, then set a variable based on the value of the device attribute "nextRunTime"
metadata {
definition (name: "Pure Groovy Cron Parser", namespace: "utility", author: "Utility") {
capability "Actuator"
capability "Sensor"
command "setCronString", [[name: "Cron String*", type: "STRING", description: "Quartz Cron format (e.g., 0 0/15 * * * ?)"]]
attribute "nextRunTime", "String"
}
}
def setCronString(String cronString) {
if (!cronString) return
try {
// Step 1: Clean up and split the Cron string into Quartz components
def parts = cronString.trim().split(/\s+/)
if (parts.size() < 6 || parts.size() > 7) {
throw new Exception("Quartz cron must have 6 or 7 fields.")
}
// Map individual fields
def cronSec = parts[0]
def cronMin = parts[1]
def cronHour = parts[2]
def cronDom = parts[3] // Day of Month
def cronMonth = parts[4]
def cronDow = parts[5] // Day of Week
def cronYear = parts.size() == 7 ? parts[6] : "*"
// Step 2: Grab the current time using the hub's local timezone
Calendar cal = Calendar.getInstance(location.timeZone)
cal.set(Calendar.MILLISECOND, 0)
// Move ahead 1 second so we look for a strictly *future* execution time
cal.add(Calendar.SECOND, 1)
// Step 3: Brute-force calendar evaluation loop (Max limit 5 years out)
long endTimeout = cal.timeInMillis + (5L * 365 * 24 * 60 * 60 * 1000)
boolean matchFound = false
while (cal.timeInMillis < endTimeout) {
// Check Year
if (!matchField(cronYear, cal.get(Calendar.YEAR))) {
cal.add(Calendar.YEAR, 1)
cal.set(Calendar.MONTH, 0)
cal.set(Calendar.DAY_OF_MONTH, 1)
cal.set(Calendar.HOUR_OF_DAY, 0)
cal.set(Calendar.MINUTE, 0)
cal.set(Calendar.SECOND, 0)
continue
}
// Check Month (Calendar.MONTH is 0-indexed, Quartz is 1-indexed)
if (!matchField(cronMonth, cal.get(Calendar.MONTH) + 1, ["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"])) {
cal.add(Calendar.MONTH, 1)
cal.set(Calendar.DAY_OF_MONTH, 1)
cal.set(Calendar.HOUR_OF_DAY, 0)
cal.set(Calendar.MINUTE, 0)
cal.set(Calendar.SECOND, 0)
continue
}
// Check Day of Month and Day of Week rules
int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH)
int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK) // 1=Sunday, 7=Saturday
// Handle Quartz specific '?' vs '*' exclusions
boolean domCheck = (cronDom == "?") || matchField(cronDom, dayOfMonth)
boolean dowCheck = (cronDow == "?") || matchField(cronDow, dayOfWeek, ["SUN","MON","TUE","WED","THU","FRI","SAT"])
if (!domCheck || !dowCheck) {
cal.add(Calendar.DAY_OF_MONTH, 1)
cal.set(Calendar.HOUR_OF_DAY, 0)
cal.set(Calendar.MINUTE, 0)
cal.set(Calendar.SECOND, 0)
continue
}
// Check Hour
if (!matchField(cronHour, cal.get(Calendar.HOUR_OF_DAY))) {
cal.add(Calendar.HOUR_OF_DAY, 1)
cal.set(Calendar.MINUTE, 0)
cal.set(Calendar.SECOND, 0)
continue
}
// Check Minute
if (!matchField(cronMin, cal.get(Calendar.MINUTE))) {
cal.add(Calendar.MINUTE, 1)
cal.set(Calendar.SECOND, 0)
continue
}
// Check Second
if (!matchField(cronSec, cal.get(Calendar.SECOND))) {
cal.add(Calendar.SECOND, 1)
continue
}
// If it rolls all the way through, we found a perfect time match!
matchFound = true
break
}
if (matchFound) {
def formattedDate = cal.time.format("yyyy-MM-dd HH:mm:ss z", location.timeZone)
sendEvent(name: "nextRunTime", value: formattedDate)
log.info "Parsed successfully! Next Run Time: ${formattedDate}"
} else {
sendEvent(name: "nextRunTime", value: "No match within 5 years")
}
} catch (Exception e) {
log.error "Cron parsing failed: ${e.message}"
sendEvent(name: "nextRunTime", value: "Invalid/Unsupported Expression")
}
}
// Helper method to evaluate structural patterns (*, increments, ranges, lists, names)
boolean matchField(String expression, int value, List aliases = []) {
String exp = expression.toUpperCase().trim()
if (exp == "*" || exp == "?") return true
// Convert named components (like "MON" or "JAN") to their integer values
aliases.eachWithIndex { name, index ->
exp = exp.replace(name, "${index + 1}")
}
// Handle comma-separated lists (e.g., "1,3,5")
if (exp.contains(",")) {
return exp.split(",").any { matchField(it, value, aliases) }
}
// Handle increments (e.g., "0/15" or "*/5")
if (exp.contains("/")) {
def parts = exp.split("/")
int start = parts[0] == "*" ? 0 : parts[0].toInteger()
int step = parts[1].toInteger()
return (value >= start) && ((value - start) % step == 0)
}
// Handle ranges (e.g., "10-15")
if (exp.contains("-")) {
def parts = exp.split("-")
int rangeStart = parts[0].toInteger()
int rangeEnd = parts[1].toInteger()
return (value >= rangeStart && value <= rangeEnd)
}
// Handle a single specific number string
if (exp.isInteger()) {
return exp.toInteger() == value
}
return false
}
Get the cron string from Google by describing the schedule.