/* * Shade Minder * Simple App to control outdoor shades based on illumance and wind speed * * 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 * ------------- ------------------- --------------------------------------------------------- * 27Oct2025 thebearmay v0.0.1 - Original code * 28Oct2025 thebearmay v0.0.2 - Add the Data Management Screen, Reverse Shade option, Daily Avg Scheduling option * 29Oct2025 thebearmay v0.0.3 - Fix edge case in time averaging * 30Oct2025 thebearmay v0.0.4 - Add the initialization values, and Tool Tips * 03Nov2025 thebearmay v0.0.5 - Add sunrise/sunset offset logic */ static String version() { return '0.0.5' } import java.text.SimpleDateFormat import groovy.json.JsonSlurper import groovy.json.JsonOutput import groovy.transform.Field //include thebearmay.uiInputElements definition ( name: "Shade Minder", namespace: "thebearmay", author: "Jean P. May, Jr.", description: "Simple App to control outdoor shades based on illumance and wind speed", category: "Utility", importUrl: "https://raw.githubusercontent.com/thebearmay/hubitat/main/apps/shadeMinder.groovy", installOnOpen: true, oauth: false, iconUrl: "", iconX2Url: "" ) preferences { page name: "decision" page name: "configPage" page name: "dataFileMgmt" } 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(){ if(!state.avgWind) state.avgWind = 0 if(!maxWind) app.updateSetting("maxWind",[type:"number",value:75]) if(!state.avgLight) state.avgLight = 0 if(!minLight) app.updateSetting("minLight",[type:"number",value:1000]) } void logsOff(){ app.updateSetting("debugEnabled",[value:"false",type:"bool"]) } def decision(){ dynamicPage (name: "decision", title: "

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

", install: true, uninstall: true) { section (name:'cPageHndl', title:''){ if(!dmFirst) configPage() else dataFileMgmt() } } } def configPage(){ dynamicPage (name: "configPage", title: "

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

", install: true, uninstall: true) { section (name:'cPageHndl', title:'Configuration Page'){ String db = getInputElemStr(name:'debugEnabled', type:'bool', width:'15em', radius:'12px', background:'#e6ffff', title:'Debug Enabled', defaultValue: "${settings['debugEnabled']}") String rev = getInputElemStr(name:'reverseDir', type:'bool', width:'15em', radius:'12px', background:'#e6ffff', title:'Reverse Shade Direction', defaultValue: "${settings['reverseDir']}", hoverText:"Reverse the Open/Close commands issued") String uDf = getInputElemStr(name:'useAvg', type:'bool', width:'15em', radius:'12px', background:'#e6ffff', title:'Use Avg Settings from File', defaultValue: "${settings['useDataFile']}", hoverText:"Turn on to schedule using average daily values from the data file") String wind = getInputElemStr(name:'windDev', type:'capability.*', width:'15em', radius:'12px', background:'#e6ffff', title:'Device for Wind Speed', defaultValue: "${settings['windDev']}") //windSpeed String sunLight = getInputElemStr(name:'luxDev', type:'capability.illuminanceMeasurement', width:'15em', radius:'12px', background:'#e6ffff', title:'Device for Light Measurement', defaultValue: "${settings['luxDev']}") //illuminance String shades = getInputElemStr(name:'shadeDev', type:'capability.windowShade', width:'15em', radius:'12px', background:'#e6ffff', title:'Shade Devices', defaultValue: "${settings['shadeDev']}, multiple:true") String begTime = getInputElemStr(name:'sTime', type:'time', width:'15em', radius:'12px', background:'#e6ffff', title:'Opening Time Override', defaultValue: "${settings['sTime']}", hoverText:"This value will override the illumination setting") String endTime = getInputElemStr(name:'eTime', type:'time', width:'15em', radius:'12px', background:'#e6ffff', title:'Closing Time Override', defaultValue: "${settings['eTime']}", hoverText:"This value will override the illumination setting") String bSrS = getInputElemStr(name:'begSunRiseSet', type:'enum', width:'15em', radius:'12px', background:'#e6ffff', title:'Use Opening Sunrise/Sunset', defaultValue: "${settings['begSunRiseSet']}", options:['Sunrise','Sunset'], hoverText:"This value will override the Opening Time above") String eSrS = getInputElemStr(name:'endSunRiseSet', type:'enum', width:'15em', radius:'12px', background:'#e6ffff', title:'Use Closing Sunrise/Sunset', defaultValue: "${settings['endSunRiseSet']}", options:['Sunrise','Sunset'], hoverText:"This value will override the Closing Time above") String bSrsO = getInputElemStr(name:'begOffset', type:'number', width:'15em', radius:'12px', background:'#e6ffff', title:'Offset in Minutes', defaultValue: "${settings['begOffset']}") String eSrsO = getInputElemStr(name:'endOffset', type:'number', width:'15em', radius:'12px', background:'#e6ffff', title:'Offset in Minutes', defaultValue: "${settings['endOffset']}") String mWind = getInputElemStr(name:'maxWind', type:'number', width:'15em', radius:'12px', background:'#e6ffff', title:'Maximum Wind Speed', defaultValue: "${settings['maxWind']}") String mLight = getInputElemStr(name:'minLight', type:'number', width:'15em', radius:'12px', background:'#e6ffff', title:'Minimum Lux to Close', defaultValue: "${settings['minLight']}") String aRename = getInputElemStr(name:"nameOverride", type:"text", title: "New Name for Application", multiple: false, defaultValue: app.getLabel(), width:'15em', radius:'12px', background:'#e6ffff', hoverText:"Change this if you need multiple instances of the app") String cTable = "${ttStyleStr}" cTable += "" cTable += "" cTable += "" cTable += "
${wind}${sunLight}${shades}
${mLight}${mWind}
${endTime}${begTime}
${eSrS}${eSrsO}
${bSrS}${bSrsO}
" ci = btnIcon(name:'event', size:'14px') paragraph getInputElemStr(name:"dMgmt", type:'href', title:"${ci} Data Management", destPage:'dataFileMgmt', width:'11em', radius:'15px', background:'#669999', hoverText:"Go to the maintenance page for the data file") paragraph cTable paragraph "
${db}${rev}
${aRename}${uDf}
" if(nameOverride != app.getLabel()) app.updateLabel(nameOverride) if(luxDev) subscribe(luxDev, "illuminance", evtLux) else unsubscribe(evtLux) if(windDev){ subscribe(windDev, "windSpeed", evtWind) if(windDev.hasAttribute('windGust')) subscribe (windDev,'windGust', evtWind) } else unsubscribe(evtWind) subscribe(location,"sunrise", evtTime) subscribe(location,"sunset", evtTime) if(begSunRiseSet || endSunRiseSet){ setByOffset() schedule("0 15 4 * * ? *", "setByOffset") } else { unschedule("setByOffset") } if(useAvg){ setByAvg() schedule("0 17 4 * * ? *", "setByAvg") paragraph "Schedule is being run using the Daily Average File" } else { unschedule("setByAvg") } checkSched() if(state.sTimeOld != sTime || state.eTimeOld != eTime){ state.sTimeOld = sTime state.eTimeOld = eTime paragraph "" } } } } def dataFileMgmt(){ dynamicPage (name: "dataFileMgmt", title: "

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

", install: true, uninstall: true) { section (name:'dFileHndl', title:'Data Management'){ SimpleDateFormat sdfJulD = new SimpleDateFormat("DDD") String dmF = getInputElemStr(name:'dmFirst', type:'bool', width:'15em', radius:'12px', background:'#e6ffff', title:'Make this the first page', defaultValue: "${settings['dmFirst']}") String cfg = getInputElemStr(name:"cfgPg", type:'href', title:"settings_applications Configuration Page", destPage:'configPage', width:'11em', radius:'15px', background:'#669999') paragraph "
${cfg}${dmF}
" String fBuffer='' try { fBuffer = new String(downloadHubFile("${app.id}.txt"),"UTF-8") } catch (e) { fBuffer = '000:1100:1300\n' uploadHubFile("${app.id}.txt",fBuffer.getBytes('UTF-8')) } String begTime = getInputElemStr(name:'s2Time', type:'time', width:'15em', radius:'12px', background:'#e6ffff', title:'Opening Time', defaultValue: "${settings['s2Time']}", hoverText:"Required, can be before or after Closing Time") String endTime = getInputElemStr(name:'e2Time', type:'time', width:'15em', radius:'12px', background:'#e6ffff', title:'Closing Time', defaultValue: "${settings['e2Time']}", hoverText:"Required, can be before or after Opening Time") String stoDate = getInputElemStr(name:'entryDate', type:'date', width:'15em', radius:'12px', background:'#e6ffff', title:'Day to Log', defaultValue: "${settings['entryDate']}", hoverText:"Required, only the Julian Day value is stored; multiple entries will be averaged") String storeD = getInputElemStr(name:'storeData', type:'button', width:'5em', radius:'12px', background:'#669999', title:'Add to File') String pData = "${ttStyleStr}" pData += "
${stoDate}${begTime}${endTime}
${storeD}
" paragraph pData if(state.updFile) { if(entryDate && s2Time && e2Time){ state.updFile = false eDate = new Date(Integer.parseInt(entryDate.substring(0,4))-1900,Integer.parseInt(entryDate.substring(5,7))-1,Integer.parseInt(entryDate.substring(8,10))) p1 = "${sdfJulD.format(eDate)}:" tWork = s2Time.toString() tW2 = tWork.substring(tWork.indexOf("T")+1,tWork.indexOf("T")+6) tW3 = tW2.split(":") p2 = "${tW3[0]}${tW3[1]}:" tWork = e2Time.toString() tW2 = tWork.substring(tWork.indexOf("T")+1,tWork.indexOf("T")+6) tW3 = tW2.split(":") p3 = "${tW3[0]}${tW3[1]}\n" newEntry = "${p1}${p2}${p3}" if(debugEnabled) log.debug newEntry fBuffer += newEntry uploadHubFile("${app.id}.txt",fSort(fBuffer).getBytes('UTF-8')) app.removeSetting("entryDate") paragraph "" } } paragraph getInputElemStr(name:'useAvg', type:'bool', width:'16em', radius:'12px', background:'#e6ffff', title:'Schedule using daily average', defaultValue: "${settings['useAvg']}",hoverText:"Turn on to schedule using average daily values from the data file") if(useAvg){ setByAvg() schedule("0 17 4 * * ? *", "setByAvg") } else { unschedule("setByAvg") unschedule("forceClose") unschedule("forceOpen") } } } } def evtLux(evt){ holdLevel = state?.preLightLevel holdTime = state?.preLightUpdate if(debugEnabled) log.debug "lux 1 - $holdLevel ${state.preLightLevel} ${state.lightLevel}" if(state.lightLevel) state.preLightLevel = state.lightLevel else state.preLightLevel = Integer.parseInt(evt.value) state.preLightUpdate = state?.lightUpdateTime state.lightLevel = Integer.parseInt(evt.value) state.lightUpdateTime = new Date().getTime() if(!holdLevel || holdLevel < 0) holdLevel = Integer.parseInt(evt.value) if(!holdTime || holdTime < 0) holdTime = new Long(0) if(debugEnabled) log.debug "lux 2 - $holdLevel ${state.preLightLevel} ${state.lightLevel}" if(state.lightUpdateTime - holdTime >= (10*60*1000)){// Only calculate a new average and check whether to open/close every 10 minutes state.avgLight = (holdLevel + state.preLightLevel + state.lightLevel)/3 openCheck() } } def evtWind(evt){ holdLevel = state?.preWindSpeed holdTime = state?.preWindUpdate if(state.windSpeed) state.preWindSpeed = state?.windSpeed else state.preWindSpeed = Float.parseFloat(evt.value) state.preWindUpdate = state?.windUpdateTime state.windSpeed = Float.parseFloat(evt.value) state.windUpdateTime = new Date().getTime() if(!holdLevel || holdLevel < 0) holdLevel = Float.parseFloat(evt.value) if(!holdTime || holdTime < 0) holdTime = new Long(0) if(Float.parseFloat(evt.value) > (1.2*maxWind)) { // open the shade if wind gust of 120% of max forceOpen() } else if(state.windUpdateTime - holdTime >= (10*60*1000)){ state.avgWind = (holdLevel + state.preWindSpeed + state.windSpeed)/3 openCheck() } } def evtTime(evt) { //sunrise sunset check openCheck() } void forceOpen(){ if (avgWind && maxWind && avgWind >= maxWind) // don't time force if recorded wind is too high return shadeDev.each { if(!reverseDir){ it.open() if(debugEnabled) log.debug "Oopening $it ${location.sunrise} $tNow ${location.sunset} ${state.avgWind} $maxWind ${state.avgLight} $minLight" } else { it.close() if(debugEnabled) log.debug "Closing $it ${location.sunrise} $tNow ${location.sunset} ${state.avgWind} $maxWind ${state.avgLight} $minLight" } } if(!reverseDir){ state.lastCommand = 'open' } else { state.lastCommand = 'close' } } void forceClose(){ shadeDev.each { if(!reverseDir){ it.close() if(debugEnabled) log.debug "Closing $it ${location.sunrise} $tNow ${location.sunset} ${state.avgWind} $maxWind ${state.avgLight} $minLight" } else { it.open() if(debugEnabled) log.debug "Opening $it ${location.sunrise} $tNow ${location.sunset} ${state.avgWind} $maxWind ${state.avgLight} $minLight" } } if(!reverseDir){ state.lastCommand = 'close' } else { state.lastCommand = 'open' } } void openCheck(){ if(eTime || sTime) // open and close are being overridden return Date tNow = new Date() if(tNow > location.sunrise && tNow < location.sunset && state.avgWind < maxWind && state.avgLight >= minLight ) { forceOpen() } else { forceClose() } } String fSort(fBuff){ String sortedBuff = '' fList = fBuff.split("\n") fList = fList.sort() fList.each{ sortedBuff += "$it\n" } return sortedBuff } List dailyAvg(){ String fBuffer='' try { fBuffer = new String(downloadHubFile("${app.id}.txt"),"UTF-8") } catch (e) { fBuffer = '000:1100:1300\n' uploadHubFile("${app.id}.txt",fBuffer.getBytes('UTF-8')) } fList = fBuffer.split("\n") List aList = [] String dayHold = '000' int i=0 int sHold=0 int eHold=0 List dRec = [] fList.each{ dRec = it.split(':') if (dRec[0] == dayHold){ sHold += Integer.parseInt(dRec[1]) eHold += Integer.parseInt(dRec[2]) i++ } else { if(debugEnabled) log.debug "$dayHold $sHold $eHold
$aList" sHoldS = (sHold/i).intValue().toString() eHoldS = (eHold/i).intValue().toString() if(debugEnabled) log.debug "$dayHold $sHoldS $eHoldS
$aList" while (sHoldS.size() < 4){ sHoldS = "0$sHoldS" } while (eHoldS.size() < 4){ eHoldS = "0$eHoldS" } aList.add("${dayHold}:${sHoldS}:${eHoldS}") i=1 sHold=Integer.parseInt(dRec[1]) eHold=Integer.parseInt(dRec[2]) dayHold = dRec[0] } } sHoldS = (sHold/i).intValue().toString() eHoldS = (eHold/i).intValue().toString() while (sHoldS.size() < 4){ sHoldS = "0$sHoldS" } while (eHoldS.size() < 4){ eHoldS = "0$eHoldS" } aList.add("${dayHold}:${sHoldS}:${eHoldS}") return aList } void setByAvg() { SimpleDateFormat sdfJulD = new SimpleDateFormat("DDD") List aList = dailyAvg() String targetInx = sdfJulD.format(new Date()) String saTime String eaTime aList.each { item = it.split(':') if(item[0] <= targetInx){ saTime = item[1] eaTime = item[2] } } if(debugEnabled) log.debug "$aList
$saTime $eaTime
$targetInx" app.updateSetting('sTime',[type:"time",value:"${saTime.substring(0,2)}:${saTime.substring(2,4)}"]) app.updateSetting('eTime',[type:"time",value:"${eaTime.substring(0,2)}:${eaTime.substring(2,4)}"]) schedule("0 ${Integer.parseInt(saTime.substring(2,4))} ${Integer.parseInt(saTime.substring(0,2))} * * ? *", "forceClose") schedule("0 ${Integer.parseInt(eaTime.substring(2,4))} ${Integer.parseInt(eaTime.substring(0,2))} * * ? *", "forceOpen") } void setByOffset(){ SimpleDateFormat sdfIn = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy") if(begSunRiseSet){ if(begSunRiseSet == 'Sunrise'){ workTime = sdfIn.parse(location.sunrise.toString()) } else workTime = sdfIn.parse(location.sunset.toString()) if(begOffset){ begOffset = begOffset * 60000 } else begOffSet = 0 nTime = new Date(workTime.getTime() + begOffset) nHours = nTime.getHours().toString() if(nHours.size()<2) nHours="0${nHours}" nMinutes = nTime.getMinutes().toString() if(nMinutes.size()<2) nMinutes="0${nMinutes}" app.updateSetting('sTime',[type:"time",value:"${nHours}:${nMinutes}"]) } if(endSunRiseSet){ if(endSunRiseSet == 'Sunrise'){ workTime = sdfIn.parse(location.sunrise.toString()) } else workTime = sdfIn.parse(location.sunset.toString()) if(begOffset){ begOffset = begOffset * 60000 } else begOffSet = 0 nTime = new Date(workTime.getTime() + begOffset) nHours = nTime.getHours().toString() if(nHours.size()<2) nHours="0${nHours}" nMinutes = nTime.getMinutes().toString() if(nMinutes.size()<2) nMinutes="0${nMinutes}" app.updateSetting('eTime',[type:"time",value:"${nHours}:${nMinutes}"]) } checkSched() } void checkSched(){ if(sTime) { tWork = sTime.toString() tW2 = tWork.substring(tWork.indexOf("T")+1,tWork.indexOf("T")+6) tW3 = tW2.split(":") schedule("0 ${Integer.parseInt(tW3[1])} ${Integer.parseInt(tW3[0])} * * ? *", "forceClose") } else unschedule("forceClose") if(eTime) { tWork = eTime.toString() tW2 = tWork.substring(tWork.indexOf("T")+1,tWork.indexOf("T")+6) tW3 = tW2.split(":") schedule("0 ${Integer.parseInt(tW3[1])} ${Integer.parseInt(tW3[0])} * * ? *", "forceOpen") } else unschedule("forceOpen") } def appButtonHandler(btn) { switch(btn) { case "storeData": state.updFile = true break default: log.error "Undefined button $btn pushed" break } } /* * * 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 */ 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.7", 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 "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 "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.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+="
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.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 = '' 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 } /***************************************************************************** * 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.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.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.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.hoverText && opt.hoverText != 'null') opt.title ="${opt.title}
${btnIcon([name:'fa-circle-info'])}${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.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.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([name:'fa-circle-info'])}${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.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.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([name:'fa-circle-info'])}${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}" } @Field static String ttStyleStr = "" @Field static String tableStyle = ""