Yet another battery status app...
This one is based on 'Battery Notifier' by Bruce Ravenel, but heavily modified as a vehicle for me learning a bit about Groovy and ChatGPT (which admittedly did all the detail work).
Main features:
- Shows only devices that report a "battery" capability.
- Selectively shows devices based on low/critical battery, offline devices, last event (any type), last battery event.
- Configurable reporting interval and reporting time.
- Output is in a sortable table for ease of reading.
- Parenthetical details in section headings can be suppressed.
- Multiple notification devices supported.
- Plain-text table for notifications (e.g., Pushover, Hubitat)
- Last run date displayed at bottom of UI.
- Configurable low battery warning and critical battery level.
- Users can select one or more reports via checkboxes.
- Each report has its own Sort By & Order.
- Tables automatically show when the app is opened.
2025-08-18 FIXES:
- Plain-text table for notification devices (e.g., Pushover, Hubitat)
- "[Never]" is red only in "Check Now" view, plain in notifications.
2025-08-19 FIXES:
- Some more cosmetic changes to the plain text "table" for notification devices
- More cosmetic changes to output tables
2025-08-22 Update
- Added "All Tables" option (sequentially outputs Any, Offline, Battery reports)
- Independent sorting per section when "All Tables" is selected
2025-09-05 Update
- Added a battery level table, configurable as to "low" and "critical" levels.
2025-09-06 Update: Fixed a bug that omitted reports when sent to a single notification device.
2025-09-19 Update: Cosmetic changes, and made the selected tables auto-generate when the app opens.
2025-10-24 Update: More cosmetic changes, and added a "Send Report Now" button.
CAVEAT: This is only my 2nd Groovy app, I'm a dabbler, not a professional programmer, and I have a VERY incomplete understanding of Groovy and the Hubitat API. Accordingly, constructive comments/critiques are welcome.
BTW, I tried (actually, ChatGPT tried multiple times) to get the table be sortable by clicking on the table column headings, but that didn't work at all. Any pointers on how that might be accomplished in the Groovy/Hubitat context would be appreciated.
Sample "Refresh Table" output:
The app code is below. Consider this to be a BETA app and use at your peril.
/*
PURPOSE: Check the state of battery based devices
FEATURES:
* Shows only devices that report a "battery" capability.
* Selectively shows devices based on low/critical battery, offline devices, last event (any type), last battery event, and last activity.
* Configurable reporting interval and reporting time.
* Output is in a sortable table for ease of reading.
* Parenthetical details in section headings can be suppressed.
* Multiple notification devices supported.
* Plain-text table for notifications (e.g., Pushover, Hubitat)
* Last run date displayed at bottom of UI.
* Configurable low battery warning and critical battery level.
* Users can select one or more reports via checkboxes.
* Each report has its own Sort By & Order.
* Tables automatically show when the app is opened.
*/
definition(
name: "Battery Device Status 1.14",
namespace: "John Land",
author: "John Land via ChatGPT",
description: "Battery Device Status with battery %, offline/low battery reporting, last activity, configurable sort options",
installOnOpen: true,
category: "Convenience",
iconUrl: "",
iconX2Url: ""
)
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 = notice ? ((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 activityIntervalLabel = activityIntervalHours ?: 24
def runTimeLabel = runTime ? timeToday(runTime, location.timeZone).format("hh:mm a") : ""
def loggingLabel = enableLogging ? "Enabled" : "Disabled"
def lowBattLabel = lowBatteryLevel ?: 80
def critBattLabel = criticalBatteryLevel ?: 60
def settingsTitle = "Report Type, Schedule & Logging"
if (showSectionDetails) {
settingsTitle += " (Daily Check: ${runTimeLabel}, Overdue Event Interval: ${reportIntervalLabel}h, Overdue Activity Interval: ${activityIntervalLabel}h, Battery Low = ${lowBattLabel}%, Battery Critical = ${critBattLabel}%, Debug Logging: ${loggingLabel})"
}
section(hideable:true, hidden:true, title: settingsTitle) {
//section(hideable:true, title: settingsTitle) {
// Report options
def reportOptions = [
"offline":"Offline Devices",
"low":"Low Battery Devices",
"battery":"Last Battery Event",
"any":"Last Event (any type)",
"activity":"Last Activity"
]
def selected = reportTables ?: []
def expanded = selected
// Handle "Select All Reports"
if (selected.contains("all")) {
expanded = reportOptions.keySet() as List
app.updateSetting("reportTables", [value: expanded, type: "enum"])
}
input "reportTables", "enum",
title:"Select which report tables to generate",
options:["all":"Select All Reports"] + reportOptions,
multiple:true,
value: expanded,
submitOnChange:true,
required:true
input name:"runTime", type:"time", title:"Daily check time", required:true
input "reportIntervalHours", "number", title:"Overdue event interval in hours (default = 24)", defaultValue:24, required:true
input "activityIntervalHours", "number", title:"Overdue activity interval in hours (default = 24)", defaultValue:24, required:true
input "lowBatteryLevel", "number", title:"Low battery warning level (%)", defaultValue:80, required:true
input "criticalBatteryLevel", "number", title:"Critically low battery level (%)", defaultValue:60, 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
section(hideable:true, hidden:true, title:"Sort Options") {
//section(hideable:true, title:"Sort Options") {
paragraph("<b>Offline Devices</b>")
input "sortBy_offline", "enum",
title:"Sort by",
options:["displayName":"Device Name"],
defaultValue:"displayName",
submitOnChange:true
input "sortOrder_offline", "enum",
title:"Order",
options:["asc":"Ascending","desc":"Descending"],
defaultValue:"asc",
submitOnChange:true
paragraph("<hr>")
paragraph("<b>Low Battery Devices</b>")
input "sortBy_low", "enum",
title:"Sort by",
options:["level":"Battery %","displayName":"Device Name"],
defaultValue:"level",
submitOnChange:true
input "sortOrder_low", "enum",
title:"Order",
options:["asc":"Ascending","desc":"Descending"],
defaultValue:"asc",
submitOnChange:true
paragraph("<hr>")
paragraph("<b>Last Battery Event</b>")
input "sortBy_battery", "enum",
title:"Sort by",
options:["lastStr":"Last Event Time","displayName":"Device Name"],
defaultValue:"lastStr",
submitOnChange:true
input "sortOrder_battery", "enum",
title:"Order",
options:["asc":"Ascending","desc":"Descending"],
defaultValue:"desc",
submitOnChange:true
paragraph("<hr>")
paragraph("<b>Last Event (any type)</b>")
input "sortBy_any", "enum",
title:"Sort by",
options:["lastStr":"Last Event Time","displayName":"Device Name","lastEventStr":"Event Description"],
defaultValue:"lastStr",
submitOnChange:true
input "sortOrder_any", "enum",
title:"Order",
options:["asc":"Ascending","desc":"Descending"],
defaultValue:"desc",
submitOnChange:true
paragraph("<hr>")
paragraph("<b>Last Activity</b>")
input "sortBy_activity", "enum",
title:"Sort by",
options:["lastActivity":"Last Activity Time","displayName":"Device Name","level":"Battery %"],
defaultValue:"lastActivity",
submitOnChange:true
input "sortOrder_activity", "enum",
title:"Order",
options:["asc":"Ascending","desc":"Descending"],
defaultValue:"desc",
submitOnChange:true
paragraph("<hr>")
}
// refresh Now + auto-run on load
section(title:"") {
input "refresh","button", title:"Refresh Table"
input "sendNow","button", title:"Send Report Now"
paragraph handler(sendNotifications=false) // show tables only
}
}
}
// Lifecycle
def installed() { initialize() }
def updated() {
unschedule()
unsubscribe()
initialize()
if (enableLogging) log.debug "Battery Device Status updated with ${devs?.size() ?: 0} devices"
}
void initialize() {
unschedule()
if (enableLogging) log.debug "Battery Device Status initializing ..."
if (runTime) schedule(runTime, handlerX)
log.info "Battery Device Status initialized with ${devs?.size() ?: 0} devices"
}
void handlerX() {
state.lastRun = new Date().format("yyyy-MM-dd hh:mm a", location.timeZone)
def noticeNames = notice ? ((notice instanceof Collection) ? notice*.displayName : [notice.displayName]) : []
log.info "Battery Device Status scheduled check running at ${state.lastRun}, notifying: ${noticeNames.join(', ')}"
handler(sendNotifications=true)
}
def appButtonHandler(btn) {
switch (btn) {
case "refresh":
if (enableLogging) log.debug "Manual refresh requested"
handler(sendNotifications=false) // Show only
break
case "sendNow":
if (enableLogging) log.debug "Immediate report send requested"
handler(sendNotifications=true) // Send notifications now
break
default:
if (enableLogging) log.debug "Unknown button: ${btn}"
break
}
}
// Main Handler
String handler(sendNotifications=false) {
state.lastRun = new Date().format("yyyy-MM-dd hh:mm a", location.timeZone)
def htmlOut = ""
def htmlOrder = ["offline","low","battery","any","activity"]
def allReportTypes = ["battery","any","offline","low","activity"]
def selectedReports = reportTables ?: allReportTypes // never contains 'all'
def reportLabels = [
"battery":"Last Battery Event",
"any":"Last Event (any type)",
"offline":"Offline Devices",
"low":"Low Battery Devices",
"activity":"Last Activity"
]
def results = []
selectedReports.each { type ->
def result = (type == "low") ? generateLowBatteryTable(sendNotifications) :
(type == "activity") ? generateActivityTable(sendNotifications) :
generateReport(type, sendNotifications)
results << [type:type, label:reportLabels[type], html:result.html, plain:result.plain]
}
htmlOrder.each { orderedType ->
def res = results.find { it.type == orderedType }
if (res) {
htmlOut += "<h3>${res.label}</h3>${res.html}<hr><br>"
}
}
if (!sendNotifications && state.lastRun) {
htmlOut += "<br><small><i>Last run: ${state.lastRun}</i></small>"
}
if (sendNotifications) {
def devices = notice instanceof Collection ? notice : (notice ? [notice] : [])
def plainOrder = htmlOrder.reverse() // <<< reverse the HTML order
// Send notifications (both scheduled and "Send Report Now") with staggered timing
plainOrder.eachWithIndex { orderedType, idx ->
def res = results.find { it.type == orderedType }
if (!res) return
def msg = res.plain?.trim() ?: "${res.label}: No issues found at ${state.lastRun}."
msg = "=== ${res.label} ===\n${msg}"
// Stagger each message by 5 seconds to preserve order
runIn(idx * 5, "sendDelayedNotification", [overwrite: false, data:[msg: msg, deviceIds: devices*.id]])
}
}
return htmlOut
}
// Helper to send delayed notifications
void sendDelayedNotification(Map data) {
def msg = data.msg
def deviceIds = data.deviceIds ?: []
def devices = deviceIds.collect { id -> notice.find { it.id == id } }.findAll { it }
devices.each { n ->
try {
n.deviceNotification(msg)
} catch (e) {
log.error "Failed to notify ${n}: ${e}"
}
}
}
// Report Generator (last event/battery/offline/any)
private Map generateReport(String type, boolean noteMode) {
def sortByVal = settings["sortBy_${type}"] ?: ((type=="offline") ? "displayName" : "lastStr")
def sortOrderVal = settings["sortOrder_${type}"] ?: "asc"
def selectedEnabledDevices = (devs ?: []).findAll{ !it.isDisabled() }
if (!selectedEnabledDevices) return [label:type, html:"No battery devices found.", plain:"No battery devices found."]
def rightNow = new Date()
def reportList = selectedEnabledDevices.collect { dev ->
// ? Fixed: "battery" type looks for battery events, "any" looks for any event
def lastEvent
if (type == "battery") {
// For "battery" type, find last battery event
lastEvent = dev.events(max:50)?.find{ it.name == "battery" }
} else if (type == "any") {
// For "any" type, find the most recent event of any type
lastEvent = dev.events(max:1)?.find{ true }
} else if (type == "offline") {
// For offline, we still check battery events for the timestamp
lastEvent = dev.events(max:50)?.find{ it.name == "battery" }
}
def lastEventDate = lastEvent?.date
def eventDesc = (type == "any" && lastEvent) ? "(Event: ${lastEvent.name} ${lastEvent.value})" : ""
def fs = '\u2007'
def batteryLevel = dev.currentBattery != null ? Math.round(dev.currentBattery).toString().padLeft(3, fs) : "N/A".padLeft(3, fs)
def devStatus = dev.getStatus()?.toUpperCase()
def isOffline = devStatus in ["OFFLINE","INACTIVE","NOT PRESENT"] || (dev.currentHealthStatus?.toLowerCase() == "offline")
def needsNotice = (type == "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 }
// Sorting for generateReport
def neverList = reportList.findAll { it.lastDate == null }
def normalList = reportList.findAll { it.lastDate != null }
// Sort "Never" group alphabetically by device name (always asc)
neverList = neverList.sort { it.device.displayName.toLowerCase() }
// Sort normal group by selected sort key
normalList = normalList.sort { it ->
switch(sortByVal) {
case "displayName": return it.device.displayName.toLowerCase()
case "lastStr": return it.lastDate
case "level": return (it.level.toString().isNumber() ? it.level.toInteger() : -1)
case "lastEventStr":return it.lastEventStr?.toLowerCase() ?: ""
default: return it.lastDate
}
}
if (sortOrderVal == "desc") normalList = normalList.reverse()
// Combine: [Never] group always first
reportList = neverList + normalList
def totalChecked = selectedEnabledDevices.size()
def notReportedCount = reportList.size()
// ? Fixed: Update header text based on report type
def eventThresholdHours = (settings["reportIntervalHours"] ?: 24) as int
def headerText
if (type == "offline") {
headerText = "${notReportedCount} of ${totalChecked} selected devices report as being \"OFFLINE\".\n\n"
} else if (type == "battery") {
headerText = notReportedCount > 0 ?
"${notReportedCount} of ${totalChecked} selected devices did not report a \"last battery event\".\n\n" :
"${totalChecked!=1?'All':'The'} ${totalChecked} selected device${totalChecked!=1?'s':''} reported battery events within ${eventThresholdHours}h."
} else if (type == "any") {
headerText = notReportedCount > 0 ?
"${notReportedCount} of ${totalChecked} selected devices did not report any event.\n\n" :
"${totalChecked!=1?'All':'The'} ${totalChecked} selected device${totalChecked!=1?'s':''} reported events within ${eventThresholdHours}h."
}
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 (type=="any") tableHtml += "<th>Event Description</th>"
tableHtml += "</tr></thead><tbody>"
reportList.each { it ->
tableHtml += "<tr>"
tableHtml += "<td>${it.lastStrUI}</td><td>${it.level}%</td><td><a href='/device/edit/${it.device.id}' target='_blank'>${it.device.displayName}</a></td>"
if (type=="any") tableHtml += "<td>${it.lastEventStr}</td>"
tableHtml += "</tr>"
}
tableHtml += "</tbody></table>"
}
def rows = []
if (notReportedCount>0) {
def header = "Last Event Time Battery % Device Name"
if (type=="any") header += " Event Description"
rows << header
rows << "-" * header.size()
reportList.each { it ->
def row = "${it.lastStrNote} ${it.level.toString()}% ${it.device}"
if (type=="any") row += " ${it.lastEventStr}"
rows << row
}
}
def plainMsg = headerText + (rows ? "\n" + rows.join("\n") : "")
return [label:type, html: headerHtml + tableHtml, plain: plainMsg]
}
// Low Battery Table
private Map generateLowBatteryTable(boolean noteMode) {
def selectedEnabledDevices = (devs ?: []).findAll{ !it.isDisabled() }
if (!selectedEnabledDevices) return [label:"Low Battery", html:"No battery devices found.", plain:"No battery devices found."]
def lowList = selectedEnabledDevices.collect { dev ->
def level = dev.currentBattery
if (level != null && level <= lowBatteryLevel) {
def critical = (level <= criticalBatteryLevel)
[device:dev, level:level, critical:critical]
}
}.findAll{ it != null }
if (!lowList) return [label:"Low Battery", html:"No low battery devices.", plain:"No low battery devices."]
def sortByVal = settings["sortBy_low"] ?: "level"
def sortOrderVal = settings["sortOrder_low"] ?: "asc"
lowList = lowList.sort { it ->
switch(sortByVal) {
case "displayName": return it.device.displayName.toLowerCase()
case "level": return it.level
default: return it.level
}
}
if (sortOrderVal == "desc") lowList = lowList.reverse()
def totalChecked = selectedEnabledDevices.size()
def lowCount = lowList.size()
def summaryText = "${lowCount} of ${totalChecked} selected devices report having low battery levels."
def tableHtml = "${summaryText}<br><br>"
tableHtml += "<table style='border-collapse:collapse;width:100%;border:1px solid black;' cellpadding='4' cellspacing='0'><thead><tr>"
tableHtml += "<th style='border:1px solid black;'>Battery %</th><th style='border:1px solid black;'>Device Name</th></tr></thead><tbody>"
lowList.each { it ->
def color = it.level <= criticalBatteryLevel ? "red" : "black"
tableHtml += "<tr>"
tableHtml += "<td style='color:${color};border:1px solid black;'>${it.level}%</td>"
tableHtml += "<td style='border:1px solid black;'><a href='/device/edit/${it.device.id}' target='_blank'>${it.device.displayName}</a></td>"
tableHtml += "</tr>"
}
tableHtml += "</tbody></table>"
def plainRows = lowList.collect { "${it.level}% ${it.device}" }
def plainMsg = "${summaryText}\n\n" + plainRows.join("\n")
return [label:"Low Battery", html: tableHtml, plain: plainMsg]
}
// Last Activity Table
private Map generateActivityTable(boolean noteMode) {
def sortByVal = settings["sortBy_activity"] ?: "lastActivity"
def sortOrderVal = settings["sortOrder_activity"] ?: "desc"
def thresholdHours = (settings["activityIntervalHours"] ?: 24) as int
def thresholdMillis = thresholdHours * 60 * 60 * 1000
def selectedEnabledDevices = (devs ?: []).findAll{ !it.isDisabled() }
if (!selectedEnabledDevices) return [label:"Last Activity", html:"No battery devices found.", plain:"No battery devices found."]
def rightNow = new Date()
def reportList = selectedEnabledDevices.collect { dev ->
def lastActivity = dev.getLastActivity()
def overdue = lastActivity ? (rightNow.time - lastActivity.time > thresholdMillis) : true
String lastStrUI
String lastStrNote
if (lastActivity) {
def formatted = lastActivity.format("yyyy-MM-dd hh:mm a", location.timeZone)
lastStrUI = "<span style='color:red;'>${formatted}</span>"
lastStrNote = formatted
} else {
lastStrUI = "<span style='color:red;'>[Never]</span>"
lastStrNote = "0000-00-00 00:00 xx"
}
def fs = '\u2007'
def batteryLevel = dev.currentBattery != null ? Math.round(dev.currentBattery).toString().padLeft(3, fs) : "N/A".padLeft(3, fs)
[device:dev,lastActivity:lastActivity,lastStrUI:lastStrUI,lastStrNote:lastStrNote,level:batteryLevel,overdue:overdue]
}.findAll { it.overdue } // ? only keep overdue devices
// Sorting for generateActivityTable
def neverList = reportList.findAll { it.lastActivity == null }
def normalList = reportList.findAll { it.lastActivity != null }
// Sort "Never" group alphabetically by device name (always asc)
neverList = neverList.sort { it.device.displayName.toLowerCase() }
// Sort normal group by selected sort key
normalList = normalList.sort { it ->
switch(sortByVal) {
case "displayName": return it.device.displayName.toLowerCase()
case "lastActivity":return it.lastActivity
case "level": return (it.level.toString().isNumber() ? it.level.toInteger() : -1)
default: return it.lastActivity
}
}
if (sortOrderVal == "desc") normalList = normalList.reverse()
// Combine: [Never] group always first
reportList = neverList + normalList
def totalChecked = selectedEnabledDevices.size()
def overdueCount = reportList.size()
def summaryText = (overdueCount > 0) ?
"${overdueCount} of ${totalChecked} selected devices have overdue activity." :
"${totalChecked!=1?'All':'The'} ${totalChecked} selected device${totalChecked!=1?'s':''} reported activity within ${thresholdHours}h."
// Build HTML table
def tableHtml = "${summaryText}<br><br>"
if (overdueCount > 0) {
tableHtml += "<table style='border-collapse:collapse;width:100%;border:1px solid black;' cellpadding='4' cellspacing='0'><thead><tr>"
tableHtml += "<th style='border:1px solid black;'>Last Activity</th><th style='border:1px solid black;'>Battery %</th><th style='border:1px solid black;'>Device Name</th></tr></thead><tbody>"
reportList.each { it ->
tableHtml += "<tr>"
tableHtml += "<td style='border:1px solid black;'>${it.lastStrUI}</td>"
tableHtml += "<td style='border:1px solid black;'>${it.level}%</td>"
tableHtml += "<td style='border:1px solid black;'><a href='/device/edit/${it.device.id}' target='_blank'>${it.device.displayName}</a></td>"
tableHtml += "</tr>"
}
tableHtml += "</tbody></table>"
}
// Plain text version
def plainRows = []
if (overdueCount > 0) {
plainRows = reportList.collect { row ->
//"?? ${row.lastStrNote} ${row.level}% ${row.device}"
"${row.lastStrNote} ${row.level}% ${row.device}"
}
}
def plainMsg = "${summaryText}\n\n" + (plainRows ? plainRows.join("\n") : "")
return [label:"Last Activity", html: tableHtml, plain: plainMsg]
}
// Shared sort helper to keep "[Never]" grouped first and alphabetically
private List sortWithNeverGrouping(List reportList, String sortByVal, String sortOrderVal, boolean activityMode=false) {
def sorted = reportList.sort { it ->
def never = activityMode ? (it.lastActivity == null) : (it.lastDate == null)
def tier = never ? 0 : 1
def secondary = never ? it.device.displayName.toLowerCase() : null
def sortKey
switch(sortByVal) {
case "displayName": sortKey = it.device.displayName.toLowerCase(); break
case "lastActivity":
sortKey = it.lastActivity ?: new Date(0)
break
case "lastStr":
sortKey = it.lastDate ?: new Date(0)
break
case "level":
sortKey = (it.level.toString().isNumber() ? it.level.toInteger() : -1)
break
case "lastEventStr":
sortKey = it.lastEventStr?.toLowerCase() ?: ""
break
default:
sortKey = activityMode ? (it.lastActivity ?: new Date(0)) : (it.lastDate ?: new Date(0))
}
return [tier, secondary, sortKey]
}
if (sortOrderVal == "desc") sorted = sorted.reverse()
return sorted
}

