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"
}
}