/* * CE Connector * * 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 * ---- --- ---- * 31Jul2023 thebearmay New Code * */ static String version() { return '1.0.14' } import groovy.json.JsonSlurper import groovy.json.JsonOutput import java.security.MessageDigest definition( name: "CE Connector", namespace: "thebearmay", author: "Jean P. May, Jr.", description: "Config sync, connector and companion App Community Edition", importUrl: "", installOnOpen: true, oauth: true, iconUrl: "", iconX2Url: "" ) preferences { page name: "mainPage" page name: "deviceMgmt" } mappings { path("/saveconfig") { action: [POST: "saveConfig"] } path("/getconfig") { action: [POST: "getConfig"] } path("/checkconfighash"){ action: [POST: "checkHash"] } path("/getguestlist") { action: [POST: "getGuestList"] } path("/updateguest"){ action: [POST: "updateGuest"] } path("/deleteguest"){ action: [POST: "deleteGuest"] } path("/saveconfigbackuplist"){ action: [POST: "createBackup"] } path("/getconfigbackuplist"){ action: [POST: "getBackup"] } path("/devices/all"){ action: [POST: "deviceListDetailed"] } path("/devices/:devId"){ action: [POST: "deviceDetails"] } path("/devices/:devId/commands"){ action: [POST: "deviceCommandList"] } path("/devices/:devId/events"){ action: [POST: "deviceEventList"] } path("/devices/:devId/capabilities"){ action: [POST: "deviceCapabilities"] } path("/devices/:devId/:cmd/delay/:seconds"){ action: [POST: "deviceIssueCommandDelay"] } path("/devices/:devId/:cmd/:secValue/delay/:seconds"){ action: [POST: "deviceIssueCommandDelay"] } path("/devices/:devId/:cmd/:secValue"){ action: [POST: "deviceIssueCommand"] } path("/devices/:devId/:cmd"){ action: [POST: "deviceIssueCommand"] } path("/devices"){ action: [POST: "deviceList", GET: "deviceList"] } path("/hsm"){ action: [POST: "getHsm"] } path("/modes"){ action: [POST: "getMode"] } } void installed() { if(debugEnabled) log.trace "installed()" state?.isInstalled = true initialize() } void initialize(){ } void logsOff(){ app.updateSetting("debugEnabled",[value:"false",type:"bool"]) } def mainPage(){ dynamicPage (name: "mainPage", title: "", install: true, uninstall: true) { if (app.getInstallationState() == 'COMPLETE') { section("${app.getLabel()} v${version()}") { input "debugEnabled", "bool", title:"Enable Debug Logging:", submitOnChange:true, required:false, defaultValue:false, width:4 if(debugEnabled) { unschedule() runIn(1800,logsOff) } } section("User Tokens", hideable: true, hidden: true) { if(state.admToken == null) state.admToken = generateRandomToken(6) paragraph "'Admin' User Token: ${state.admToken}" input ("genUT", "button", title:"Reset 'Admin' User Token") if(state.getUT == true){ state.getUT = false state.admToken = generateRandomToken(6) } int countOfGuestTokens = 0 htmlOut = "" if(state?.guest == null) state.guest = [] sortedGuest = state.guest.sort { e1, e2 -> e1.key <=> e2.key }*.key sortedGuest.each{ countOfGuestTokens++ htmlOut+="" } htmlOut+="
TokenNamePermissionsDash ID
${it.value}${state.guest[it].name}${state.guest[it].permissions}${state.guest[it].defaultDashId}
" paragraph "'Guest' User Token/s: ${countOfGuestTokens < 1 ? 'none' : countOfGuestTokens}" if(countOfGuestTokens > 0) paragraph "$htmlOut" } section("Hub Security", hideable: true, hidden: true){ if(state.accessToken == null) createAccessToken() tDefault = state.accessToken aDefault = getFullLocalApiServerUrl() input "sourceSecurity", "bool", title: "Hub Security Enabled", defaultValue: false, submitOnChange: true, width:4 if (sourceSecurity) { input("sUsername", "string", title: "Hub Security Username", required: false) input("sPassword", "password", title: "Hub Security Password", required: false) } } section("Endpoint Information", hideable: true, hidden: true){ if(state.accessToken == null) createAccessToken() paragraph "AppID: ${app.id}" paragraph "Access Token: ${state.accessToken}" paragraph "Cloud Token: ${app.getHubUID()}" createQR() paragraph "" paragraph "
" input "resetToken", "button", title:"Reset Access Token" paragraph "Local Server: ${getFullLocalApiServerUrl()}" paragraph "Cloud Server: ${getFullApiServerUrl()}" } section(""){ href "deviceMgmt", title: "Device Management", required: false } section("Reset Application Name", hideable: true, hidden: 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) } } else { section("") { paragraph title: "Click Done", "Please click Done to install app before continuing" } } } } def deviceMgmt(){ dynamicPage (name: "deviceMgmt", title: "Device Management", install: false, uninstall: false) { section(""){ input "assignedDevices", "capability.*", title: "Available Devices:", multiple: true, required: false, submitOnChange: true } } } def saveConfig(){ jsonData = (HashMap) request.JSON if(debugEnabled) log.debug "saveConfig - ${jsonData}" if(getPermissions(jsonData?.token) == "rw"){ fData = JsonOutput.toJson(jsonData).getBytes("UTF-8") uploadHubFile("ceConnConf.txt",fData) retHash = sha256("${jsonData.jsonCustomThemes}${jsonData.jsonDashCfg}","s4ltyS3cre7K3y") state.lastHash=retHash jsonText = JsonOutput.toJson([isError:false, staus:"ok", message:"success",confighash:"$retHash"]) render contentType:'application/json', data: "$jsonText", status:200 } else { def bodyText = JsonOutput.toJson([isError:false, staus:"fail", message:"Access Denied"]) render contentType:'application/json', data: "$bodyText", status:200 } } def createBackup(){ jsonData = (HashMap) request.JSON if(getPermissions(jsonData?.token) == "rw"){ fData = downloadHubFile("ceConnConf.txt") uploadHubFile("ceConnConfBck.txt",fData) jsonText = JsonOutput.toJson([isError:false, staus:"ok", message:"success"]) render contentType:'application/json', data: "$jsonText", status:200 } else { def bodyText = JsonOutput.toJson([isError:false, staus:"fail", message:"Access Denied"]) render contentType:'application/json', data: "$bodyText", status:200 } } def getConfig(){ jsonData = (HashMap) request.JSON if(debugEnabled) log.debug "getConfig - ${jsonData}" if(getPermissions(jsonData?.token) == "rw" || getPermissions(jsonData?.token) == "ro") { Map rMap = [jsonCustomThemes:[:],jsonDashCfg:[:]] try { byte[] rData = downloadHubFile("ceConnConf.txt") String rFile = new String(new String(rData)) JsonSlurper jSlurp = new JsonSlurper() rMap = (Map)jSlurp.parseText(rFile) } catch (Exception e) {} rMap.put("permissions",getPermissions(jsonData?.token)) rMap.put("confighash",sha256("${rMap.jsonCustomThemes}${rMap.jsonDashCfg}","s4ltyS3cre7K3y")) rMap.put("isError",false) rMap.put("status","ok") rMap.put("message","success") def bodyText = JsonOutput.toJson(rMap) render contentType:'application/json', data: "$bodyText", status:200 } else { def bodyText = JsonOutput.toJson([isError:false, staus:"fail", message:"Access Denied"]) render contentType:'application/json', data: "$bodyText", status:200 } } def getBackup(){ jsonData = (HashMap) request.JSON if(getPermissions(jsonData?.token) == "rw") { Map rMap = [configbackuplist:[:]] try { byte[] rData = downloadHubFile("ceConnConfBck.txt") String rFile = new String(new String(rData)) JsonSlurper jSlurp = new JsonSlurper() rMap = (Map)jSlurp.parseText(rFile) } catch (Exception e) {} rMap.put("isError",false) rMap.put("status","ok") rMap.put("message","success") def bodyText = JsonOutput.toJson(rMap) render contentType:'application/json', data: "$bodyText", status:200 } else { def bodyText = JsonOutput.toJson([isError:false, staus:"fail", message:"Access Denied"]) render contentType:'application/json', data: "$bodyText", status:200 } } def checkHash(){ rMap = [status:"ok", confighash:"$state.lastHash"] def bodyText = JsonOutput.toJson(rMap) render contentType:'application/json', data: "$bodyText", status:200 } def updateGuest(){ jsonData = (HashMap) request.JSON if(debugEnabled) if(debugEnabled) log.debug "updateGuest - ${jsonData}" if(getPermissions(jsonData?.token) == "rw"){ if(!state.guest) state.guest = [:] if(jsonData.guesttoken) { state.guest["${jsonData.guesttoken}"]=[:] state.guest["${jsonData.guesttoken}"]?.name = jsonData.name state.guest["${jsonData.guesttoken}"]?.defaultDashId = jsonData.defaultDashId state.guest["${jsonData.guesttoken}"]?.permissions = jsonData.permissions def bodyText = JsonOutput.toJson([status:"ok"]) render contentType:'application/json', data: "$bodyText", status:200 } else { def bodyText = JsonOutput.toJson([staus:"fail", message:"Guest Token Missing"]) render contentType:'application/json', data: "$bodyText", status:400 } } else { def bodyText = JsonOutput.toJson([staus:"fail", message:"Access Denied"]) render contentType:'application/json', data: "$bodyText", status:403 } } def getGuestList(){ rMap = [:] if(!state.guest) state.guest = [:] state.guest.each{ if(debugEnabled) log.debug "${it.properties}" tMap = [:] tMap.put("name","${it.value.name}") tMap.put("permissions","${it.value.permissions}") tMap.put("defaultDashId","${it.value.defaultDashId}") rMap.put("${it.key}",tMap) } zMap = [status:"ok"] zMap+=[guestList:rMap] def bodyText = JsonOutput.toJson(zMap) render contentType:'application/json', data: "$bodyText", status:200 } def deleteGuest(){ jsonData = (HashMap) request.JSON if(getPermissions(jsonData?.token) == "rw"){ tempMap = state.guest state.guest = [:] tempMap.each{ if(jsonData.guesttoken != it.key){ state.guest["${it.key}"]=[:] state.guest["${it.key}"].name = it.value.name state.guest["${it.key}"].defaultDashId = it.value.defaultDashId state.guest["${it.key}"].permissions = it.value.permissions } } def bodyText = JsonOutput.toJson([status:"ok"]) render contentType:'application/json', data: "$bodyText", status:200 } else { def bodyText = JsonOutput.toJson([staus:"invalid", message:"Access Denied"]) render contentType:'application/json', data: "$bodyText", status:403 } } def deviceList(){ devMap = [] assignedDevices.each{ devMap=devMap+[id:"${it.properties.id}",name:"${it.properties.name}",label:"${it.properties.label}",room:"${it.properties.roomName}"] } def bodyText = JsonOutput.toJson(devMap) render contentType:'application/json', data: "$bodyText", status:200 } def deviceListDetailed(){ devMap = [] assignedDevices.each{ tMap=[id:"${it.properties.id}",name:"${it.properties.name}",label:"${it.properties.label}",type:"${it.device.deviceTypeName}", date:"${it.properties.lastActivity}",model:"${it.device.properties.data.model}",manufacturer:"${it.device.properties.data.manufacturer}",room:"${it.properties.roomName}"] aMap=[:] it.currentStates.each{ aMap.put("${it.name}","${it.value}") } cMap=[] it.properties.supportedCommands.each{ cMap+=[command:"${it}"] } tMap+=[attributes:aMap]+[commands:cMap] devMap=devMap+tMap } def bodyText = JsonOutput.toJson(devMap) render contentType:'application/json', data: "$bodyText", status:200 } def deviceDetails(){ assignedDevices.each{ if(it.properties.id == params.devId) { tMap=[id:"${it.properties.id}",name:"${it.properties.name}",label:"${it.properties.label}",type:"${it.device.deviceTypeName}",room:"${it.properties.roomName}",date:"${it.properties.lastActivity}",model:"${it.device.properties.data.model}",manufacturer:"$it.device.properties.data.manufacturer}"] aMap=[:] it.currentStates.each{ aMap.put("${it.name}","${it.value}") } cMap=[] it.properties.supportedCommands.each{ cMap+=[command:"${it}"] } tMap+=[attributes:aMap]+[commands:cMap] } } def bodyText = JsonOutput.toJson(tMap) render contentType:'application/json', data: "$bodyText", status:200 } def deviceCommandList(){ assignedDevices.each{ if(it.properties.id == params.devId) { cMap=[] it.properties.supportedCommands.each{ cMap+=[command:"${it}"] } } } def bodyText = JsonOutput.toJson(cMap) render contentType:'application/json', data: "$bodyText", status:200 } def deviceEventList(){ assignedDevices.each{d-> if(d.properties.id == params.devId) { eList = d.events() eMap = [] eList.each { eMap += [device_id:"${d.properties.id}",name:"${d.properties.name}",label:"${d.properties.label}",stateName:"${it.name}",value:"${it.value}",date:"${it.getDate()}",isStateChange:"${it.isStateChange}",source:"${it.source}"] } } } def bodyText = JsonOutput.toJson(eMap) render contentType:'application/json', data: "$bodyText", status:200 } def deviceCapabilities(){ if(debugEnabled) log.debug "Capabilities" assignedDevices.each{ if(it.properties.id == params.devId) { tMap=[capabilities:"${it.capabilities}"]//+[attributes:aMap] } } def bodyText = JsonOutput.toJson(tMap) render contentType:'application/json', data: "$bodyText", status:200 } def deviceIssueCommandDelay(){ assignedDevices.each{d-> if(d.properties.id == params.devId) { if(debugEnabled) log.debug "$d ${params.cmd}" runIn(params.seconds.toInteger(),"commandDelay",[overwrite:false,data:[id:"${params.devId}",cmd:"${params.cmd}",secVal:"${params.secValue}"]]) } } def bodyText = JsonOutput.toJson([status:"ok"]) render contentType:'application/json', data: "$bodyText", status:200 } def commandDelay(data) { if(debugEnabled) log.debug "${data} ${data['id']} ${data['cmd']} ${data['secVal']}" assignedDevices.each{d-> if(debugEnabled) log.debug "${d.properties.id} == ${data['id']}" if(d.properties.id == data.id) { if(debugEnabled) log.debug "$d ${data.cmd}" if(data.secVal == "null") data.secVal = null d."${data.cmd}"(data.secVal) } } } def deviceIssueCommand(){ assignedDevices.each{d-> if(d.properties.id == params.devId) { if(debugEnabled) log.debug "$d ${params.cmd}" d."${params.cmd}"(params.secValue) } } def bodyText = JsonOutput.toJson([status:"ok"]) render contentType:'application/json', data: "$bodyText", status:200 } def getHsm(){ def bodyText = JsonOutput.toJson([hsm:"${location.hsmStatus}"]) render contentType:'application/json', data: "$bodyText", status:200 } def getMode(){ locationMap =[] location.modes.each{ if("$it" == location.mode) actMode = "true" else actMode = "false" locationMap+=[name:"$it", active:"$actMode"] } def bodyText = JsonOutput.toJson(locationMap) render contentType:'application/json', data: "$bodyText", status:200 } private generateRandomToken(int length) { def characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" def token = "" // Create a new instance of Random class def random = new Random() // Generate each character of the password (1..length).each { def randomIndex = random.nextInt(characters.length()) def randomCharacter = characters.charAt(randomIndex) token += randomCharacter } return token } private String getPermissions(token) { if(token == state.admToken) return "rw" else if(state.guest["$token"]) return state.guest["$token"]?.permissions else return "fail" } private String sha256(String message, String salt) { MessageDigest md = MessageDigest.getInstance("SHA-256") md.update(salt.getBytes()); byte[] digest = md.digest(message.getBytes()) StringBuffer hexString = new StringBuffer() for (int i = 0;i