/* 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
*/
import groovy.transform.Field
static String version() { return '2.0.8' }
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") {
"
"
}
String buttonLink2(String btnName, String linkText, color = "#1A77C9", bkColor = "#FFFFFF", font = "15px") {
"
"
}
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 += "${it} " }
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 += "$settingsIcon "
if(it.controllerType == "LNK")
singleDev = "⛔ "
else
singleDev = buttonLink("singleDev${it.id}", "$settingsIcon", "#000000", "12px")
str += "$singleDev "
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 += "$devSel ${devName} $hoverList$noteList "
str += " "
}
String addNote = "+ "
//buttonLink("addNote", "+ ", "#007009", "25px")
str += "$addNote ←Add/Edit/Remove Note "
str += "
"
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 " "
//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 ""
// 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 = ""