/* Device Custom Note * * 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * * Change History: * * Date Who What * ---- --- ---- * 01Mar2022 thebearmay 1.0.1 - Add message for any hub mesh device a note is attached to (meshed devices won't retain note) * 1.0.2 - Use controllerType to determine Mesh status * 02Mar2022 thebearmay 1.0.3 - Add warning message for missing note text * 01Oct2024 thebearmay 2.0.0..2.0.4 - Rewrite of the UI * 20Oct2024 2.0.5 - Add more debug * 21Oct2024 2.0.6 - Change buttons on maintenance pages * 08Nov2024 2.0.7 - Left Align device name */ import groovy.transform.Field static String version() { return '2.0.7' } String appLocation() { return "http:${location.hub.localIP}/installedapp/configure/${app.id}/mainPage" } definition ( name: "Custom Device Note", namespace: "thebearmay", author: "Jean P. May, Jr.", description: "Add a custom note to any device.", category: "Utility", importUrl: "https://raw.githubusercontent.com/thebearmay/hubitat/main/apps/custDevNote.groovy", oauth: false, installOnOpen: true, singleThreaded: true, iconUrl: "", iconX2Url: "" ) preferences { page name: "mainPage" page name: "noteMaint" } 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("debugEnable",[value:"false",type:"bool"]) } def mainPage(){ dynamicPage (name: "mainPage", title: "

Custom Note v${version()}

", install: true, uninstall: true) { if (app.getInstallationState() == 'COMPLETE') { section("

Configuration

", hideable:true, hidden: true){ input "qryDevice", "capability.*", title: "Populate Device Table:", multiple: true, required: false, submitOnChange: true input "debugEnabled", "bool", title: "Enable Debug", defaultValue: false, submitOnChange:true input "nameOverride", "text", title: "New Name for Application", multiple: false, required: false, submitOnChange: true, defaultValue: app.getLabel() if(nameOverride != app.getLabel()) app.updateLabel(nameOverride) } section(""){ fileList = getFiles() if(!fileList.contains('iconify')) fetchJS() settings.each{ if("${it.key}".contains("sdKey")){ app.removeSetting("${it.key}") } } state.sdSave = false paragraph buildDeviceTable() if(state.singleDev){ state.singleDev = false paragraph "" } //href name: "noteMaintHref", page: "noteMaint",title: "${btnIcon('pi-pencil')} Note Maint", description: "", width: 4, newLine: false//, params:[did: 30] } } else { section("") { paragraph title: "Click Done", "Please click Done to install app before continuing" } } } } String buttonLink(String btnName, String linkText, color = "#1A77C9", font = "15px") { "
$linkText
" } String buttonLink2(String btnName, String linkText, color = "#1A77C9", bkColor = "#FFFFFF", font = "15px") { "
$linkText
" } String btnIcon(String name) { return "" } String buildDeviceTable(){ ArrayList tHead = ["","Sel","Device","Notes"] String X = "" String O = "" String settingsIcon = "settings_applications" String removeIcon = "" String str = "$ttStyleStr" str += "$tableStyle
" + "" tHead.each { str += "" } str += "" if(qryDevice) devSort = qryDevice.sort { a, b -> a.displayName <=> b.displayName } devSort.each{ noteMap = it.getData() noteList = '' hoverList = "
    " i=0 noteMap.sort().each { if ( i > 0 ) noteList += ", " noteList += "${it.key}" i++ hoverList += "
  • ${it.key}:${it.value}
  • " } hoverList += "
" //str += "" if(it.controllerType == "LNK") singleDev = "" else singleDev = buttonLink("singleDev${it.id}", "$settingsIcon", "#000000", "12px") str += "" if(it.controllerType == "LNK") devSel = "" else devSel = buttonLink("devSel${it.id}", "${state["devSel${it.id}"]? X : O}", "#000000", "12px") if(it.controllerType == "LNK") devName = "${it.displayName} (HubMesh Device)" else devName = it.displayName str += "" str += "" } String addNote = "+" //buttonLink("addNote", "", "#007009", "25px") str += "" str += "
${it}
$settingsIcon
$singleDev$devSel${devName}$hoverList$noteList
$addNote←Add/Edit/Remove Note
" return str } def noteMaint(){ dynamicPage (name:"noteMaint", title: "

Custom Note v${version()}

", install: false, uninstall: false, nextPage:mainPage) { dList = getDevList() if(dList.devList.size() <= 0){ section("") { paragraph "

No devices selected

" } }else if(dList.devList.size() == 1) { section("

Single Device Maintenance

", hideable:false, hidden: false){ paragraph "Selected device: ${dList.devListName[0]}" saveBtn = buttonLink2("sdSave", "Save", "#FFFFFF", "#007000", "15px") remBtn = buttonLink2("sdRem", "Remove", "#FFFFFF", "#700000", "15px") paragraph "
$saveBtn$remBtn
" //input "sdSave", "button", title:"Save", width:2, backgroundColor:'#007000',textColor:'#ffffff' //input "sdRem", "button", title:"Remove", width:2, backgroundColor:'#700000',textColor:'#ffffff' //input "hidden","hidden", title:"", width:8 input "newKey", "text", title:"New/Remove Key",submitOnChange:true, width:6 if(newKey) app.updateSetting("newKey",[value:"${toCamelCase(newKey)}",type:"text"]) input "newVal", "text", title:"New Note",submitOnChange:true, width:6 qryDevice.each{ if("${it.id}" == dList.devList[0]){ noteMap = it.getData() noteMap.sort().each { input "sdKey${it.key}", "text", title:"${it.key}", defaultValue:"${it.value}", submitOnChange:true, width:6 } } } if (state.sdSave) { state.sdSave = false if(debugEnabled)log.debug "Single save requested" qryDevice.each{ dev -> if("${dev.id}" == dList.devList[0]){ if(debugEnabled)log.debug "newKey:$newKey newVal:$newval" if(newKey && newVal){ if(debugEnabled)log.debug "Updating $newKey:$newVal" dev.updateDataValue("$newKey", "$newVal") app.removeSetting("newKey") app.removeSetting("newVal") } settings.each{ if("${it.key}".contains("sdKey")){ if(debugEnabled)log.debug "Updating previous setting ${it.key.substring(5,)}:${it.value}" dev.updateDataValue(it.key.substring(5,),it.value) app.removeSetting("${it.key}") } } } } paragraph "" } if (state.sdRem) { state.sdRem = false if(debugEnabled)log.debug "Remove requested for $newKey" qryDevice.each{ dev -> if("${dev.id}" == devList[0]){ if(debugEnabled)log.debug "Remove from ${dev.id}" dev.removeDataValue(newKey) app.removeSetting("sdKey$newKey") app.removeSetting("newKey") app.removeSetting("newVal") } } paragraph "" } input "mainPage", "button", title:"Return" if(state.mainPg){ state.mainPg = false paragraph "" } } } else { section("

Multi-Device Maintenance

", hideable:false, hidden: false){ paragraph "Selected devices: ${dList.devListName.sort()}" saveBtn = buttonLink2("mdSave", "Save", "#FFFFFF", "#007000", "15px") remBtn = buttonLink2("mdRem", "Remove", "#FFFFFF", "#700000", "15px") paragraph "
$saveBtn$remBtn
" // input "mdSave", "button", title:"Save", width:2, backgroundColor:'#007000',textColor:'#ffffff' // input "mdRem", "button", title:"Remove", width:2, backgroundColor:'#700000',textColor:'#ffffff' // input "hidden","hidden", title:"", width:8 input "newKey", "text", title:"New/Remove/Update Key",submitOnChange:true, width:6 if(newKey) app.updateSetting("newKey",[value:"${toCamelCase(newKey)}",type:"text"]) input "newVal", "text", title:"New/Updated Note",submitOnChange:true, width:6 if (state.mdSave) { state.mdSave = false if(debugEnabled)log.debug "Save requested $newKey:$newVal" qryDevice.each{ dev -> if(state["devSel${dev.id}"]){ if(newKey && newVal){ if(debugEnabled)log.debug "Updating $dev.id for $newKey:$newVal}" dev.updateDataValue("$newKey", "$newVal") } } } app.removeSetting("newKey") app.removeSetting("newVal") paragraph "" } if (state.mdRem) { state.mdRem = false if(debugEnabled)log.debug "Remove requested" qryDevice.each{ dev -> if(state["devSel${dev.id}"]){ if(debugEnabled)log.debug "Removing $newKey from ${dev.id}" dev.removeDataValue(newKey) } } app.removeSetting("newKey") app.removeSetting("newVal") paragraph "" } input "mainPage", "button", title:"Return" if(state.mainPg){ state.mainPg = false paragraph "" } } } } } def getDevList(){ devList = [] devListName = [] qryDevice.each { if(state["devSel${it.id}"]){ devList.add("${it.id}") devListName.add("${it.displayName}") } } return [devList:devList, devListName:devListName] } def toCamelCase(init) { if (init == null) return null; String ret = "" List word = init.split(" ") if(word.size == 1) return init word.each{ ret+=Character.toUpperCase(it.charAt(0)) ret+=it.substring(1).toLowerCase() } ret="${Character.toLowerCase(ret.charAt(0))}${ret.substring(1)}" if(debugEnabled) log.debug "toCamelCase return $ret" return ret; } def appButtonHandler(btn) { if(debugEnabled)log.debug "$btn pressed" switch(btn) { case "addNote": /* if(it.controllerType == "LNK") { atomicState.meshedDeviceMsg+="$it is a Hub Mesh Device, note must be added to the REAL device to be retained
" } } if(atomicState.meshedDeviceMsg == "") atomicState.meshedDeviceMsg = "Update Successful" */ state.addNote = true break case "remNote": qryDevice.each{ it.removeDataValue(noteName) } break case "sdSave": state.sdSave = true break case "sdRem": state.sdRem = true break case "mdSave": state.mdSave = true break case "mdRem": state.mdRem = true break case "mainPage": state.mainPg = true break default: if (btn.contains("devSel")){ if(state?."$btn") state."$btn" = false else state."$btn" = true } else if (btn.contains("singleDev")){ singleDev = "$btn".substring(9,) qryDevice.each{ if("${it.id}" != singleDev) state["devSel${it.id}"] = false else state["devSel${it.id}"] = true } state.singleDev = true } else log.error "Undefined button $btn pushed" break } } ArrayList getFiles(){ fileList =[] params = [ uri : "http://127.0.0.1:8080", path : "/hub/fileManager/json", headers: [ accept : "application/json" ], ] httpGet(params) { resp -> resp.data.files.each { fileList.add(it.name) } } return fileList.sort() } String readExtFile(fName){ def params = [ uri: fName, contentType: "text/html", textParser: true ] try { httpGet(params) { resp -> if(resp!= null) { return """${resp.data}""" } else { log.error "Read External - Null Response" return null } } } catch (exception) { log.error "Read Ext Error: ${exception.message}" return null } } def fetchJS(){ jsFile = readExtFile("https://raw.githubusercontent.com/thebearmay/hubitat/main/libraries/iconify-icon.min.js") if(jsFile){ bArray = (jsFile.getBytes("UTF-8")) uploadHubFile("iconify-icon.min.js",bArray) } else log.error "iconify-icon.min.js not found" } def intialize() { } @Field static String ttStyleStr = "" @Field static String tableStyle = ""