preferences { input name: "IP", type: "string", title: "Tailwind Controller IP", required: "True" input name: "cName", type: "string", title: "Tailwind Controller Name", required: "True", description: 'Changes the name for the controller displayed in dashboards, DOES affect children unique deviceNetworkId. Changing this will re-create the children devices.' input name: "token", type: "password", title: "Local Command Key", required: "True", description: 'login with your tailwind app credentials to https://web.gotailwind.com/, go to Local Control Key, create a new local command key. This is per-account and is the same for each device you may have on your account.' input name: "doorCount", type: "number", title: "Number of Doors", required: "True", range: "0..3", defaultValue : 1 input name: "interval", type: "enum", title: "Polling interval", required: "True", options: ["1", "5", "10", "15", "30"], defaultValue : 1, description: ' Main polling interval for when nothing is happening, this is in Minutes so as not to hog resources on the Hub. If you want it more frequently than 1 minute, you can use Rules.' input name: "fastPollInterval", type: "number", title: "Fast Polling interval", required: "True", range: "1-30", defaultValue : 2, description: 'This polling interval is used when an action has been performed (such as open/close) and will update the status of the door triggered.' input name: "garageDoorTimeout", type: "number", title: "Door Open/Close timeout", required: "True", defaultValue : 60, description: ' Seconds. How long should faster polling be run before giving up on waiting to check and see if the door status has changed after issuing a command.' input name: "debugEnable", type: "bool", title: "Enable debug logging?", defaultValue: true, description: 'for 2 hours' if(doorCount > 0){input name: "d1Name", type: "string", title: "Door 1 Name", required: "false", defaultValue : "Door 1", description: 'Changes the name for Door 1 displayed in dashboards, does not affect children unique deviceNetworkId. Changing this will have no effect on the children devices being re-created.'} if(doorCount > 1){input name: "d2Name", type: "string", title: "Door 2 Name", required: "false", defaultValue : "Door 2", description: 'Changes the name for Door 2 displayed in dashboards, does not affect children unique deviceNetworkId. Changing this will have no effect on the children devices being re-created.'} if(doorCount == 3){input name: "d3Name", type: "string", title: "Door 3 Name", required: "false", defaultValue : "Door 3", description: 'Changes the name for Door 3 displayed in dashboards, does not affect children unique deviceNetworkId. Changing this will have no effect on the children devices being re-created.'} } metadata { definition ( name: "Tailwind Garage Door", namespace: "dabtailwind-gd", author: "dbadge", importUrl: "https://raw.githubusercontent.com/Gelix/HubitatTailwind/main/tailwinddriver.groovy" ) { capability "Polling" attribute "Status", "string" command "childOpen", ["integer"] command "childClose", ["integer"] } } def installed() { //log.info "Clearing schedule for Polling interval" //unschedule() //init() } def uninstalled() { getChildDevices().each { deleteChildDevice("${it.deviceNetworkId}") } } def updated() { log.info "Clearing schedule for Polling interval" unschedule() //disable logging after 2 hours if (debugEnable) runIn(7200,disableDebug) init() } def init() { log.info "Scheduling Polling interval for ${settings.interval} minute(s)..." addChildren() sendEvent(name: "Status", value: 0) //schedule("0/${settings.interval} * * ? * * *", poll) if (settings.interval == "1") runEvery1Minute(poll) else if (settings.interval == "5") runEvery5Minutes(poll) else if (settings.interval == "10") runEvery10Minutes(poll) else if (settings.interval == "15") runEvery15Minutes(poll) else if (settings.interval == "30") runEvery30Minutes(poll) poll() } def disableDebug(String level) { log.info "Timed elapsed, disabling debug logging" device.updateSetting("debugEnable", [value: 'false', type: 'bool']) } void addChildren(){ int dc = doorCount.toString().toInteger() //Cleanup any children that are no longer needed due to doorCount change or name mismatch getChildDevices().each { spl = it.deviceNetworkId.split(':') if(spl[0] != cName || spl[1].toInteger() > dc){ if(debugEnable) log.debug "delete ${it.deviceNetworkId}" deleteChildDevice("${it.deviceNetworkId}") } } //loop through up to doorCount to create children for (int c = 0; c < dc; c++) { def d = c + 1 def dn="" if (d == 1){dn = d1Name} if (d == 2){dn = d2Name} if (d == 3){dn = d3Name} if(debugEnable) log.debug ("${cName}:${d}") def cd = getChildDevice("${cName}:${d}") if(!cd) { cd = addChildDevice("dabtailwind-gd","Tailwind Garage Door Child Device","${cName}:${d}", [label: "${cName} : ${dn}", name: "${d}", isComponent: true]) if(cd && debugEnable){ if(debugEnable) log.debug "Child device ${cd.displayName} was created" }else if (!cd){ log.error "Could not create child device" } } if(debugEnable) log.debug "deviceNetworkId ${cd.deviceNetworkId}=${cName}:${d} name ${cd.name}=${d} label ${cd.label}=${cName} : ${dn}" if(cd.label != "${cName} : ${dn}") { if(debugEnable) log.debug "Correcting child label mismatch ${cd.label}=${cName} : ${dn}" cd.label = "${cName} : ${dn}" } if(cd.name != "${d}") { if(debugEnable) log.debug "Correcting child name mismatch ${cd.name}=${d}" cd.name = "${d}" } } } def poll() { def s = checkStatus() def old = device.currentValue("Status").toInteger() //only set status if it changed. a lot less event spam this way. if(s != old) { if(debugEnable) log.debug "Status changed from ${old} to ${s}" setDoorStatus(s) } } def openClose(String command, Integer doorNumber){ def desiredStatus = "closed" def Integer cmd = doorNumber * -1 if(doorNumber == 3) cmd = cmd - 1 //assuming documentation is correct on -4 and 4 for door #3, another user reported this being 3, but mine still seems to be 4. if(command == "open") { desiredStatus = "open" cmd = cmd * -1 } log.info "Attempting to ${command} door ${doorNumber}" def postParams = [uri: "http://${IP}/cmd", body : "${cmd}", headers: ['TOKEN' : "${token}"]] if(debugEnable) log.debug postParams httpPost(postParams) { def resp -> if(debugEnable) log.debug "${command} Response (should match ${cmd}): ${resp.data}" if ("${resp.data}" != "${cmd}" ) { if(debugEnable) log.debug "in 1 second, start polling every ${fastPollInterval} seconds for door to ${desiredStatus}." //schedule to run the refresh for rapid updates on dashboard runIn(1, "postActionRefresh", [data:["desiredStatus":"${desiredStatus}","doorNumber":doorNumber]]) } } } void postActionRefresh(data){ def Integer loopSpeed = fastPollInterval String desiredStatus = data.get("desiredStatus") def Integer doorStatus = checkStatus() Integer doorNumber = data.get("doorNumber").toInteger() if(debugEnable) log.debug "Now polling every ${loopSpeed} seconds for door to ${desiredStatus}." if(debugEnable) log.debug "${doorStatus} Door #${doorNumber} Desired Status: ${desiredStatus} Current status: ${doorCheck(doorNumber,doorStatus)}" def Integer i = 0 //count seconds elapsed after door command while ( doorCheck(doorNumber, doorStatus) != desiredStatus){ doorStatus = checkStatus() if(debugEnable) log.debug "Door #${doorNumber} Desired Status: ${desiredStatus} Current status: ${doorCheck(doorNumber,doorStatus)}" pauseExecution(loopSpeed * 1000) //break out of loop after a period, infinite loops are bad i += loopSpeed if(i >= garageDoorTimeout) { log.warn "${garageDoorTimeout} seconds is too long for a door, probably something went wrong physically (blocked sensor, stuck/etc)." break } } if (doorCheck(doorNumber, doorStatus) == desiredStatus){ setDoorStatus(doorStatus) log.info "Completed ${desiredStatus} Successfully on Door #${doorNumber} Desired Status: ${desiredStatus} Current status: ${doorCheck(doorNumber,doorStatus)}" } else { log.warn "Failed to ${desiredStatus} on Door #${doorNumber} Desired Status: ${desiredStatus} Current status: ${doorCheck(doorNumber,doorStatus)}" } } def doorCheck(Integer doorNumber, Integer doorStatus){ checkNumber = doorNumber -1 //the statusCode is 0,1,2 so subtract 1 r = getDoorOpenClose(getDoorStatus(doorStatus,checkNumber)) return r } def childOpen(Integer doorNumber){ open(doorNumber) } def childClose(Integer doorNumber){ close(doorNumber) } def open(Integer doorNumber) { openClose("open",doorNumber) } def close(Integer doorNumber) { openClose("close",doorNumber) } def checkStatus() { def params = [uri : "http://${ IP }/status", headers: [ 'TOKEN' : "${token}"]] httpGet(params) {resp -> if(debugEnable) log.debug "POST Door Status: ${resp.data}" return resp.data.toInteger() } } void setDoorStatus(status){ if(debugEnable) log.debug "Setting Door Status attribute to ${status}" sendEvent(name: "Status", value: status) for(int i =0; i < doorCount.toInteger(); i++){ ds = getDoorStatus(status,i) dStatus = getDoorOpenClose(ds) if(debugEnable) log.debug "Real door ${i+1} is ${ds} ${dStatus}" setChildStatus(i+1, dStatus) } } def getDoorStatus(Integer status, Integer door){ statusCodes=[ [-1, -2, -4], //[closed,closed,closed] [1, -2, -4], //[open,closed,closed] [-1, 2, -4], //[closed,open,closed] [1, 2, -4], //[open,open,closed] [-1, -2, 4], //[closed,closed,open] [1, -2, 4], //[open,closed,open] [-1, 2, 4], //[closed,open,open] [1, 2, 4] //[open,open,open] ] return statusCodes[status][door] } void setChildStatus(dNum, status){ def cd = getChildDevice("${cName}:${dNum}") if(cd.latestValue("door") == status){ if(debugEnable) log.debug "Child device ${cName}:${dNum} Matches real door" } else{ if(debugEnable) log.debug "Child device ${cName}:${dNum} DOESN'T match real door, update child to match" cd.sendEvent(name:"door", value:"${status}") } if (status ==~ /open|closed/ && cd.latestValue('contact') != status) { cd.sendEvent(name:'contact', value:"${status}") } } def getDoorOpenClose(Integer curStatus) { if(curStatus < 0){ return "closed" } else if (curStatus > 0){ return "open" } else { return "unknown" } }