/* Copyright 2022 - tomw Licensed under 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: 0.9.10 - tomw - Added support for importing LIRC codes from file in local storage. 0.9.3 - tomw - Rework app UI to use buttons. 0.9.2 - tomw - Rename codes (supported in app). 0.9.0 - tomw - Initial release. */ definition( name: "Broadlink System Manager", namespace: "tomw", author: "tomw", description: "", category: "Convenience", iconUrl: "", iconX2Url: "", iconX3Url: "", importUrl: "https://raw.githubusercontent.com/tomwpublic/hubitat_broadlink/main/broadlinkSystemManagerApp") preferences { page(name: "mainPage") page(name: "pullCodesPage") page(name: "pushCodesPage") page(name: "codeManagementPage") } @Field deviceFilterString = "device.BroadlinkRemote" def mainPage() { dynamicPage(name: "mainPage", title: "", install: true, uninstall: true) { section("Broadlink System Manager") { href(page: "pullCodesPage", title: "Save codes from virtual devices into this app.") href(page: "pushCodesPage", title: "Sync codes from this app out to virtual devices.") href(page: "codeManagementPage", title: "Manage saved codes and import new codes into this app.") } section("Configuration options:") { input name: "enableLogging", type: "bool", title: "Enable debug logging?", defaultValue: false, required: true } } } def linkToMain() { section { href(page: "mainPage", title: "Return to previous page", description: "") } } def cleanBreak(numBreaks) { def breakSection = "
" * numBreaks section { paragraph(breakSection) } } def displayStatus(status) { section { paragraph("Status: ${status} (ts = ${new Date().getTime()})") } } def shortDelay() { pauseExecution(100) } def clearInputs() { // clear out text inputs when we're done with them app.updateSetting("newCodeName", "") app.updateSetting("codesToImport", "") app.updateSetting("prontoToImport", "") app.updateSetting("prontoName", "") } def pullCodesPage() { // make sure this page lags appButtonHandler when necessary shortDelay() dynamicPage(name: "pullCodesPage", title: "", install: true, uninstall: true) { def status = "Waiting for user input." if(getVolatileState("actuallyPull")) { setVolatileState("actuallyPull", false) def codesOptions = collectCodesFromDevs() def codesToPull = [:] if(null != keysToPull) { codesToPull = codesOptions?.subMap(keysToPull) } // meaningful status by default, in case nothing was selected status = "No codes selected." codesToPull.each { addSavedCode(it.key, it.value) status = "Codes saved in to app." } } displayStatus(status) section { input(name: "syncInDevs", type: deviceFilterString, title: "Select Broadlink Remotes to save codes from", multiple: true, required: false, submitOnChange: true) input(name: "keysToPull", type: "enum", title: "Select codes to save into this app", options: collectCodesFromDevs()?.keySet()?.sort(), multiple: true, required: false, offerAll:true, submitOnChange: true) if(null != keysToPull) { input(name: "actuallyPull", type: "button", title: "Press to proceed!") } } cleanBreak(1) linkToMain() } } def pushCodesPage() { // make sure this page lags appButtonHandler when necessary shortDelay() dynamicPage(name: "pushCodesPage", title: "", install: true, uninstall: true) { def status = "Waiting for user input." if(getVolatileState("actuallyPush")) { setVolatileState("actuallyPush", false) Map codes = [:] // meaningful status by default, in case nothing was selected status = "No codes selected." if(null != codesToPush) { codes = allKnownCodesMap()?.subMap(codesToPush) pushCodesToDevs(codes) status = "Codes synced out to devices." } } displayStatus(status) section { input(name: "syncOutDevs", type: deviceFilterString, title: "Select Broadlink Remotes to sync codes to from app", multiple: true, required: false, submitOnChange: true) input(name: "codesToPush", type: "enum", title: "Select codes to sync to these devices", options: allKnownCodesKeys(), multiple: true, required: false, offerAll:true, submitOnChange: true) if(null != codesToPush) { input(name: "actuallyPush", type: "button", title: "Press to proceed!") } } cleanBreak(1) linkToMain() } } def codeManagementPage() { // make sure this page lags appButtonHandler when necessary shortDelay() dynamicPage(name: "codeManagementPage", title: "", install: true, uninstall: true) { def status = "Waiting for user input." if(getVolatileState("clearAllCodes")) { setVolatileState("clearAllCodes", false) clearSavedCodes() status = "Cleared all codes." } if(getVolatileState("deleteSelectedCodes")) { setVolatileState("deleteSelectedCodes", false) codesToDelete.each { deleteSavedCode(it) } status = "Deleted selected codes." } if(getVolatileState("renameSelectedCode")) { setVolatileState("renameSelectedCode", false) // default status, in case rename fails status = "Rename failed!" if(renameSavedCode(codeToRename, newCodeName)) { status ="Renamed code \"${codeToRename}\" to \"${newCodeName}\"" } } if(getVolatileState("importEnteredCodes")) { setVolatileState("importEnteredCodes", false) importCodes(codesToImport) status = "Imported codes." } if(getVolatileState("importProntoCodes")) { setVolatileState("importProntoCodes", false) // default status, in case conversion fails status = "Pronto import failed!" def code = convertProntoToBroadlink(prontoToImport) if(code) { addSavedCode(prontoName, code) status = "Imported Pronto code as ${prontoName}." } } if(getVolatileState("importLircCodes")) { setVolatileState("importLircCodes", false) // default status, in case conversion fails status = "LIRC import failed!" def lircInput, lircContent try { lircInput = downloadHubFile(lircFilename) lircContent = lircParseFile(new String(lircInput)) if(lircContent) { lircContent.codeSpecs.each { thisLircSpec = lircContent.lircSpec += [code: it.code] def code = buildBroadlinkFromLircCode(thisLircSpec) addSavedCode(it.name, code) } } status = "LIRC import succeeded." } catch(java.nio.file.NoSuchFileException e) { status = "LIRC file does not exist!" } catch(e) { logDebug e } } displayStatus(status) section("Delete saved codes") { input(name: "codesToDelete", type: "enum", title: "Select saved codes to delete from app", options: allKnownCodesKeys(), multiple: true, required: false, submitOnChange: true, offerAll:true) if(null != codesToDelete) { input(name: "deleteSelectedCodes", type: "button", title: "Delete selected codes", width: 3) } input(name: "clearAllCodes", type: "button", title: "Delete ALL codes from app", width:3) } section("Rename saved codes") { input(name: "codeToRename", type: "enum", title: "Select code to rename", options: allKnownCodesKeys(), multiple: false, required: false, submitOnChange: true, width:4) input(name: "newCodeName", type: "text", title: "Rename to:", required: false, submitOnChange: true, width:2) if(![codeToRename, newCodeName].contains(null)) { input(name: "renameSelectedCode", type: "button", title: "Rename selected code") } } section("Import hex codes") { input(name: "codesToImport", type: "text", title: "Codes to import (in form of {name=2600...,name2=B200...})", required: false) input(name: "importEnteredCodes", type: "button", title: "Press to import entered codes") } section("Import pronto codes") { input(name: "prontoToImport", type: "text", title: "Pronto code to import (in form of \"0000 12AB\")", required: false, width: 8) input(name: "prontoName", type: "text", title: "Save as:", required: false, width: 4) input(name: "importProntoCodes", type: "button", title: "Press to import entered codes") } section("Import LIRC codes") { input(name: "lircFilename", type: "string", title: "Filename of LIRC file in hub local storage", required: false) input(name: "importLircCodes", type: "button", title: "Press to import entered file") } cleanBreak(1) linkToMain() } } def updated() { installed() } def installed() { unsubscribe() clearInputs() } void appButtonHandler(btn) { // flag button pushed and let pages sort it out setVolatileState(btn, true) } #include tomw.broadlinkHelpers def collectCodesFromDevs() { def combinedCodes = [:] syncInDevs.each { it.cacheCodesForApp(true) Map intCodes = new groovy.json.JsonSlurper().parseText(it.getDataValue("codes")) if(intCodes) { combinedCodes << intCodes } it.cacheCodesForApp(false) } return combinedCodes } def pushCodesToDevs(codes) { if(!codes) { return } synchronized(codesSync) { def outStr = [] // make this look like "name1=code1,name2=code2" for use with importCodes() on device codes.each { k, v -> outStr += ["${k}=${v}"]} outStr = outStr.join(',') logDebug("pushing codes to devices: ${outStr}") syncOutDevs.each { it.importCodes(outStr) } } } def logDebug(msg) { if(enableLogging) { log.debug "${msg}" } }