/* 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 * 17Nov2024 2.0.8 - button styling * 27Feb2025 2.0.9 - code cleanup */ import groovy.transform.Field static String version() { return '2.0.9' } 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(""){ settings.each{ if("${it.key}".contains("sdKey")){ app.removeSetting("${it.key}") } } state.sdSave = false paragraph buildDeviceTable() if(state.singleDev){ state.singleDev = false inx = appLocation().lastIndexOf("/") 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(HashMap opt) { if(!opt.name || !opt.title ) return "Error missing name or title" if(!opt.color) opt.color = "#1A77C9" if(!opt.background) opt.background = "#FFFFFF" if(!opt.fontSize) opt.fontSize = "15px" if(!opt.width) opt.width = '10em' if(!opt.radius) opt.radius = '25px' return "
${opt.title}
" } 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 += "" } inx = appLocation().lastIndexOf("/") 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) { 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([name:"sdSave", title:"Save", color:"#FFFFFF", background:"#007000", fontSize:"15px"]) remBtn = buttonLink2([name:"sdRem", title:"Remove", color:"#FFFFFF", background:"#700000", fontSize:"15px"]) paragraph "
$saveBtn$remBtn
" 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([name:"mdSave", title:"Save", color:"#FFFFFF", background:"#007000", fontSize:"15px"]) remBtn = buttonLink2([name:"mdRem", title:"Remove", color:"#FFFFFF", background:"#700000", fontSize:"15px"]) paragraph "
$saveBtn$remBtn
" 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": 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 } } def intialize() { } @Field static String ttStyleStr = "" @Field static String tableStyle = ""