/* * Device UI * * * Licensed Virtual the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WIyTHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * * Date Who Description * ------------- ------------------- --------------------------------------------------------- * */ static String version() { return '0.1.2' } import java.text.SimpleDateFormat import groovy.json.JsonSlurper import groovy.json.JsonOutput import groovy.transform.Field //include thebearmay.uiInputElements definition ( name: "Device UI", namespace: "thebearmay", author: "Jean P. May, Jr.", description: "Alternate Device UI", category: "Utility", importUrl: "https://raw.githubusercontent.com/thebearmay/hubitat/main/apps/deviceUI.groovy", installOnOpen: true, oauth: false, iconUrl: "", iconX2Url: "" ) preferences { page name: "deviceList" page name: "uiRender" } mappings { /* path("/refresh") { action: [POST: "refresh", GET: "refresh"] } */ } def installed() { // log.trace "installed()" state?.isInstalled = true initialize() } def updated(){ // log.trace "updated()" if(!state?.isInstalled) { state?.isInstalled = true } if(debugEnabled) runIn(1800,logsOff) } def initialize(){ } void logsOff(){ app.updateSetting("debugEnabled",[value:"false",type:"bool"]) } def deviceList(){ dynamicPage (name: "deviceList", title: "

${app.getLabel()} v${version()}

", install: true, uninstall: true) { section (name:'cPageHndl', title:''){ paragraph getInputElemStr(name:"selDev", type:'capability.*', width:'15em', radius:'12px', color:'#000000', background:'#2596be', title:"Device to View") //log.debug "${getInputElemStr(name:"selDev", type:'capability.*', width:'15em', radius:'12px', color:'#000000', background:'#2596be', title:"Device to View").replace('<','<')}" paragraph getInputElemStr(name:"debugEnabled", type:'bool', width:'15em', radius:'12px', color:'#000000', background:'#2596be', title:"Enable Debug") paragraph getInputElemStr(name:"nameOverride", type:'text', width:'15em', radius:'12px', color:'#000000', background:'#2596be', title:"New Name for Application", defaultValue:app.getLabel()) if(selDev){ inx = appLocation().lastIndexOf("/") paragraph "" } if(debugEnabled) runIn(1800, 'logsOff') if(nameOverride != app.getLabel()) app.updateLabel(nameOverride) } } } def uiRender(){ dynamicPage (name: "uiRender", title: "", install: false, uninstall: false) { section(name:"visualDisp",title:"", hideable: false, hidden: false){ if(state.message){ paragraph "${state.message}" state.message ='' } if(state.refreshNeeded){ state.refreshNeeded = false paragraph "" } ndBtn = getInputElemStr(name:"newDev", type:'button', width:'10em', radius:'12px', background:'#2596be', title:"Change Device") resetLO = getInputElemStr(name:"resetLayout", type:'button', width:'10em', radius:'12px', background:'#2596be', title:"Reset Layout") if(selDev){ devId = selDev.id deviceMap = getDevice(devId) } else { inx = appLocation().lastIndexOf("/") paragraph "" return } pContent= buildPage(deviceMap) if(debugEnable) { pname = "devUIWork${app.id}.htm" uploadHubFile ("$pname",pContent.getBytes("UTF-8")) } if(selDev){ paragraph "${fullScrn}
${selDev.displayName}(${selDev.id})${ndBtn}${resetLO}
" paragraph pContent } if(state.prefDirty) paragraph '' if(state.devInfoDirty) paragraph '' if(state.ndBtnPushed){ state.ndBtnPushed = false stateList = [] state.each{ stateList.add(it.key) } stateList.each{ if(it != "isInstalled" && it!= null ){ app.state.remove(it) } } settings.each { if(it.key != 'savePos') app.removeSetting(it.key) } inx = appLocation().lastIndexOf("/") paragraph "" } if(state.resetLayoutPushed){ state.resetLayoutPushed = false app.removeSetting("savePos") paragraph "" } if(state.savePrefPushed){ state.prefDirty = false state.savePrefPushed = false settings.each{ if(it.key.startsWith('pref')) { nKey = it.key.substring(4,).trim() sType = app.getSettingType("${it.key}") //log.debug "$nKey $sType ${it.value}" selDev.updateSetting("${nKey}",[value:"${it.value}".trim(),type:"$sType"]) } } paragraph "" } if(state.resetPrefPushed){ state.resetPrefPushed = false settings.each{ if(it.key.startsWith('pref')) { app.removeSetting("${it.key}") } } paragraph '' } if(state.saveDevInfPushed){ state.devInfoDirty = false state.saveDevInfPushed = false saveDevInfo(deviceMap) paragraph "" } if(state.resetDevInfPushed){ state.resetDevInfPushed = false settings.each{ if(it.key.startsWith('devInf')) { app.removeSetting("${it.key}") } } paragraph '' } if(state.unschedPushed) { state.unschedPushed = false jId = state.unschedId state.remove('unschedId') removeJob(jId) } } } } def removeJob(jId){ String jobId= "dev${selDev.id}${state["job$jId"].type}.${state["job$jId"].method}" try{ params = [ uri: "http://127.0.0.1:8080/hub/advanced/deleteScheduledJob?id=$jobId", headers: [ "Accept": "application/json" ] ] if(debugEnabled) log.debug "$params" httpGet(params){ resp -> if(debugEnabled) log.debug "$resp.data" return resp.data } }catch (e){ log.error "$e" } } def appButtonHandler(btn) { switch(btn) { case 'newDev': state.ndBtnPushed = true break case 'resetLayout': state.resetLayoutPushed = true break case 'savePref': state.message = 'Saving' state.savePrefPushed = true break case "resetPref": state.resetPrefPushed = true break case 'saveDevInf': state.message = 'Saving' state.saveDevInfPushed = true break case "resetDevInf": state.resetDevInfPushed = true break default: //state."${btn}Pushed" = true if(btn.startsWith('unsched')){ state.unschedPushed = true state.unschedId = btn.substring(7,) } else { bParms = '' pList = [] inx=0 settings.sort().each { if(it.key.startsWith("$btn")){ // log.debug "${it.properties}" if(inx>0) bParms += ',' bParms=it.value pList.add(it.value) app.removeSetting("$btn$inx") inx++ } } if(inx > 1) selDev."$btn"(pList.each{it.value}) else if(inx==1) selDev."$btn"(bParms) else selDev."$btn"() //log.debug "command selDev.$btn(${pList.each{it.value}}) requested" state.message ="Command Executed" } state.refreshNeeded = true break } } HashMap jsonResponse(retMap){ return JsonOutput.toJson(retMap) } String buildPage(deviceMap){ if(!selDev) return if (debugEnabled) log.debug "Build Page" String region1 = '' String region2 = '

Current States

' String region3 = '' String region4 = '' String region5 = '' String region6 = '' deviceMap.commands.sort{it.name}.each { region1 += buildCommandBlock(it) } deviceMap.device.currentStates.sort().each{ it.value.stringValue=it.value.stringValue.replace('\"','\\\"') region2 += "${it.value.attributeName}: ${it.value.stringValue}
" } region2 += '

State Variables

' deviceMap.deviceState.sort().each{ it.value=it.value.toString().replace('\"','\\\"') region2 += "

${it.key}: ${it.value}

" } region3 = buildPreference(deviceMap.settings) region3 += "


" savePref = getInputElemStr(name:"savePref", type:'button', width:'10em', radius:'12px', background:'#2596be', title:"Save Prefs") revPref = getInputElemStr(name:"resetPref", type:'button', width:'10em', radius:'12px', background:'#2596be', title:"Reset Prefs") region3 += "
${savePref}${revPref}
" region4 = buildDevInfo(deviceMap) region4 += "


" saveDevInf = getInputElemStr(name:"saveDevInf", type:'button', width:'10em', radius:'12px', background:'#2596be', title:"Save Dev Info") revDevInf = getInputElemStr(name:"resetDevInf", type:'button', width:'10em', radius:'12px', background:'#2596be', title:"Reset Dev Info") region4 += "
${saveDevInf}${revDevInf}
" region5 = buildEventList() region6 = buildJobList(deviceMap.scheduledJobs) String regionDef1 = getRegion('region-1', 'Commands', region1) String regionDef2 = getRegion('region-2', 'States', region2) String regionDef3 = getRegion('region-3', 'Preferences', region3) String regionDef4 = getRegion('region-4', 'Device Information', region4) String regionDef5 = getRegion('region-5', 'Events', region5) String regionDef6 = getRegion('region-6', 'Scheduled Jobs', region6) pContent = getRegionsPage(['region-1':regionDef1,'region-2':regionDef2,'region-3':regionDef3,'region-4':regionDef4,'region-5':regionDef5,'region-6':regionDef6], true ) pContent+="" return pContent } HashMap getDevice(devId){ try{ params = [ uri: "http://127.0.0.1:8080/device/fullJson/$devId", headers: [ "Accept": "application/json" ] ] if(debugEnabled) log.debug "$params" httpGet(params){ resp -> if(debugEnabled) log.debug "$resp.data" return resp.data } }catch (e){ log.error "$e" } } String buildCommandBlock(parms){ if(debugEnabled) log.debug "buildCommandBlock" LinkedList textType=["text","number","decimal","date","time","password","color"] String cBlock = '' String title = reverseCamel(parms.name) String btn = getInputElemStr(name:"${parms.name}", type:'button', width:'5em', radius:'12px', background:'#2596be', title:"Submit") cBlock = "

$title

" String params = '' if(parms.parameters){ if(debugEnabled) log.debug "Found ${parms.parameters.size()} parameters" int inx = 0 parms.parameters.each { pType = it.type.toLowerCase().trim() if("$pType" == "string") pType = 'text' String nameBlock = '' if(it.name) nameBlock += "${it.name}
" if(it.description) nameBlock += "${it.description}
" if(nameBlock == '') nameBlock = "Enter a ($pType) value
" if(textType.contains(pType)) { params += getInputElemStr( [name:"${parms.name}${inx}", type:"${pType}", title:"${nameBlock}", width:"12em", background:"#ADD8E6", radius:"15px", defaultValue:""]) } else if(pType == 'enum') { params += getInputElemStr( [name:"${parms.name}${inx}", type:"${pType}", title:"${nameBlock}", width:"12em", background:"#ADD8E6", radius:"15px", defaultValue:"", options:it.constraints]) } else { params += "Unknown pType - $pType" } params += "
" inx++ } cBlock+= "$params" } else if(parms.arguments){ if(debugEnabled) log.debug "Found ${parms?.arguments?.size()} arguments" } cBlock+="$btn
" if(debugEnabled) log.debug "$cBlock
" return cBlock } String buildPreference(pSet){ if(debugEnabled) log.debug "buildPreferenceList($pSet)" ArrayList textType=["text","number","decimal","date","time","password","color"] pBlock = '' state.prefDirty = false pSet.each { if (it.options) { //optsWork = "${it.options}".replace('[','').replace(']','').split(',') optsWork = "${it.options}".split(',') //log.debug optsWork } if(settings["pref${it.name}"] != null) defVal = settings["pref${it.name}"] else if(it.value) defVal = it.value else defVal = it.defaultValue if(debugEnabled) log.debug "${it.name} ${settings["pref${it.name}"]} ${it.value} ${it.defaultValue} => $defVal" if("$defVal".trim() != "${it.value}".trim()){ if(debugEnabled) log.debug "${it.name} is dirty: $defVal / ${it.value}" state.prefDirty = true } if (it.type == 'string') it.type = 'text' if(it.type == 'bool') itemWidth = "${it.title.size()*0.70}em" else itemWidth = '12em' pBlock +=getInputElemStr( [name:"pref${it.name}", type:"${it.type}", title:"${it.title}", background:"#ADD8E6", width:itemWidth, radius:"15px", options:optsWork, defaultValue:defVal, submitOnChange:true ]) } return pBlock } String buildDevInfo(deviceMap) { if(debugEnabled) log.debug "buildDevInfo" dMap = deviceMap.device state.devInfoDirty = false diBlock = '' driverList = getDrivers().drivers roomsList = getRooms() ArrayList dList = [] ArrayList rList = [] ArrayList dashList = [] ArrayList dashSelList = [] driverList.sort{it.name}.each { dList.add("${it.id}:${it.name}") } if(roomsList) { roomsList.sort{it.name}.each { rList.add("${it.id}:${it.name}") } } deviceMap.dashboards.sort{it.name}.each{ dashList.add("${it.id}:${it.name}") if(it.selected) dashSelList.add("${it.id}") } dVal = settings["devInfName"]?:dMap.name if(dVal != dMap.name) { state.lastDirty = 'displayName' state.devInfoDirty = true } diBlock += "
"+getInputElemStr( [name:"devInfName", type:"text", title:"Device Name", width:"20em", background:"#ADD8E6", radius:"15px", submitOnChange:true, defaultValue:"${dVal}" ]) dVal = settings["devInfLabel"]?:dMap.label?:"" if(dVal != dMap.label && dMap.label != null) { state.lastDirty = 'label' state.devInfoDirty = true } diBlock += ""+getInputElemStr( [name:"devInfLabel", type:"text", title:"Device Label", width:"20em", background:"#ADD8E6", radius:"15px", submitOnChange:true, defaultValue:"${dVal}" ]) dVal = settings["devInfDNI"]?:dMap.deviceNetworkId if(dVal != dMap.deviceNetworkId) { state.lastDirty = 'DNI' state.devInfoDirty = true } diBlock += ""+getInputElemStr( [name:"devInfDNI", type:"text", title:"Device Network ID", width:"20em", background:"#ADD8E6", radius:"15px", submitOnChange:true, defaultValue:"${dVal}" ]) dVal = settings["devInfDevTypeId"]?:dMap.deviceTypeId if("$dVal" != "${dMap.deviceTypeId}") { state.lastDirty = 'deviceTypeId' state.devInfoDirty = true } //log.debug "${getInputElemStr( [name:"devInfDevTypeId", type:"enum", title:"Device Type", width:"20em", background:"#ADD8E6", radius:"15px", options:dList, submitOnChange:true, defaultValue:"${dVal}" ]).replace('<','<')}" diBlock += "
"+getInputElemStr( [name:"devInfDevTypeId", type:"enum", title:"Device Type", width:"20em", background:"#ADD8E6", radius:"15px", options:dList, submitOnChange:true, defaultValue:"${dVal}" ])//+"" //dVal = "${settings["devInfRoom"]}"?:"${dMap.roomId}"?:"" if(settings["devInfRoom"]) dVal = settings["devInfRoom"] else if (dMap.roomId) dVal = dMap.roomId else dVal = '' //log.debug "${settings["devInfRoom"]} ${dMap.roomId} $dVal" if("$dVal" != "${dMap.roomId}" && dMap.roomId != null) { state.lastDirty = 'roomId' state.devInfoDirty = true } diBlock += ""+getInputElemStr( [name:"devInfRoom", type:"enum", title:"Room", width:"20em", background:"#ADD8E6", radius:"15px", options:rList, submitOnChange:true, defaultValue:"${dVal}" ])//+"" dVal = settings["devInfDash"]?:dashSelList?:"" if(dVal != dashSelList && dashSelList != []) { state.lastDirty = "dashSelList $dval $dashSelList" state.devInfoDirty = true } diBlock += ""+getInputElemStr( [name:"devInfDash", type:"enum", title:"Dashboards", width:"20em", background:"#ADD8E6", radius:"15px", options:dashList, submitOnChange:true, defaultValue:dVal, multiple:true ])//+"" dVal = settings["devInfMaxEvent"]?:dMap.maxEvents?:11 if(dVal != dMap.maxEvents && dMap.maxEvents != null) { state.lastDirty = 'maxEvents' state.devInfoDirty = true } diBlock += "
"+getInputElemStr( [name:"devInfMaxEvent", type:"number", title:"Max Events
* per event type (1-2000)", width:"20em", background:"#ADD8E6", radius:"15px", submitOnChange:true, defaultValue:dVal ]) dVal = settings["devInfMaxStates"]?:dMap.maxStates?:30 if(dVal != dMap.maxStates && dMap.maxStates != null) { state.lastDirty = 'maxStates' state.devInfoDirty = true } diBlock += "
"+getInputElemStr( [name:"devInfMaxStates", type:"number", title:"State History Size
* per attribute (1-2000)", width:"20em", background:"#ADD8E6", radius:"15px", submitOnChange:true, defaultValue:dVal ]) dVal = settings["devInfSpammy"]?:dMap.spammyThreshold?:300 if(dVal != dMap.spammyThreshold && dMap.spammyThreshold != null) { state.lastDirty = 'spammyThreshold' state.devInfoDirty = true } diBlock += "
"+getInputElemStr( [name:"devInfSpammy", type:"number", title:"Too many events alert threshold
* per hour (100-2000)", width:"20em", background:"#ADD8E6", radius:"15px", submitOnChange:true, defaultValue:dVal ]) //dVal = "${settings['devInfMeshEnabled']}"?:"${dMap.meshEnabled}"?:false if(settings['devInfMeshEnabled']) dVal = settings['devInfMeshEnabled'] else if(dMap.meshEnabled) dVal = dMap.meshEnabled else dVal = false if("${dVal}" != "${dMap.meshEnabled}" && (dVal != false && dMap.meshEnabled == null)) { state.lastDirty = 'meshEnabled' state.devInfoDirty = true //log.debug "${settings["devInfMeshEnabled"]} ${dMap.meshEnabled} $dVal" } diBlock += "
"+getInputElemStr( [name:"devInfMeshEnabled", type:"bool", title:"Hub Mesh Enabled", width:"20em", background:"#ADD8E6", radius:"15px", submitOnChange:true, defaultValue:dVal ]) dVal = "${settings["devInfHomeKitEnabled"]}"?:"${dMap.homeKitEnabled}"?:false if("${dVal}" != "${dMap.homeKitEnabled}" && (dVal == false && dMap.homeKitEnabled != null) ) { state.lastDirty = 'homeKitEnabled' state.devInfoDirty = true //log.debug "${settings["devInfHomeKitEnabled"]} ${dMap.homeKitEnabled} $dVal" } diBlock += ""+getInputElemStr( [name:"devInfHomeKitEnabled", type:"bool", title:"HomeKit Enabled", width:"20em", background:"#ADD8E6", radius:"15px", submitOnChange:true, defaultValue:dVal ]) dVal = settings["devInfNotes"]?:dMap.notes?:'' if(dVal != dMap.notes && dMap.notes != null){ state.lastDirty = 'notes' state.devInfoDirty = true } diBlock += "
"+getInputElemStr( [name:"devInfNotes", type:"text", title:"Device Note", width:"20em", background:"#ADD8E6", radius:"15px", submitOnChange:true, defaultValue:"${dVal}" ]) String notesBlock = '' dMap.data.each{ notesBlock += "${it.key}:${it.value}
" } String usedBy = 'In Use By
' deviceMap.appsUsing.each { usedBy+="${it.label}
" } String dateBlock = '' SimpleDateFormat sdfIn = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") if(dMap.createTime) cDate = sdfIn.parse(dMap.createTime) dateBlock += "Dates
Create Date: $cDate
" if(dMap.updateTime) cDate = sdfIn.parse(dMap.updateTime) dateBlock += "Last Update: $cDate
" if(dMap.lastActivityTime) cDate = sdfIn.parse(dMap.lastActivityTime) dateBlock += "Last Activity: $cDate" notesBlock += "

$usedBy$dateBlock
" diBlock += "
Device Data
$notesBlock
" return diBlock } def saveDevInfo(deviceMap) { //log.debug "saveDevInfo" driverData = getDriverData(settings['devInfDevTypeId']) //log.debug "Driver Data:
$driverData" bodyContent = [ name: "${settings['devInfName']}", label: "${settings['devInfLabel']}", zigbeeId: "${deviceMap.device.zigbeeId}", maxEvents: "${settings['devInfMaxEvent']}", maxStates: "${settings['devInfMaxStates']}", spammyThreshold: "${settings['devInfSpammy']}", deviceNetworkId: "${settings['devInfDNI']}", deviceTypeId: "${settings['devInfDevTypeId']}", deviceTypeReadableType: (driverData.type == 'sys') ? 'System':'User', roomId: "${settings['devInfRoom']}", meshEnabled: "${settings['devInfMeshEnabled']}", retryEnabled: "${deviceMap.device.retryEnabled}", meshFullSync: "${deviceMap.device.meshFullSync}", homeKitEnabled: "${settings['devInfHomeKitEnabled']}", locationId: "${deviceMap.device.locationId}", hubId: "${deviceMap.device.hubId}", groupId: "${deviceMap.device.groupId}", dashboardIds: "${settings['devInfDash']}", tags: (deviceMap.device.tags != null) ? "${deviceMap.device.tags}" : "", defaultIcon: "${deviceMap.device.defaultIcon}", notes: "${settings['devInfNotes']}", id: "${selDev.id}", version: "${driverData.version}", controllerType: "${deviceMap.device.controllerType}" ] try{ params = [ uri: "http://127.0.0.1:8080/device/update", headers: [ "Content-Type": "application/x-www-form-urlencoded" ], followRedirects:false, body: bodyContent ] if(debugEnabled) log.debug params httpPost(params) { resp -> } }catch (e){ log.error "$e" } state.resetDevInfPushed = true } String buildEventList() { eList = selDev.events([max:settings['devInfMaxEvent']]) eTable = '' SimpleDateFormat sdfIn = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS") SimpleDateFormat sdfOut = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS a") eList.each { eDate = sdfIn.parse("${it.date}") eTable += "" } eTable +='
DateNameValueUnitDescriptionDescriptive TextSource
${sdfOut.format(eDate)}${it.name}${it.value}${it.unit?:''}${it.description?.replace('<','<')?:''}${it.descriptionText?.replace('<','<')?:''}${it.source}
' return eTable } String buildJobList(jList) { String removeIcon = "" jTable = '' SimpleDateFormat sdfIn = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") SimpleDateFormat sdfOut = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS a") inx = 0 jList.each { state["job$inx"] = [id:inx, method:"${it.handler}", nRun:"${it.nextRunTime}", type:("${it.schedule}" == "Once") ? "Once" : "Recur"] delBtn = getInputElemStr(type:"button",name:"unsched$inx", title:"$removeIcon", color:"#ff0000", width:"26px") jTable+="" inx++ } jTable+='
NameScheduleNextPreviousStatus
${it.handler}${it.schedule}${sdfOut.format(sdfIn.parse(it.nextRunTime))}${(it.prevRunTime) ? sdfOut.format(sdfIn.parse(it.prevRunTime)):""}${it.status}$delBtn
' return jTable } def getDriverData(dId){ //log.debug "getDriverData($dId)" dMap = getDrivers().drivers def holdIt inx = 0 dMap.each { if("${it.id}" == "$dId"){ holdIt = it } } return holdIt } def getDrivers(){ try{ params = [ uri: "http://127.0.0.1:8080/device/drivers", headers: [ "Accept": "application/json" ] ] if(debugEnabled) log.debug "$params" httpGet(params){ resp -> if(debugEnabled) log.debug "$resp.data" return resp.data } }catch (e){ log.error "$e" } } ArrayList getRooms(){ try{ params = [ uri: "http://127.0.0.1:8080/room/listRoomsJson", headers: [ "Accept": "application/json" ] ] if(debugEnabled) log.debug "$params" httpGet(params){ resp -> if(debugEnabled) log.debug "$resp.data" return resp.data } }catch (e){ log.error "$e" } } String reverseCamel(sVal){ String result = '' for(i=0;i
${regionTitle}
-
${regionContent}
""" return region } String getRegionsPage( regionsList, fullScreen ){ // regionsList should be a list of map elements [regionName:regionContentString] String regionsMerged = '' String dragList = '' String defaultPos = '' int regionsInx = 0 int l = 50 int t = 0 int w = 300 int h = 250 regionsList.each { regionsMerged += it.value if(regionsInx > 0) { dragList += ',' defaultPos += ',' } dragList += "'${it.key}'" defaultPos += "'${it.key}': { left: '${l}px', top: '${t}px', width: '${w}px', height: '${h}px', zIndex: '${regionsInx+1}' }" t+= 44 l+= 30 regionsInx++ } String bodyHtml = """
${inputHiddenElem(name:'savePos', type:'hidden', width:'1em', radius:'12px', background:'#2596be', title:'', submitOnChange:true, defaultValue:'')} ${regionsMerged}
""" if(settings["savePos"]) posStr = "" else posStr = "" bodyHtml+=posStr if(fullScreen) return bodyHtml + fullScrn else return bodyHtml } String inputHiddenElem(HashMap opt) { if(!opt.name || !opt.type) return "Error missing name or type" if(settings[opt.name] != null){ if(opt.type != 'time') { opt.defaultValue = settings[opt.name] } else { SimpleDateFormat sdf = new SimpleDateFormat('HH:mm') SimpleDateFormat sdfIn = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") opt.defaultValue = sdf.format(sdfIn.parse(settings[opt.name])) } } typeAlt = opt.type if(opt.type == 'number') { step = ' step=\"1\" ' } else if (opt.type == 'decimal') { step = ' step=\"any\" ' typeAlt = 'number' } else { step = ' ' } String computedStyle = '' if(opt.type == 'hidden'){ opt.type='text' typeAlt = 'hidden' computedStyle += 'visibility:hidden;' } if (opt.float) computedStyle +="float:${opt.float};" if(opt.width) computedStyle += "width:${opt.width};min-width:${opt.width};" if(opt.background) computedStyle += "background-color:${opt.background};" if(opt.color) computedStyle += "color:${opt.color};" if(opt.fontSize) computedStyle += "font-size:${opt.fontSize};" if(opt.radius) computedStyle += "border-radius:${opt.radius};" if(!opt.multiple) opt.multiple = false if(opt.hoverText && opt.hoverText != 'null'){ opt.title ="${opt.title}
${btnIcon([name:'fa-circle-info'])}${opt.hoverText}
" } String retVal = "
" retVal+="
" retVal+="
" retVal+="
" if(typeAlt != 'hidden') retVal +="
Save
" retVal +="
" return retVal } /* * * Set of methods for UI elements * * * Licensed Virtual the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WIyTHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * * Date Who Description * ---------- -------------- ------------------------------------------------------------------------- * 11Mar2025 thebearmay Add checkbox uiType, add trackColor and switchColor for type = bool * 13Mar2025 Added hoverText, code cleanup * 15Mar2025 Expand btnIcon to handle he- and fa- icons * 18Mar2025 Add btnDivHide to hide/display div's (uiType='divHide') * 03Apr2025 Enable a default value for enums * 04Apr2025 Size option for icons * 23May2025 Add device. to capability * 07Oct2025 Added textarea uiType * 20Feb2026 Minor enhancements and added 'hidden' as a valid input field type * 06Mar2026 Fixed mis-matched
in capability element */ import groovy.transform.Field import java.text.SimpleDateFormat library ( base: "app", author: "Jean P. May Jr.", category: "UI", description: "Set of methods that allow the customization of the INPUT UI Elements", name: "uiInputElements", namespace: "thebearmay", importUrl: "https://raw.githubusercontent.com/thebearmay/hubitat/main/libraries/uiInputElements.groovy", version: "0.0.9", documentationLink: "" ) /************************************************************************ * Note: If using hoverText, you must add $ttStyleStr to at least one * * element display * ************************************************************************/ String getInputElemStr(HashMap opt){ switch (opt.type){ case "text": return inputItem(opt) break case "hidden": return inputItem(opt) break case "number": return inputItem(opt) break case "decimal": return inputItem(opt) break case "date": return inputItem(opt) break case "time": return inputItem(opt) break case "password": return inputItem(opt) break case "color": return inputItem(opt) break case "textarea": return inputTarea(opt) break case "enum": return inputEnum(opt) break case "mode": return inputEnum(opt) break case "bool": return inputBool(opt) break case "checkbox": return inputCheckbox(opt) break case "button": return buttonLink(opt) break case "icon": return btnIcon(opt) break case "href": return buttonHref(opt) break case "divHide": return btnDivHide(opt) break default: if(opt.type && (opt.type.contains('capability') || opt.type.contains('device'))) return inputCap(opt) else return "Type ${opt.type} is not supported" break } } String appLocation() { return "http://${location.hub.localIP}/installedapp/configure/${app.id}/" } /***************************************************************************** * Returns a string that will create an input element for an app - limited to * * text, password, number, date and time inputs currently * * * * HashMap fields: * * name - (required) name to store the input as a setting, no spaces or * * special characters * * type - (required) input type * * title - displayed description for the input element * * width - CSS descriptor for field width * * background - CSS color descriptor for the input background color * * color - CSS color descriptor for text color * * fontSize - CSS text size descriptor * * multiple - true/ * * defaultValue - default for the field * * radius - CSS border radius value (rounded corners) * * hoverText - Text to display as a tool tip * *****************************************************************************/ String inputItem(HashMap opt) { if(!opt.name || !opt.type) return "Error missing name or type" if(settings[opt.name] != null){ if(opt.type != 'time') { opt.defaultValue = settings[opt.name] } else { SimpleDateFormat sdf = new SimpleDateFormat('HH:mm') SimpleDateFormat sdfIn = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") opt.defaultValue = sdf.format(sdfIn.parse(settings[opt.name])) } } typeAlt = opt.type if(opt.type == 'number') { step = ' step=\"1\" ' } else if (opt.type == 'decimal') { step = ' step=\"any\" ' typeAlt = 'number' } else { step = ' ' } String computedStyle = '' if(opt.type == 'hidden'){ opt.type='text' typeAlt = 'hidden' computedStyle += 'visibility:hidden;' } if (opt.float) computedStyle +="float:${opt.float};" if(opt.width) computedStyle += "width:${opt.width};min-width:${opt.width};" if(opt.background) computedStyle += "background-color:${opt.background};" if(opt.color) computedStyle += "color:${opt.color};" if(opt.fontSize) computedStyle += "font-size:${opt.fontSize};" if(opt.radius) computedStyle += "border-radius:${opt.radius};" if(!opt.multiple) opt.multiple = false if(opt.hoverText && opt.hoverText != 'null'){ opt.title ="${opt.title}
${btnIcon([name:'fa-circle-info'])}${opt.hoverText}
" } String retVal = "
" retVal+="
" retVal+="
" retVal+="
" if(typeAlt != 'hidden') retVal +="
Save
" retVal +="
" return retVal } /***************************************************************************** * Returns a string that will create an textArea element for an app - * * * * HashMap fields: * * name - (required) name to store the input as a setting, no spaces or * * special characters * * type - (required) input type * * title - displayed description for the input element * * width - CSS descriptor for field width * * height - CSS descriptor for field height * * background - CSS color descriptor for the input background color * * color - CSS color descriptor for text color * * fontSize - CSS text size descriptor * * defaultValue - default for the field * * radius - CSS border radius value (rounded corners) * * hoverText - Text to display as a tool tip * *****************************************************************************/ String inputTarea(HashMap opt) { if(!opt.name || !opt.type) return "Error missing name or type" typeAlt = opt.type String computedStyle = 'resize:both;' if(opt.width) computedStyle += "width:${opt.width};min-width:${opt.width};" if(opt.height) computedStyle += "height:${opt.height};" if(opt.background) computedStyle += "background-color:${opt.background};" if(opt.color) computedStyle += "color:${opt.color};" if(opt.fontSize) computedStyle += "font-size:${opt.fontSize};" if(opt.radius) computedStyle += "border-radius:${opt.radius};" if (opt.float) computedStyle +="float:${opt.float};" if(opt.hoverText && opt.hoverText != 'null'){ opt.title ="${opt.title}
${btnIcon([name:'fa-circle-info'])}${opt.hoverText}
" } String retVal = "
" retVal+="
" retVal+="
" //retVal+="
Save
" return retVal } /***************************************************************************** * Returns a string that will create an input capability element for an app * * * * HashMap fields: * * name - (required) name to store the input as a setting, no spaces or * * special characters * * type - (required) capability type, 'capability.' * * title - displayed description for the input element * * width - CSS descriptor for field width * * background - CSS color descriptor for the input background color * * color - CSS color descriptor for text color * * fontSize - CSS text size descriptor * * multiple - true/ * * radius - CSS border radius value (rounded corners) * * hoverText - Text to display as a tool tip * *****************************************************************************/ String inputCap(HashMap opt) { String computedStyle = '' if (opt.float) computedStyle +="float:${opt.float};" if(opt.width) computedStyle += "width:${opt.width};min-width:${opt.width};" if(opt.background) computedStyle += "background-color:${opt.background};" if(opt.color) computedStyle += "color:${opt.color};" if(opt.fontSize) computedStyle += "font-size:${opt.fontSize}" if(opt.radius) computedStyle += "border-radius:${opt.radius};" else opt.radius = '1px' if(!opt.multiple) opt.multiple = false String dList = '' String idList = '' int i=0 if(settings["${opt.name}"]){ ArrayList devNameId = [] settings["${opt.name}"].each{ devNameId.add([name:"${it.displayName}", devId:it.deviceId]) } ArrayList devNameIdSorted = devNameId.sort(){it.name} devNameIdSorted.each{ if(i>0) { dList +='
' idList += ',' } dList+="${it.name}" idList+="${it.devId}" i++ } } else { dList = 'Click to set' } String capAlt = opt.type.replace('.','') if(opt.hoverText && opt.hoverText != 'null') opt.title ="${opt.title}
${btnIcon([name:'fa-circle-info'])}${opt.hoverText}
" String retVal = "
" retVal += "
"//${computedStyle} retVal += "
" retVal += "" retVal += "
Update
" return retVal } /***************************************************************************** * Returns a string that will create an input enum or mode element for an app * * * * HashMap fields: * * name - (required) name to store the input as a setting, no spaces or * * special characters * * type - (required) capability type, * * title - displayed description for the input element * * width - CSS descriptor for field width * * background - CSS color descriptor for the input background color * * color - CSS color descriptor for text color * * fontSize - CSS text size descriptor * * multiple - true/ * * options - list of values for the enum (modes will autofill) * * defaultValue - default for the field * * radius - CSS border radius value (rounded corners) * * hoverText - Text to display as a tool tip * *****************************************************************************/ String inputEnum(HashMap opt){ String computedStyle = opt.style ? opt.style:'' if (opt.float) computedStyle +="float:${opt.float};" if(opt.type == 'mode') opt.options = location.getModes() if(opt.width) computedStyle += "width:${opt.width};min-width:${opt.width};" if(opt.background) computedStyle += "background-color:${opt.background};" if(opt.color) computedStyle += "color:${opt.color};" if(opt.fontSize) computedStyle += "font-size:${opt.fontSize};" if(opt.radius) computedStyle += "border-radius:${opt.radius};" if(!opt.multiple) { opt.multiple = false mult = ' ' } else { mult = 'multiple' } if(opt.hoverText && opt.hoverText != 'null') opt.title ="${opt.title}
${btnIcon([name:'fa-circle-info'])}${opt.hoverText}
" String retVal = "
" retVal += "
" retVal += "
" return retVal } int ordinalIndexOf(String str, String substr, int n) { int pos = -1 (0..n).each { if (pos != -1 || it == 0) pos = str.indexOf(substr, pos + 1) } pos } /***************************************************************************** * Returns a string that will create an input boolean element for an app * * * * HashMap fields: * * name - (required) name to store the input as a setting, no spaces or * * special characters * * type - (required) capability type, * * title - displayed description for the input element * * width - CSS descriptor for field width * * background - CSS color descriptor for the input background color * * color - CSS color descriptor for text color * * fontSize - CSS text size descriptor * * defaultValue - default for the field * * radius - CSS border radius value (rounded corners) * * trackColor - CSS color descriptor for the switch track * * switchColor - CSS color descriptor for the switch knob * * hoverText - Text to display as a tool tip * *****************************************************************************/ String inputBool(HashMap opt) { if(!opt.name || !opt.type) return "Error missing name or type" if(opt.hoverText && opt.hoverText != 'null') opt.title ="${opt.title}
${btnIcon([name:'fa-circle-info'])}${opt.hoverText}
" String computedStyle = '' if (opt.float) computedStyle +="float:${opt.float};" if(opt.width) computedStyle += "width:${opt.width};min-width:${opt.width};" if(opt.background) computedStyle += "background-color:${opt.background};" if(opt.color) computedStyle += "color:${opt.color};" if(opt.fontSize) computedStyle += "font-size:${opt.fontSize};" if(opt.radius) computedStyle += "border-radius:${opt.radius};" if(!opt.multiple) opt.multiple = false String trackColor = ' ' String switchColor = ' ' if(opt.trackColor) trackColor = "background-color:$opt.trackColor" if(opt.switchColor) switchColor = "background-color:$opt.switchColor" if(settings["${opt.name}"]) opt.defaultValue = settings["${opt.name}"] String retVal = "
" retVal += "" retVal+="
" retVal += "" retVal += "
" retVal += "" retVal += "
" return retVal } /***************************************************************************** * Returns a string that will create an input checkbox element for an app * * * * HashMap fields: * * name - (required) name to store the input as a setting, no spaces or * * special characters * * type - (required) capability type, * * title - displayed description for the input element * * width - CSS descriptor for field width * * background - CSS color descriptor for the input background color * * color - CSS color descriptor for text color * * fontSize - CSS text size descriptor * * defaultValue - default for the field * * radius - CSS border radius value (rounded corners) * * cBoxColor - CSS color descriptor for the checkbox color * * hoverText - Text to display as a tool tip * *****************************************************************************/ String inputCheckbox(HashMap opt) { if(!opt.name || !opt.type) return "Error missing name or type" if(opt.hoverText && opt.hoverText != 'null') opt.title ="${opt.title}
${btnIcon([name:'fa-circle-info'])}${opt.hoverText}
" opt.type = 'bool' String computedStyle = '' if (opt.float) computedStyle +="float:${opt.float};" if(opt.width) computedStyle += "width:${opt.width};min-width:${opt.width};" if(opt.background) computedStyle += "background-color:${opt.background};" if(opt.color) computedStyle += "color:${opt.color};" if(opt.fontSize) computedStyle += "font-size:${opt.fontSize};" if(opt.radius) computedStyle += "border-radius:${opt.radius};" if(!opt.multiple) opt.multiple = false if(settings["${opt.name}"]) opt.defaultValue = settings["${opt.name}"] else opt.defaultValue = false if(!opt.cBoxColor) opt.cBoxColor = '#000000' String cbClass = 'he-checkbox-unchecked' if(opt.defaultValue) cbClass = 'he-checkbox-checked' String retVal = "
" retVal += "" retVal+="
" retVal += "" retVal += "
" retVal += "" retVal += "
" return retVal } /***************************************************************************** * Returns a string that will create an button element for an app * * * * HashMap fields: * * name - (required) name to identify the button, no spaces or * * special characters * * title - (required) button label * * width - CSS descriptor for field width * * background - CSS color descriptor for the input background color * * color - CSS color descriptor for text color * * fontSize - CSS text size descriptor * * radius - CSS border radius descriptor (corner rounding) * * hoverText - Text to display as a tool tip * *****************************************************************************/ String buttonLink(HashMap opt) { //modified slightly from jtp10181's code if(!opt.name || !opt.title ) return "Error missing name or title" String computedStyle = 'cursor:pointer;text-align:center;box-shadow: 2px 2px 4px #71797E;' if (opt.float) computedStyle +="float:${opt.float};" if(opt.width) computedStyle += "width:${opt.width};min-width:${opt.width};" if(opt.background) computedStyle += "background-color:${opt.background};" if(opt.color) computedStyle += "color:${opt.color};" if(opt.fontSize) computedStyle += "font-size:${opt.fontSize};" if(opt.radius) computedStyle += "border-radius:${opt.radius};" if(!opt.icon) opt.icon = [name:'fa-circle-info'] if(opt.hoverText && opt.hoverText != 'null') opt.title ="${opt.title}
${btnIcon(opt.icon)}${opt.hoverText}
" return "
${opt.title}
" } /***************************************************************************** * Returns a string that will create an button HREF element for an app * * * * HashMap fields: * * name - (required) name to identify the button, no spaces or * * special characters * * title - (required) button label * * destPage - (required unless using destUrl) name of the app page to go to * * destUrl - (required unless using destPage) URL for the external page * * width - CSS descriptor for field width * * background - CSS color descriptor for the input background color * * color - CSS color descriptor for text color * * fontSize - CSS text size descriptor * * radius - CSS border radius descriptor (corner rounding) * *****************************************************************************/ String buttonHref(HashMap opt) { //modified jtp10181's code if(!opt.name || !opt.title ) return "Error missing name or title" if(!opt.destPage && !opt.destUrl) return "Error missing Destination info" String computedStyle = 'cursor:pointer;text-align:center;box-shadow: 2px 2px 4px #71797E;' if (opt.float) computedStyle +="float:${opt.float};" if(opt.width) computedStyle += "width:${opt.width};min-width:${opt.width};" if(opt.background) computedStyle += "background-color:${opt.background};" if(opt.color) computedStyle += "color:${opt.color};" if(opt.fontSize) computedStyle += "font-size:${opt.fontSize};" if(opt.radius) computedStyle += "border-radius:${opt.radius};" if(!opt.icon) opt.icon = [name:'fa-circle-info'] if(opt.destPage) { inx = appLocation().lastIndexOf("/") dest = appLocation().substring(0,inx)+"/${opt.destPage}" } else if(opt.destUrl) { dest=opt.destUrl } if(opt.hoverText && opt.hoverText != 'null') opt.title ="${opt.title}
${btnIcon(opt.icon)}${opt.hoverText}
" return "
${opt.title}
" } /***************************************************************************** * Returns a string that will create an button element to hide/display a div * * for an app * * HashMap fields: * * name - (required) name to identify the button, no spaces or * * special characters * * title - (required) button label * * divName - (require) name of the division to hide or display * * hidden - if true will hide the div immediately * * width - CSS descriptor for field width * * background - CSS color descriptor for the input background color * * color - CSS color descriptor for text color * * fontSize - CSS text size descriptor * * radius - CSS border radius descriptor (corner rounding) * *****************************************************************************/ String btnDivHide(HashMap opt) { if(!opt.name || !opt.title || !opt.divName) return "Error missing name, title or division" String computedStyle = 'cursor:pointer;box-shadow: 2px 2px 4px #71797E;' if (opt.float) computedStyle +="float:${opt.float};" if(!opt.width) opt.width = '100%' computedStyle += "width:${opt.width};" if(opt.background) computedStyle += "background-color:${opt.background};" if(opt.color) computedStyle += "color:${opt.color};" if(opt.fontSize) computedStyle += "font-size:${opt.fontSize};" if(opt.radius) computedStyle += "border-radius:${opt.radius};" if(!opt.icon) opt.icon = [name:'fa-circle-info'] if(opt.destPage) { inx = appLocation().lastIndexOf("/") dest = appLocation().substring(0,inx)+"/${opt.destPage}" } else if(opt.destUrl) { dest=opt.destUrl } String btnElem = "" String script= "" if(opt.hidden){ btnElem = "" script="" } opt.title = "${btnElem} ${opt.title}" if(opt.hoverText && opt.hoverText != 'null') opt.title ="${opt.title}
${btnIcon(opt.icon)}${opt.hoverText}
" return "$script
${opt.title}
" } /***************************************************************************** * Returns a string that will create an button icon element for an app from * * the materials-icon font * * * * name - (required) name of the icon to create * *****************************************************************************/ String btnIcon(HashMap opt) { //modified from jtp10181's code String computedStyle = ' ' if(opt.size) computedStyle += "font-size:${opt.size};" if(opt.color) computedStyle += "color:${opt.color};" if(opt.name.startsWith('he')) return "" else if(opt.name.startsWith('fa')) return ""//fa-circle-info else return "${opt.name}" } /***************************************************************************** * Code sample that returns a string that will create a standard HE table * *****************************************************************************/ /* String listTable() { ArrayList tHead = ["","Disable","Name","Device","Attributes","Interval","Output File",""] String X = "" String O = "" String settingsIcon = "settings_applications" String removeIcon = "" String str = "$tableStyle
" + "" tHead.each { str += "" } str += "" ... } */ @Field static String fullScrn = "" @Field static String ttStyleStr = "" @Field static String tableStyle = ""
${it}