Honeywell Total Connect

Has anybody developed a method for arming/disarming a Honeywell alarm panel? Either through TotalConnect 2,0 or locally (if local to panel is even possible)

I believe TotalConnect is the only option and the EvoHome app is running fine on SmartThings and I too had hoped to leverage it from Hubitat. However the Hubitat hub app is only unidirectional and so you can’t ‘control’ things on ST from Hubitat this way. Which I feel is a shame, and people would really benefit from a bidirectional facility being included in the app for exactly the reason in your original post.

However, and assuming you have an ST Hub, you can use Kevin’s other hub app and create a virtual device in ST that is controllable from Hubitat and would hence allow you to do what you want by linking it to the TotalConnect app. Messier but should work

However… I would be very concerned about allowing ‘unset’ as an option by such simple network command rather than a pin feature.

K

… reading your OP I’m not sure you have an ST hub though ?..

I do have a ST hub, and I have ordered a Hubitat hub to convert to. I am currently using the following code that looks at the ST Smart Home Monitor and arms/disarms accordingly. Was hoping this code could be adapted to Hubitat? I do not know Groovy, so this is not something I can do. Is it possible?

/**
 *  TotalConnect
 *
 *  Copyright 2017 Yogesh Mhatre, Oendaril, jhstroebel@github
 *
 *  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.
 *
 */
 /*
Version: vSHM - Smart Home Monitor Edition
 - Per request, the SmartApp is modified to subscribe to your SmartThings Smart Home Monitor Status.

 */

include 'asynchttp_v1'

definition(
    name: "TotalConnect SHM edition V1",
    namespace: "Security",
    author: "Yogesh Mhatre",
    description: "Total Connect App to lock/unlock your home based on SmartThings Smart Home Monitor Status",
    category: "My Apps",
    iconUrl: "https://s3.amazonaws.com/yogi/TotalConnect/150.png",
    iconX2Url: "https://s3.amazonaws.com/yogi/TotalConnect/300.png")

preferences {
	page(name: "authenticate", content: "authenticate")
  page(name: "selectLocation", content: "selectLocation")
  page(name: "confirmation", content: "confirmation")
}

// First Page for authentication
def authenticate() {
	dynamicPage(name: "authenticate", title: "Total Connect Login", nextPage: "selectLocation",uninstall: true, install: false){
    section("Give your TotalConnect credentials") {
      paragraph "It is recommended to make another total connect user for SmartThings. This user should have a passcode SET"
      input("userName", "text", title: "Username", description: "Your username for TotalConnect")
      input("password", "password", title: "Password", description: "Your Password for TotalConnect", submitOnChange:true)
    }
    section("Backend TotalConnect Values - DO NOT CHANGE", hideable: true, hidden: true) {
      paragraph "These are required for login. They typically do not change"
      input("applicationId", "text", title: "Application ID currently - 14588", description: "Application ID", defaultValue: "14588")
      input("applicationVersion", "text", title: "Application Version currently - 3.24.1", description: "Application Version", defaultValue: "3.24.1")
    }
  }

}

// Second Page that pulls Location Map to select a Location for multiLocation users of TotalConnect
def selectLocation() {
  atomicState.applicationId = settings.applicationId
  atomicState.applicationVersion = settings.applicationVersion
  //log.debug "During authentication page applicationId is $atomicState.applicationId & applicationVersion is $atomicState.applicationVersion"
  getLocations()
  while (atomicState.locationMap == null)
    {
      pause(1000)
    } // This while loop is "bit disturbing, but needed. Until atomicState.locationMap does not have any value, we are making the app pause for a seconds. Typically in my observance, the while loop is only executed thrice or less"
  //log.debug "During selectLocation Page, LocationMap is $atomicState.locationMap"
  def locations = atomicState.locationMap
  def optionList = locations.keySet() as List
  //log.debug "OptionList are " + optionList
  dynamicPage(name: "selectLocation", title: "Select the Location of your Total Connect Alarm",nextPage: "confirmation", uninstall: true, install: false) {
    section("Select from the following Locations for Total Connect.") {
      input(name: "selectLocation", type: "enum", required: true, title: "Select the Location", options:optionList, multiple: false, submitOnChange:true)
    }
  }
}

// Third page to store LocationID & DeviceID for SmartApp use
def confirmation(){
  //log.debug "Option selected is $settings.selectLocation"
  def selectedLocation = settings.selectLocation
  log.debug "During Confirmation Page, LocationMap: $atomicState.locationMap, DeviceMap: $atomicState.deviceMap"
  def locations = atomicState.locationMap
  def devices = atomicState.deviceMap
  def deviceId = devices["${selectedLocation}"]
  def locationId = locations["${selectedLocation}"]
  log.debug "DeviceId is $deviceId & LocationId is $locationId"
  dynamicPage(name: "confirmation", title: "Selected LocationID & DeviceID", uninstall: true, install: true) {
    section("DO NOT CHANGE - These values are fetched from your selected Location.") {
      input(name: "locationId", type: "text", required: true, title: "LocationID", defaultValue: locationId)
      input(name: "deviceId", type: "text", required: true, title: "DeviceID", defaultValue: deviceId)
    }
  }
}

// SmartThings defaults
def installed() {
	  log.debug "Installed with settings: Username: $settings.userName, ApplicationId: $settings.applicationId & ApplicationVersion: $settings.applicationVersion, DeviceId: $settings.deviceId & LocationId: $settings.locationId"
    subscribe(location, "alarmSystemStatus", alarmStatusHandler)
}

def updated() {
  log.debug "Updated with settings: Username: $settings.userName, ApplicationId: $settings.applicationId & ApplicationVersion: $settings.applicationVersion, DeviceId: $settings.deviceId & LocationId: $settings.locationId"
	  unsubscribe()
    subscribe(location, "alarmSystemStatus", alarmStatusHandler)
}

// Logic for Triggers based on mode change of SmartThings
def alarmStatusHandler(evt) {
    	if (evt.value == "away") {
            	log.debug "Smart Home Monitor is set to Away, Performing ArmAway"
            	armAway()
            }
        else if (evt.value == "stay") {
            	log.debug "Smart Home Monitor is set to Night, Performing ArmStay"
            	armStay()
            }
        else if (evt.value == "off") {
            	log.debug "Smart Home Monitor is set to Home, Performing Disarm"
            	disarm()
        }
}

// disarm Function
def disarm() {
	if(isTokenValid())
    	disarmAuthenticated()
    else {
		login(disarmAuthenticated)
    }
}

def disarmAuthenticated() {
	tcCommandAsync("DisarmSecuritySystem", [SessionID: state.token, LocationID: settings.locationId, DeviceID: settings.deviceId, UserCode: '-1'], 0, "disarm")
}

// armStay Function
def armStay() {
	if(isTokenValid())
    	armStayAuthenticated()
    else {
		login(armStayAuthenticated)
    }
}

def armStayAuthenticated() {
	tcCommandAsync("ArmSecuritySystem", [SessionID: state.token, LocationID: settings.locationId, DeviceID: settings.deviceId, ArmType: 1, UserCode: '-1'], 0, "armStay")
}

// armAway Function
def armAway() {
	if(isTokenValid())
    	armAwayAuthenticated()
    else {
		login(armAwayAuthenticated)
    }
}

def armAwayAuthenticated() {
	tcCommandAsync("ArmSecuritySystem", [SessionID: state.token, LocationID: settings.locationId, DeviceID: settings.deviceId, ArmType: 0, UserCode: '-1'], 0 , "armAway")
}

// Login Function.
def login(callback) {
	//log.debug "Executed login"
    tcCommandAsync("AuthenticateUserLogin",  [userName: settings.userName , password: settings.password, ApplicationID: settings.applicationId, ApplicationVersion: settings.applicationVersion], 0, callback)
}

def loginResponse(token, callback) {
  if(token != null) {
    //log.debug "new token is ${token}"
      state.token = "${token}"
      state.tokenRefresh = now()
  }
    switch(callback) {
      case "refresh":
          refresh()
          break
        case "refreshAuthenticated":
          refreshAuthenticated()
          break
        case "getSessionDetails":
        	getSessionDetails()
          break
        case "armAway":
        	armAway()
        	break
        case "armAwayAuthenticated":
        	armAwayAuthenticated()
        	break
        case "armStay":
        	armStay()
        	break
        case "armStayAuthenticated":
        	armStayAuthenticated()
        	break
        case "getPanelMetadata":
        	getPanelMetadata()
        	break
        case "disarm":
        	disarm()
        	break
        default:
            return
        break
    }
}

def logout() {
    tcCommandAsync("Logout",  [SessionID: state.token], 0, "logout")
} //Takes token as argument

// Asyn APIs
def tcCommandAsync(path, body, retry, callback) {
  //log.debug "tcCommandAsync was Executed"
	String stringBody = ""

    body.each { k, v ->
    	if(!(stringBody == "")) {
        	stringBody += "&" }
        stringBody += "${k}=${v}"
    }//convert Map to String

	//log.debug "stringBody: ${stringBody}"

    def params = [
		uri: "https://rs.alarmnet.com/TC21API/TC2.asmx/",
		path: path,
    	body: stringBody,
        requestContentType: "application/x-www-form-urlencoded",
        contentType: "application/xml"
    ]

    def handler

    switch(path) {
        case "GetPanelMetaDataAndFullStatusEx":
        	handler = "panel"
            break
        case "GetZonesListInStateEx":
        	handler = "zone"
            break
        case "AuthenticateUserLogin":
        	handler = "login"
            break
        case "GetSessionDetails":
        	handler = "details"
            break
        case "ArmSecuritySystem":
          handler = "refresh"
            break
        case "DisarmSecuritySystem":
        	handler = "refresh"
            break
        default:
        	handler = "none"
            break
    }//define handler based on method called

    def data = [
    	path: path,
        body: stringBody,
        handler: handler,
        callback: callback,
        retry: retry
    ] //Data for Async Command.  Params to retry, handler to handle, and retry count if needed

    try {
    	asynchttp_v1.post('asyncResponse', params, data)
        //log.debug "Sent asynchhttp_v1.post(responseHandler, ${params}, ${data})"
    } catch (e) {
    	log.error "Something unexpected went wrong in tcCommandAsync: ${e}"
	}//try / catch for asynchttpPost
}//async post command

def asyncResponse(response, data) {
    //log.debug "asyncresponse was Executed"
    if (response.hasError()) {
        log.debug "error response data: ${response.errorData}"
        try {
            // exception thrown if xml cannot be parsed from response
            log.debug "error response xml: ${response.errorXml}"
        } catch (e) {
            log.warn "error parsing xml: ${e}"
        }
        try {
            // exception thrown if json cannot be parsed from response
            log.debug "error response json: ${response.errorJson}"
        } catch (e) {
            log.warn "error parsing json: ${e}"
        }
    }

    response = response.getXml()
 	//log.debug "data:  ${data}"
  //log.debug "response received: ${response}"
    try {
    	def handler = data.get('handler')
        def callback = data.get('callback')

        if(handler == "login") {
            if(response.ResultCode == "0") {
            	loginResponse(response.SessionID, callback)
            }
            else {
                log.error "Command Type: ${data} failed with ResultCode: ${resultCode} and ResultData: ${resultData}"
            }
        }
        else {
            //validate response
            def resultCode = response.ResultCode
            def resultData = response.ResultData

            switch(resultCode) {
                case "0": //Successful Command
                case "4500": //Successful Command for Arm Action
                    state.tokenRefresh = now() //we ran a successful command, that will keep the token alive

                    //log.debug "Handler: ${data.get('handler')}"
                    switch(handler) {
                        case "details":
                            //details handler would be executed only when you need LocationID & DeviceID, that is during initial Setup & Updates.
                            def locationId
                            def deviceId
                            def locationName
                            Map locationMap = [:]
                            Map deviceMap = [:]
                            response.Locations.LocationInfoBasic.each
                            {
                                LocationInfoBasic ->
                                locationName = LocationInfoBasic.LocationName
                                locationId = LocationInfoBasic.LocationID
                                deviceId = LocationInfoBasic.DeviceList.DeviceInfoBasic.DeviceID
                                locationMap["${locationName}"] = "${locationId}"
                                deviceMap["${locationName}"] = "${deviceId}"
                            }
                            atomicState.locationMap = locationMap
                            atomicState.deviceMap = deviceMap
                            log.debug "During 'details' handler, LocationMap is $atomicState.locationMap & DeviceMap is $atomicState.deviceMap"
                            break
                        case "panel":
                            updateAlarmStatus(getAlarmStatus(response))
                            break
                        case "refresh":
                            refresh()
                            break
                        default:
                            return
                            break
                    }//switch(data)
                    break
                case "-102":
                    //this means the Session ID is invalid, needs to login and try again
                    log.error "Command Type: ${data} failed with ResultCode: ${resultCode} and ResultData: ${resultData}"
                    log.debug "Attempting to refresh token and try again for method ${callback}"
                    state.token = null
                    if(state.loginRetry == null || state.loginRetry == 0) {
                      state.loginRetry = 1;
                      state.token = null
                      login(callback)
                    }
                    else {
                      state.loginRetry = 0;
                    }
                    break
                case "4101": //We are unable to connect to the security panel. Please try again later or contact support
                case "4108": //Panel not connected with Virtual Keypad. Check Power/Communication failure
                case "-4002": //The specified location is not valid
                case "-4108": //Cannot establish a connection at this time. Please contact your Security Professional if the problem persists.
                default: //Other Errors
                    log.error "Command Type: ${data} failed with ResultCode: ${resultCode} and ResultData: ${resultData}"
                    break
            }//switch
        }
	} catch (SocketTimeoutException e) {
        //identify a timeout and retry?
		log.error "Timeout Error: $e"
    } catch (e) {
    	log.error "Something unexpected went wrong in asyncResponse: $e"
	}//try / catch for httpPost
}//asyncResponse

// Other necessary functions.
def isTokenValid() {
	def isValid = true
    if(state.token == null) {
    	isValid = false
    }
    else {
        Long timeSinceRefresh = now() - (state.tokenRefresh != null ? state.tokenRefresh : 0)

        //return false if time since refresh is over 4 minutes (likely timeout)
        if(timeSinceRefresh > 240000) {
            state.token = null
            isValid = false
        }
    }

    return isValid
} // This is a logical check only, assuming known timeout values and clearing token on loggout.  This method does no testing of the actua


def getSessionDetails() {
	tcCommandAsync("GetSessionDetails", [SessionID: state.token, ApplicationID: settings.applicationId, ApplicationVersion: settings.applicationVersion], 0, "getSessionDetails") //This updates panel status
}

def getLocations() {
	login(getSessionDetails)
}

def getPanelMetadata() {
	tcCommandAsync("GetPanelMetaDataAndFullStatusEx", [SessionID: state.token, LocationID: settings.locationId, LastSequenceNumber: 0, LastUpdatedTimestampTicks: 0, PartitionID: 1], 0, "getPanelMetadata") //This updates panel status
}

def refresh() {
	if(isTokenValid()) {
    	refreshAuthenticated()
    }
    else {
    	login(refreshAuthenticated)
    }
}

def refreshAuthenticated() {
	getPanelMetadata() // Gets AlarmCode
}

def updateAlarmStatus(alarmCode) {
    if(state.alarmCode != alarmCode) {
        if (alarmCode == "10200") {
            log.debug "Status is: Disarmed"
            sendPush("TotalConnect Alarm is Disarmed successfully")
            //sendEvent(name: "lock", value: "unlocked", displayed: "true", description: "Disarming")
            //sendEvent(name: "switch", value: "off", displayed: "true", description: "Disarming")
            //sendEvent(name: "status", value: "Disarmed", displayed: "true", description: "Refresh: Alarm is Disarmed")
        } else if (alarmCode == "10203") {
            log.debug "Status is: Armed Stay"
            sendPush("TotalConnect Alarm is armed in Night mode successfully")
            //sendEvent(name: "status", value: "Armed Stay", displayed: "true", description: "Refresh: Alarm is Armed Stay")
            //sendEvent(name: "switch", value: "on", displayed: "true", description: "Arming Stay")
        } else if (alarmCode =="10201") {
            log.debug "Status is: Armed Away"
            sendPush("TotalConnect Alarm is now Armed successfully")
        }
    }
	//logout(token)
    state.alarmCode = alarmCode
}

// Gets Panel Metadata.
def getAlarmStatus(response) {
	String alarmCode
	alarmCode = response.PanelMetadataAndStatus.Partitions.PartitionInfo.ArmingState
	state.alarmStatusRefresh = now()
	return alarmCode
} //returns alarmCode

I’m snowed under at the moment but someone else might have some helpful hints. It’s not a complex API to TotalConnect

/**
 *  TotalConnect
 *
 *  Copyright 2017 Yogesh Mhatre, Oendaril, jhstroebel@github
 *
 *  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.
 *
 */
 /*
Version: vSHM - Smart Home Monitor Edition
 - Per request, the SmartApp is modified to subscribe to your SmartThings Smart Home Monitor Status.

 */

include 'asynchttp_v1'

definition(
    name: "TotalConnect SHM edition V1",
    namespace: "Security",
    author: "Yogesh Mhatre",
    description: "Total Connect App to lock/unlock your home based on SmartThings Smart Home Monitor Status",
    category: "My Apps",
    iconUrl: "https://s3.amazonaws.com/yogi/TotalConnect/150.png",
    iconX2Url: "https://s3.amazonaws.com/yogi/TotalConnect/300.png")

preferences {
	page(name: "authenticate", content: "authenticate")
  page(name: "selectLocation", content: "selectLocation")
  page(name: "confirmation", content: "confirmation")
}

// First Page for authentication
def authenticate() {
	dynamicPage(name: "authenticate", title: "Total Connect Login", nextPage: "selectLocation",uninstall: true, install: false){
    section("Give your TotalConnect credentials") {
      paragraph "It is recommended to make another total connect user for SmartThings. This user should have a passcode SET"
      input("userName", "text", title: "Username", description: "Your username for TotalConnect")
      input("password", "password", title: "Password", description: "Your Password for TotalConnect", submitOnChange:true)
    }
    section("Backend TotalConnect Values - DO NOT CHANGE", hideable: true, hidden: true) {
      paragraph "These are required for login. They typically do not change"
      input("applicationId", "text", title: "Application ID currently - 14588", description: "Application ID", defaultValue: "14588")
      input("applicationVersion", "text", title: "Application Version currently - 3.24.1", description: "Application Version", defaultValue: "3.24.1")
    }
  }

}

// Second Page that pulls Location Map to select a Location for multiLocation users of TotalConnect
def selectLocation() {
  atomicState.applicationId = settings.applicationId
  atomicState.applicationVersion = settings.applicationVersion
  //log.debug "During authentication page applicationId is $atomicState.applicationId & applicationVersion is $atomicState.applicationVersion"
  getLocations()
  while (atomicState.locationMap == null)
    {
      pause(1000)
    } // This while loop is "bit disturbing, but needed. Until atomicState.locationMap does not have any value, we are making the app pause for a seconds. Typically in my observance, the while loop is only executed thrice or less"
  //log.debug "During selectLocation Page, LocationMap is $atomicState.locationMap"
  def locations = atomicState.locationMap
  def optionList = locations.keySet() as List
  //log.debug "OptionList are " + optionList
  dynamicPage(name: "selectLocation", title: "Select the Location of your Total Connect Alarm",nextPage: "confirmation", uninstall: true, install: false) {
    section("Select from the following Locations for Total Connect.") {
      input(name: "selectLocation", type: "enum", required: true, title: "Select the Location", options:optionList, multiple: false, submitOnChange:true)
    }
  }
}

// Third page to store LocationID & DeviceID for SmartApp use
def confirmation(){
  //log.debug "Option selected is $settings.selectLocation"
  def selectedLocation = settings.selectLocation
  log.debug "During Confirmation Page, LocationMap: $atomicState.locationMap, DeviceMap: $atomicState.deviceMap"
  def locations = atomicState.locationMap
  def devices = atomicState.deviceMap
  def deviceId = devices["${selectedLocation}"]
  def locationId = locations["${selectedLocation}"]
  log.debug "DeviceId is $deviceId & LocationId is $locationId"
  dynamicPage(name: "confirmation", title: "Selected LocationID & DeviceID", uninstall: true, install: true) {
    section("DO NOT CHANGE - These values are fetched from your selected Location.") {
      input(name: "locationId", type: "text", required: true, title: "LocationID", defaultValue: locationId)
      input(name: "deviceId", type: "text", required: true, title: "DeviceID", defaultValue: deviceId)
    }
  }
}

// SmartThings defaults
def installed() {
	  log.debug "Installed with settings: Username: $settings.userName, ApplicationId: $settings.applicationId & ApplicationVersion: $settings.applicationVersion, DeviceId: $settings.deviceId & LocationId: $settings.locationId"
    subscribe(location, "alarmSystemStatus", alarmStatusHandler)
}

def updated() {
  log.debug "Updated with settings: Username: $settings.userName, ApplicationId: $settings.applicationId & ApplicationVersion: $settings.applicationVersion, DeviceId: $settings.deviceId & LocationId: $settings.locationId"
	  unsubscribe()
    subscribe(location, "alarmSystemStatus", alarmStatusHandler)
}

// Logic for Triggers based on mode change of SmartThings
def alarmStatusHandler(evt) {
    	if (evt.value == "away") {
            	log.debug "Smart Home Monitor is set to Away, Performing ArmAway"
            	armAway()
            }
        else if (evt.value == "stay") {
            	log.debug "Smart Home Monitor is set to Night, Performing ArmStay"
            	armStay()
            }
        else if (evt.value == "off") {
            	log.debug "Smart Home Monitor is set to Home, Performing Disarm"
            	disarm()
        }
}

// disarm Function
def disarm() {
	if(isTokenValid())
    	disarmAuthenticated()
    else {
		login(disarmAuthenticated)
    }
}

def disarmAuthenticated() {
	tcCommandAsync("DisarmSecuritySystem", [SessionID: state.token, LocationID: settings.locationId, DeviceID: settings.deviceId, UserCode: '-1'], 0, "disarm")
}

// armStay Function
def armStay() {
	if(isTokenValid())
    	armStayAuthenticated()
    else {
		login(armStayAuthenticated)
    }
}

def armStayAuthenticated() {
	tcCommandAsync("ArmSecuritySystem", [SessionID: state.token, LocationID: settings.locationId, DeviceID: settings.deviceId, ArmType: 1, UserCode: '-1'], 0, "armStay")
}

// armAway Function
def armAway() {
	if(isTokenValid())
    	armAwayAuthenticated()
    else {
		login(armAwayAuthenticated)
    }
}

def armAwayAuthenticated() {
	tcCommandAsync("ArmSecuritySystem", [SessionID: state.token, LocationID: settings.locationId, DeviceID: settings.deviceId, ArmType: 0, UserCode: '-1'], 0 , "armAway")
}

// Login Function.
def login(callback) {
	//log.debug "Executed login"
    tcCommandAsync("AuthenticateUserLogin",  [userName: settings.userName , password: settings.password, ApplicationID: settings.applicationId, ApplicationVersion: settings.applicationVersion], 0, callback)
}

def loginResponse(token, callback) {
  if(token != null) {
    //log.debug "new token is ${token}"
      state.token = "${token}"
      state.tokenRefresh = now()
  }
    switch(callback) {
      case "refresh":
          refresh()
          break
        case "refreshAuthenticated":
          refreshAuthenticated()
          break
        case "getSessionDetails":
        	getSessionDetails()
          break
        case "armAway":
        	armAway()
        	break
        case "armAwayAuthenticated":
        	armAwayAuthenticated()
        	break
        case "armStay":
        	armStay()
        	break
        case "armStayAuthenticated":
        	armStayAuthenticated()
        	break
        case "getPanelMetadata":
        	getPanelMetadata()
        	break
        case "disarm":
        	disarm()
        	break
        default:
            return
        break
    }
}

def logout() {
    tcCommandAsync("Logout",  [SessionID: state.token], 0, "logout")
} //Takes token as argument

// Asyn APIs
def tcCommandAsync(path, body, retry, callback) {
  //log.debug "tcCommandAsync was Executed"
	String stringBody = ""

    body.each { k, v ->
    	if(!(stringBody == "")) {
        	stringBody += "&" }
        stringBody += "${k}=${v}"
    }//convert Map to String

	//log.debug "stringBody: ${stringBody}"

    def params = [
		uri: "https://rs.alarmnet.com/TC21API/TC2.asmx/",
		path: path,
    	body: stringBody,
        requestContentType: "application/x-www-form-urlencoded",
        contentType: "application/xml"
    ]

    def handler

    switch(path) {
        case "GetPanelMetaDataAndFullStatusEx":
        	handler = "panel"
            break
        case "GetZonesListInStateEx":
        	handler = "zone"
            break
        case "AuthenticateUserLogin":
        	handler = "login"
            break
        case "GetSessionDetails":
        	handler = "details"
            break
        case "ArmSecuritySystem":
          handler = "refresh"
            break
        case "DisarmSecuritySystem":
        	handler = "refresh"
            break
        default:
        	handler = "none"
            break
    }//define handler based on method called

    def data = [
    	path: path,
        body: stringBody,
        handler: handler,
        callback: callback,
        retry: retry
    ] //Data for Async Command.  Params to retry, handler to handle, and retry count if needed

    try {
    	asynchttp_v1.post('asyncResponse', params, data)
        //log.debug "Sent asynchhttp_v1.post(responseHandler, ${params}, ${data})"
    } catch (e) {
    	log.error "Something unexpected went wrong in tcCommandAsync: ${e}"
	}//try / catch for asynchttpPost
}//async post command

def asyncResponse(response, data) {
    //log.debug "asyncresponse was Executed"
    if (response.hasError()) {
        log.debug "error response data: ${response.errorData}"
        try {
            // exception thrown if xml cannot be parsed from response
            log.debug "error response xml: ${response.errorXml}"
        } catch (e) {
            log.warn "error parsing xml: ${e}"
        }
        try {
            // exception thrown if json cannot be parsed from response
            log.debug "error response json: ${response.errorJson}"
        } catch (e) {
            log.warn "error parsing json: ${e}"
        }
    }

    response = response.getXml()
 	//log.debug "data:  ${data}"
  //log.debug "response received: ${response}"
    try {
    	def handler = data.get('handler')
        def callback = data.get('callback')

        if(handler == "login") {
            if(response.ResultCode == "0") {
            	loginResponse(response.SessionID, callback)
            }
            else {
                log.error "Command Type: ${data} failed with ResultCode: ${resultCode} and ResultData: ${resultData}"
            }
        }
        else {
            //validate response
            def resultCode = response.ResultCode
            def resultData = response.ResultData

            switch(resultCode) {
                case "0": //Successful Command
                case "4500": //Successful Command for Arm Action
                    state.tokenRefresh = now() //we ran a successful command, that will keep the token alive

                    //log.debug "Handler: ${data.get('handler')}"
                    switch(handler) {
                        case "details":
                            //details handler would be executed only when you need LocationID & DeviceID, that is during initial Setup & Updates.
                            def locationId
                            def deviceId
                            def locationName
                            Map locationMap = [:]
                            Map deviceMap = [:]
                            response.Locations.LocationInfoBasic.each
                            {
                                LocationInfoBasic ->
                                locationName = LocationInfoBasic.LocationName
                                locationId = LocationInfoBasic.LocationID
                                deviceId = LocationInfoBasic.DeviceList.DeviceInfoBasic.DeviceID
                                locationMap["${locationName}"] = "${locationId}"
                                deviceMap["${locationName}"] = "${deviceId}"
                            }
                            atomicState.locationMap = locationMap
                            atomicState.deviceMap = deviceMap
                            log.debug "During 'details' handler, LocationMap is $atomicState.locationMap & DeviceMap is $atomicState.deviceMap"
                            break
                        case "panel":
                            updateAlarmStatus(getAlarmStatus(response))
                            break
                        case "refresh":
                            refresh()
                            break
                        default:
                            return
                            break
                    }//switch(data)
                    break
                case "-102":
                    //this means the Session ID is invalid, needs to login and try again
                    log.error "Command Type: ${data} failed with ResultCode: ${resultCode} and ResultData: ${resultData}"
                    log.debug "Attempting to refresh token and try again for method ${callback}"
                    state.token = null
                    if(state.loginRetry == null || state.loginRetry == 0) {
                      state.loginRetry = 1;
                      state.token = null
                      login(callback)
                    }
                    else {
                      state.loginRetry = 0;
                    }
                    break
                case "4101": //We are unable to connect to the security panel. Please try again later or contact support
                case "4108": //Panel not connected with Virtual Keypad. Check Power/Communication failure
                case "-4002": //The specified location is not valid
                case "-4108": //Cannot establish a connection at this time. Please contact your Security Professional if the problem persists.
                default: //Other Errors
                    log.error "Command Type: ${data} failed with ResultCode: ${resultCode} and ResultData: ${resultData}"
                    break
            }//switch
        }
	} catch (SocketTimeoutException e) {
        //identify a timeout and retry?
		log.error "Timeout Error: $e"
    } catch (e) {
    	log.error "Something unexpected went wrong in asyncResponse: $e"
	}//try / catch for httpPost
}//asyncResponse

// Other necessary functions.
def isTokenValid() {
	def isValid = true
    if(state.token == null) {
    	isValid = false
    }
    else {
        Long timeSinceRefresh = now() - (state.tokenRefresh != null ? state.tokenRefresh : 0)

        //return false if time since refresh is over 4 minutes (likely timeout)
        if(timeSinceRefresh > 240000) {
            state.token = null
            isValid = false
        }
    }

    return isValid
} // This is a logical check only, assuming known timeout values and clearing token on loggout.  This method does no testing of the actua


def getSessionDetails() {
	tcCommandAsync("GetSessionDetails", [SessionID: state.token, ApplicationID: settings.applicationId, ApplicationVersion: settings.applicationVersion], 0, "getSessionDetails") //This updates panel status
}

def getLocations() {
	login(getSessionDetails)
}

def getPanelMetadata() {
	tcCommandAsync("GetPanelMetaDataAndFullStatusEx", [SessionID: state.token, LocationID: settings.locationId, LastSequenceNumber: 0, LastUpdatedTimestampTicks: 0, PartitionID: 1], 0, "getPanelMetadata") //This updates panel status
}

def refresh() {
	if(isTokenValid()) {
    	refreshAuthenticated()
    }
    else {
    	login(refreshAuthenticated)
    }
}

def refreshAuthenticated() {
	getPanelMetadata() // Gets AlarmCode
}

def updateAlarmStatus(alarmCode) {
    if(state.alarmCode != alarmCode) {
        if (alarmCode == "10200") {
            log.debug "Status is: Disarmed"
            sendPush("TotalConnect Alarm is Disarmed successfully")
            //sendEvent(name: "lock", value: "unlocked", displayed: "true", description: "Disarming")
            //sendEvent(name: "switch", value: "off", displayed: "true", description: "Disarming")
            //sendEvent(name: "status", value: "Disarmed", displayed: "true", description: "Refresh: Alarm is Disarmed")
        } else if (alarmCode == "10203") {
            log.debug "Status is: Armed Stay"
            sendPush("TotalConnect Alarm is armed in Night mode successfully")
            //sendEvent(name: "status", value: "Armed Stay", displayed: "true", description: "Refresh: Alarm is Armed Stay")
            //sendEvent(name: "switch", value: "on", displayed: "true", description: "Arming Stay")
        } else if (alarmCode =="10201") {
            log.debug "Status is: Armed Away"
            sendPush("TotalConnect Alarm is now Armed successfully")
        }
    }
	//logout(token)
    state.alarmCode = alarmCode
}

// Gets Panel Metadata.
def getAlarmStatus(response) {
	String alarmCode
	alarmCode = response.PanelMetadataAndStatus.Partitions.PartitionInfo.ArmingState
	state.alarmStatusRefresh = now()
	return alarmCode
} //returns alarmCode

Hi Gunnar, did you manage to get your Evohome working with HE?

Does anyone know how to connect a Honeywell WIFI thermostat to Hubitat? It uses Total Connect API.

I am currently converting over to Hubitat and need to manage my thermostat with Hubitat instead of going back to their native app or SmartThings aka Samsung Dumb Things!

Don't start on my that I am hating. But the wife approval after last outage is like this.
Wife: Give me your hammer.
Me: Why?
Wife: SmartThings must die. Your buttons don't work.
Me: Just checked my e-mail and they are fixing the outage.
Funny!

Perhaps this?

2 Likes

Thank you. Working on that now.

Works. And able to add it to the dashboard. Thanks.

1 Like

@csteele I am grateful for your diligence keeping up with the TCC changes. I was running 1.2.2 and wasn't aware of your update to 1.2.3.

Is there another thread I should subscribe to keep up with updates (because I didn't see it)?

Related, how does updateCheck work? My device has it scheduled for tomorrow, but I don't know what happens, or what I'm supposed to look for, when it runs and detects an update.

Thanks!

EDIT:
Further adding to my confoundment is the version shown in the DH:
Screenshot_20190801-191034

I pulled up the Drivers Code that is installed, and it looks like this:

updateCheck is just a once a week compare of the github json with version values against the driver's internals. All it does is .. a) nothing if they match or b) tell you there's an update, if they don't match. There's a third case of telling you the whole check thing didn't work, but I hope that I'm the only one that sees that :slight_smile:

The message will appear in State Variables, and look similar to:

55%20AM

1 Like

Thank you very much for sharing this, I have followed the instructions on GH and added the driver, then when adding the new "Device" in HE what should I select from Device type?

Many thanks in advance for the work you have done on this.
Kind regards
Paul

If the driver is installed, down under "User" drivers, you should see:

Thank you Cal, that's very kind and totally accurate, I fear I may have something else with Honey well as it only shows one simple button with a temperature of 68 on it. I was hoping to see about 50 devices come flooding through but hey ho I have it installed on the dashboard and will engage with Honeywell who are usually super responsive as well. Thank you.
Paul

@csteele Thanks for writing this driver. I currently have a two-zone system so I have a device for each of the zone's thermostats. I was setting up a shutoff routine and looking at the logs to see if everything was firing correctly and noticed this:

groovyx.net.http.HttpResponseException: Unauthorized on line 446 (setThermostatMode)

I appears the requested changes were still processed correctly through the API, is this an issue or is it just something I can ignore. I'm happy to share any system details you might need.

I also see plenty of Unauthorized especially when I'm testing and am hitting Honeywell's site every 30 seconds +

They do dynamically rate limit. And it's my interpretation that those messages are the result.

As you're new to the Driver, I'd expect you to be whacking buttons left and right... but realize that Honeywell may rate limit you. Back off for 5 mins and you'll get another 'heaping spoonful' of access. :slight_smile:

@csteele So actually I'm not totally convinced yet that's the issue. I read most of the posts about overutilizing the API before I implemented it. I'm testing the routines it's true but very slowly, each step that accesses the API has a 90-second delay between requests, do you think this is adequate? I also have my polling intervals set to 60 minutes on both devices.

Strange, I turned on debug logging and didn't get the error.

I must admit I don't use mine much.

I just looked at my event logs for the device and saw that the most recent two entries are 2 months apart. I clicked buttons on my dashboard and can see it's still working but I don't see the error today, and obviously any logs are too long ago.

On the other hand, it's entirely possible the 'cookie accumulation process' used during login has a flaw. I have it on my To-Do list to convert the driver to Async, but other things have pushed themselves in front. (The async part is trivial, it's the ordering of the result that is all the work.)

The Honeywell site does not seem to use a fixed rate limiter.. the site seems to be dynamic. I send a few dozen 'debug while developing' cycles without an issue. Then I get the error and back away for a couple moments. It's all fine again. I don't think a 90second delay is required, it's more the total number of requests in 1 min and 5 min that matter. Send 20 requests rapidly, then wait an 6 min, I would predict no Auth errors. 20 requests, wait 90 seconds, 20 requests.. I'd predict a throttle.