Carrier Infinity / Bryant Evolution compatibility?

Mr. Matt Nohr,

This morning I have been looking into what it would take to get this going. You mentioned that you have this thermostat and you are interested in getting it working with smartthings. I am surely not alone in joining you in this endeavor. I have gotten far enough into this to know that it’s a little over my abilities. It seems as copyninjas app is combining the infinitude code with a smartthings smart app and with your help we now have a way to connect to our thermostats via smartthings>carrier api>thermostat. We can read temperatures, settings and change setpoints etc… (thanks to infinitude). From here we need to set up a device handler in order to interface with this app. I have found documentation detailing this process with a zwave thermostat and it seems pretty straight forward. Would just need adapted for use with the copyninja application. I am willing to help in any way possible, but I have to ask? Do you have intentions to develop this further as an unofficial Compatibility?

Also installed, and was able to select my thermostat, login and see zones.

Thank you for the recent commits. I am really excited that progress is being made.

I got this working as well but only to the point of being able to Read values. I don’t see how anything can be controlled. Anyone know of any API paths to control the fan or set the temperature points?

Looks like I missed the “config” POST API at this URL https://www.app-api.ing.carrier.com/systems/XXXXXXXXXXX/config
I captured the packet when changing the temperature setting from 69 to 68 degrees and noticed that the whole entire settings state (including period settings for all zones whether they are active or not) is sent over the wire. Basically you can do a GET on the “config” URL, modify whatever you want and then do a POST to push the setting change.
For example: Here’s a diff for going from an automated schedule to a hold until 11:15am in away mode (this is a per zone setting).

>       <holdActivity/>
>       <hold>off</hold>
>       <otmr/>
---
<       <holdActivity>away</holdActivity>
<       <hold>on</hold>
<       <otmr>11:15</otmr>

I don’t think I will have the time to put this all together into a device handler + parent app hoping someone else can pull it off.

I’ve modified a bunch of stuff and created a working App and Device Handler for showing the current status based on the work done by copy-ninja and reusing DH for a smartthings thermostat. I did not have time to make anything pretty and this code is ugly but it works for Carrier Infinity Remote (feel free to make it better and post updates).

First the App:

/**
 *  Bryant MyEvolution (Connect)
 *
 *  Copyright 2015 Jason Mok
 *
 *  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.
 *
 */
definition(
    name: "Carrier Infinity Remote",
    namespace: "copy-ninja-mod",
    author: "Jason Mok Modded",
    description: "Connect to Carrier MyInfinity to control your thermostats",
    category: "SmartThings Labs",
    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch.png",
    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png",
)


preferences {
    page(name: "prefLogIn", title: "Carrier MyInfinity")
    page(name: "prefListDevice", title: "Carrier MyInfinity")
}

/* Preferences */
def prefLogIn() {
    def showUninstall = username != null && password != null
    return dynamicPage(name: "prefLogIn", title: "Connect to MyInfinity", nextPage:"prefListDevice", uninstall:showUninstall, install: false) {
        section("Login Credentials"){
            input("username", "text", title: "Username", defaultValue: "", description: "MyInfinity Username (case sensitive)")
            input("password", "password", title: "Password", description: "MyInfinity password (case sensitive)")
        }
        section("Advanced Options"){
            input(name: "polling", title: "Server Polling (in Minutes)", type: "int", description: "in minutes", defaultValue: "5" )
        }
    }
}

def prefListDevice() {
    if (login()) {
        setLookupInfo()
        def thermostatList = getThermostatList()
        log.debug "Query complete"
        if (thermostatList) {
        	log.debug "Got a list"
            return dynamicPage(name: "prefListDevice",  title: "Thermostats", install:true, uninstall:true) {
                section("Select which thermostat/zones to use"){
                    input(name: "thermostats", type: "enum", required:false, multiple:true, metadata:[values:thermostatList])
                }
            }
        } else {
        	log.debug "Empty list returned"
            return dynamicPage(name: "prefListDevice",  title: "Error!", install:false, uninstall:true) {
                section(""){ paragraph "Could not find any devices "  }
            }
        }
    } else {
        return dynamicPage(name: "prefListDevice",  title: "Error!", install:false, uninstall:true) {
            section(""){ paragraph "The username or password you entered is incorrect. Try again. " }
        }
    }
}

/* Initialization */
def installed() { initialize() }
def updated() {
    unsubscribe()
    initialize()
}
def uninstalled() {
    unschedule()
    unsubscribe()
    getAllChildDevices().each { deleteChildDevice(it.deviceNetworkId) }
}

def initialize() {
    // Set initial states
    /**
    state.polling = [ last: 0, rescheduler: now() ]
    state.data = [:]
    state.setData = [:]
    setLookupInfo()
    login()
    getThermostatList()
    **/
    state.data = [:]
    state.setData = [:]
    setLookupInfo()
    login()
    getThermostatList()
    log.debug "initialize"
	def devices = thermostats.collect { dni ->
    	//log.debug "Processing DNI: " + dni
		def d = getChildDevice(dni)
		if(!d) {
			d = addChildDevice("SmartThingsMod", "Carrier Thermostat", dni, null, ["label" : "Carrier Thermostat Zone " + dni.split("\\|")[2]])
			log.debug "----->created ${d.displayName} with id $dni"
		} else {
			log.debug "found ${d.displayName} with id $dni already exists"
		}
		return d
	}
    log.debug "Completerd creating devices"
	syncThermostats()
}

def refreshChild(dni) {
	log.debug "Refreshing for child " + dni
    syncThermostats()
	//def dni = [ app.id, systemID, zone.'@id'.text().toString() ].join('|')

    def d = getChildDevice(dni)
    if(d) {
    	d.zUpdate(state.data[dni].temperature,
            state.data[dni].thermostatOperatingState,
            state.data[dni].humidity,
            state.data[dni].heatingSetpoint,
            state.data[dni].coolingSetpoint,
            state.data[dni].thermostatFanMode,
            state.data[dni].thermostatActivityState)
		log.debug "Data sent to DH"
	}
    else {
    	log.debug "Somethig went wrong!"
    }
}

private syncThermostats() {
    log.debug "Syncing thermostats"
	if(!state.systemsList) {
    	log.error "System list missing"
        return
    }
    login()
    state.systemsList.each { systemName, systemID ->
        apiGet("/systems/" + systemID + "/status") { response ->
            if (response.status == 200) {
                response.data.zones.zone.each { zone ->
                    if (zone.enabled.text() == "on") {
                    	//DNI DNI: aaaaaaaa-bbbb-cccc-dddd-aaaaaaaaaaaa|zzzzzzzzzzz|1
                        def dni = [ app.id, systemID, zone.'@id'.text().toString() ].join('|')
                        //Get the current status of each device
                            state.data[dni] = [
                                temperature: zone.rt.toInteger(),
                                humidity: zone.rh.toInteger(),
                                coolingSetpoint: zone.clsp.toInteger(),
                                heatingSetpoint: zone.htsp.toInteger(),
                                thermostatFanMode: lookupInfo( "thermostatFanMode", zone.fan.text().toString(), true ),
                                thermostatOperatingState: lookupInfo( "thermostatOperatingState", response.data.mode.text().toString(), true ),
                                thermostatActivityState: zone.currentActivity.text().toString(),
                            ]
                            /***** DEBUG
                            log.debug "Temperature: " + state.data[dni].temperature
                            log.debug "Humidity: " + state.data[dni].humidity
                            log.debug "Heat set point: " + zone.htsp.toInteger()
                            log.debug "Fan: " + lookupInfo( "thermostatFanMode", zone.fan.text().toString(), true )
                            log.debug "Operating state: " + lookupInfo( "thermostatOperatingState", response.data.mode.text().toString(), true )
                            log.debug "Current schedule mode: " + zone.currentActivity.text().toString()
                            log.debug "=====Sync Done====="
                            *****/
                    }
                }
            }
        }
    }
}
// Listing all the thermostats you have in iComfort
private getThermostatList() {
    def thermostatList = [:]
    def systemsList = [:]
    state.data = [:]

    //Get Thermostat Mode lookups
    apiGet("/users/" + settings.username + "/locations") { response ->
        if (response.status == 200) {
            log.debug "APIRESP(users/USER/locations) " + response.data
            response.data.declareNamespace("atom":"http://www.w3.org/2005/Atom")
            response.data.location.each { location ->
                location.systems.system.each {
                    systemsList[it.'atom:link'.@title] = it.'atom:link'.@href.text().tokenize("/").last()
                }
            }
        }
    }
    state.systemsList = systemsList    
    systemsList.each { systemName, systemID ->
        apiGet("/systems/" + systemID + "/status") { response ->
            if (response.status == 200) {
                //log.debug "APIRESP(systems/id/status) " + response.data
                response.data.zones.zone.each { zone ->
                    if (zone.enabled.text() == "on") {
                    	//DNI DNI: AppId|SystemId|1
                        def dni = [ app.id, systemID, zone.'@id'.text().toString() ].join('|')
                        log.debug "APIRESP(systems/id/status) " + systemName.toString() + ": " + zone.name.text().toString() + " DNI: " + dni

                        thermostatList[dni] = systemName.toString() + ": " + zone.name.text().toString()

                        //Get the current status of each device
                            state.data[dni] = [
                                temperature: zone.rt.toInteger(),
                                humidity: zone.rh.toInteger(),
                                coolingSetpoint: zone.clsp.toInteger(),
                                heatingSetpoint: zone.htsp.toInteger(),
                                thermostatFanMode: lookupInfo( "thermostatFanMode", zone.fan.text().toString(), true ),
                                thermostatOperatingState: lookupInfo( "thermostatOperatingState", response.data.mode.text().toString(), true ),
                                thermostatActivityState: zone.currentActivity.text().toString(),
                            ]
                            log.debug "Temperature: " + state.data[dni].temperature
                            log.debug "Humidity: " + state.data[dni].humidity
                            log.debug "Heat set point: " + zone.htsp.toInteger()
                            log.debug "Fan: " + lookupInfo( "thermostatFanMode", zone.fan.text().toString(), true )
                            log.debug "Operating state: " + lookupInfo( "thermostatOperatingState", response.data.mode.text().toString(), true )
                            log.debug "Current schedule mode: " + zone.currentActivity.text().toString()
                            log.debug "=====Done====="
                    }
                }
            }
        }
        /*
        apiGet("/systems/" + systemID) { response ->
            if (response.status == 200) {
                log.debug "APIRESP(systems/id) " + response.data
                response.data.config.zones.zone.each { zone ->
                    if (zone.enabled.text() == "on") {
                        def dni = [ app.id, systemID, zone.'@id'.text().toString() ].join('|')
                        log.debug response.data.config.utilityEvent.maxLimit
						log.debug response.data.config.utilityEvent.minLimit
                        log.debug response.data.config.cfgdead
						log.debug "=====Done====="
                        //state.lookup.activity.putAt(dni, [:])
                        //state.lookup.coolingSetPointHigh.putAt(dni, (response.data.config.utilityEvent.maxLimit.toInteger() - response.data.config.cfgdead.toInteger()))
                        //state.lookup.coolingSetPointLow.putAt(dni, response.data.config.utilityEvent.minLimit.toInteger())
                        //state.lookup.heatingSetPointHigh.putAt(dni, response.data.config.utilityEvent.maxLimit.toInteger())
                        //state.lookup.heatingSetPointLow.putAt(dni, (response.data.config.utilityEvent.minLimit.toInteger() + response.data.config.cfgdead.toInteger()))
                        //state.lookup.differenceSetPoint.putAt(dni, response.data.config.cfgdead.toInteger())
                        //zone.activities.activity.each { activity -> state.lookup.activity[dni].putAt(activity.'@id'.toString(), "Activity :\n" + activity.'@id'.toString()) }
                        //if(state?.data) {
                        //    state.data[dni] = state.data[dni] + [ thermostatMode: lookupInfo( "thermostatMode", response.data.config.mode.text().toString(), true ) ]
                        //}
                    }
                }
            }
        }
        */
    }
    log.debug thermostatList
    return thermostatList
}

def setLookupInfo() {
    state.lookup = [
         thermostatOperatingState: [:],
         thermostatFanMode: [:],
         thermostatMode: [:],
         activity: [:],
         coolingSetPointHigh: [:],
         coolingSetPointLow: [:],
         heatingSetPointHigh: [:],
         heatingSetPointLow: [:],
         differenceSetPoint: [:],
         temperatureRangeF: [:]
     ]
    state.lookup.thermostatMode["off"] = "off"
    state.lookup.thermostatMode["cool"] = "cool"
    state.lookup.thermostatMode["heat"] = "heat"
    state.lookup.thermostatMode["fanonly"] = "off"
    state.lookup.thermostatMode["auto"] = "auto"
    state.lookup.thermostatOperatingState["heat"] = "heating"
    state.lookup.thermostatOperatingState["hpheat"] = "heating"
    state.lookup.thermostatOperatingState["cool"] = "cooling"
    state.lookup.thermostatOperatingState["off"] = "idle"
    state.lookup.thermostatOperatingState["fanonly"] = "fan only"
    state.lookup.thermostatFanMode["off"] = "auto"
    state.lookup.thermostatFanMode["low"] = "circulate"
    state.lookup.thermostatFanMode["med"] = "on"
    state.lookup.thermostatFanMode["high"] = "on"
}

// lookup value translation
def lookupInfo( lookupName, lookupValue, lookupMode ) {
    if (lookupName == "thermostatFanMode") {
        if (lookupMode) {
            return state.lookup.thermostatFanMode.getAt(lookupValue.toString())
        } else {
            return state.lookup.thermostatFanMode.find{it.value==lookupValue.toString()}?.key
        }
    }
    if (lookupName == "thermostatMode") {
        if (lookupMode) {
            return state.lookup.thermostatMode.getAt(lookupValue.toString())
        } else {
            return state.lookup.thermostatMode.find{it.value==lookupValue.toString()}?.key
        }
    }
    if (lookupName == "thermostatOperatingState") {
        if (lookupMode) {
            return state.lookup.thermostatOperatingState.getAt(lookupValue.toString())
        } else {
            return state.lookup.thermostatOperatingState.find{it.value==lookupValue.toString()}?.key
        }
    }
}

/* Access Management */
private login() {
    log.debug "Infinity: Attempting login"
    if (state.session?.expiration < now()) {
        state.session = [:]
        def apiBody = "data=" + URLEncoder.encode("<credentials><username><![CDATA[" + settings.username + "]]></username><password><![CDATA[" + settings.password + "]]></password></credentials>")
        apiPost("/users/authenticated", apiBody ) { response ->
            if (response.status == 200) {
                if (response.data.text() != "Failed") {
                    state.session = [
                        cookie: response.headers.getAt('Set-Cookie').toString().substring(11),
                        accessToken: response.data.text(),
                        expiration: now() + 600000
                    ]
                    //state.curlheader = getApiAuth("GET","/systems/2913W002173/status","")
                    //log.debug "CURL: " + "-H " + state.curlheader
                    return true
                } else {
                    return false
                }
            } else {
                return false
            }
        }
    } else {
        return true
    }
}

/* API Management */
// HTTP GET call
private apiGet(apiPath, callback = {}) {
    // set up parameters
    def apiParams = [
        uri: getApiURL(),
        path: apiPath,
        contentType: "text/xml",
        headers: getApiHeaders("GET",apiPath,"")
    ]
    try {
        
        httpGet(apiParams) { response -> callback(response) }
    }    catch (Error e)    {
        log.debug "API Error: $e"
    }
}



// HTTP POST call
private apiPost(apiPath, apiBody, callback = {}) {
    // set up parameters
    def apiParams = [
        uri: getApiURL(),
        path: apiPath,
        contentType: "text/xml",
        headers: getApiHeaders("POST",apiPath,apiBody),
        requestContentType: "application/x-www-form-urlencoded",
        body: apiBody
    ]
    try {
        httpPost(apiParams) { response -> callback(response) }
    }    catch (Error e)    {
        log.debug "API Error: $e"
    }
}
private getApiHeaders(apiMethod,apiPath,apiBody) {
    def apiHeaders = [
        "Authorization": getApiAuth(apiMethod,apiPath,apiBody),
        "User-Agent": "Mozilla/5.0 (Windows; U; en-US) AppleWebKit/533.19.4 (KHTML, like Gecko) AdobeAIR/18.0",
    ]
    if (state.session?.expiration > now()) apiHeaders = apiHeaders + ["Set-Cookie": state.session?.cookie]
    return apiHeaders
}
private getApiURL() { return "https://www.app-api.ing.carrier.com" }
private getApiURL(x) { return "https://www.app-api.ing.carrier.com" }
private getApiAuth(oauth_httpmethod,oauth_urlPath, oauth_param) {
    def oauth_url = getApiURL(true) + oauth_urlPath.replace("?", "")
    def oauth_version = "1.0"
    def oauth_consumer_key = "8j30j19aj103911h"
    def oauth_token = settings.username
    def oauth_signature_method = "HMAC-SHA1"
    def oauth_nonce = now().toString()
    def oauth_timestamp = oauth_nonce.substring(0,10)
    def oauth_base = getOAuthBase(oauth_httpmethod, oauth_url, oauth_param, oauth_consumer_key, oauth_nonce, oauth_signature_method, oauth_timestamp, oauth_token, oauth_version)
    //log.debug oauth_base
    def oauth_signature = URLEncoder.encode(getOAuthSignature(oauth_base).toString())
    return "OAuth realm=\"" + oauth_url + "\",oauth_consumer_key=\"" + oauth_consumer_key + "\",oauth_nonce=\"" + oauth_nonce + "\",oauth_signature=\"" + oauth_signature + "\",oauth_signature_method=\"" + oauth_signature_method + "\",oauth_timestamp=\"" + oauth_timestamp + "\",oauth_token=\"" + oauth_token + "\",oauth_version=\"" + oauth_version + "\""
}
private getOAuthBase(oauth_httpmethod, oauth_url, oauth_param, oauth_consumer_key, oauth_nonce, oauth_signature_method, oauth_timestamp, oauth_token, oauth_version) {
    return oauth_httpmethod + "&" + URLEncoder.encode(oauth_url) + "&" + URLEncoder.encode(((!oauth_param?.isEmpty())?(oauth_param + "&"):"") + "oauth_consumer_key=" + oauth_consumer_key + "&oauth_nonce=" + oauth_nonce + "&oauth_signature_method=" + oauth_signature_method + "&oauth_timestamp=" + oauth_timestamp + "&oauth_token=" + oauth_token + "&oauth_version=" + oauth_version)
}
private getOAuthSignature(oauth_base) {
    def oauth_key = "0f5ur7d89sjv8d45&"
    oauth_key = (state.session?.accessToken)? (oauth_key + URLEncoder.encode(state.session?.accessToken)) : oauth_key
    javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA1")
    mac.init(new javax.crypto.spec.SecretKeySpec(oauth_key.getBytes(), "HmacSHA1"))
    return mac.doFinal(oauth_base.getBytes()).encodeAsBase64()
}

The device handler:

/**
 *  Copyright 2015 SmartThings
 *
 *  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.
 *
 *	(Based on) Ecobee Thermostat
 *
 *	Author: SmartThings
 *	Date: 2013-06-13
 */
metadata {
	definition (name: "Carrier Thermostat", namespace: "SmartThingsMod", author: "SmartThingsMod") {
		capability "Actuator"
		capability "Thermostat"
		capability "Temperature Measurement"
		capability "Sensor"
		capability "Refresh"
		capability "Relative Humidity Measurement"
		capability "Health Check"

		command "generateEvent"
		command "resumeProgram"
		command "switchMode"
		command "switchFanMode"
		command "lowerHeatingSetpoint"
		command "raiseHeatingSetpoint"
		command "lowerCoolSetpoint"
		command "raiseCoolSetpoint"
		// To satisfy some SA/rules that incorrectly using poll instead of Refresh
		command "poll"

		attribute "thermostat", "string"
		attribute "maxHeatingSetpoint", "number"
		attribute "minHeatingSetpoint", "number"
		attribute "maxCoolingSetpoint", "number"
		attribute "minCoolingSetpoint", "number"
		attribute "deviceTemperatureUnit", "string"
		attribute "deviceAlive", "enum", ["true", "false"]
		attribute "thermostatSchedule", "string"
	}

	tiles {
		multiAttributeTile(name:"temperature", type:"generic", width:3, height:2, canChangeIcon: true) {
			tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
				attributeState("temperature", label:'${currentValue}°', icon: "st.alarm.temperature.normal",
					backgroundColors:[
							// Celsius
							[value: 0, color: "#153591"],
							[value: 7, color: "#1e9cbb"],
							[value: 15, color: "#90d2a7"],
							[value: 23, color: "#44b621"],
							[value: 28, color: "#f1d801"],
							[value: 35, color: "#d04e00"],
							[value: 37, color: "#bc2323"],
							// Fahrenheit
							[value: 40, color: "#153591"],
							[value: 44, color: "#1e9cbb"],
							[value: 59, color: "#90d2a7"],
							[value: 74, color: "#44b621"],
							[value: 84, color: "#f1d801"],
							[value: 95, color: "#d04e00"],
							[value: 96, color: "#bc2323"]
					]
				)
			}
			tileAttribute("device.humidity", key: "SECONDARY_CONTROL") {
				attributeState "humidity", label:'${currentValue}%', icon:"st.Weather.weather12"
			}
		}
		standardTile("lowerHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
			state "heatingSetpoint", action:"lowerHeatingSetpoint", icon:"st.thermostat.thermostat-left"
		}
		valueTile("heatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
			state "heatingSetpoint", label:'${currentValue}° heat', backgroundColor:"#ffffff"
		}
		standardTile("raiseHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
			state "heatingSetpoint", action:"raiseHeatingSetpoint", icon:"st.thermostat.thermostat-right"
		}
		standardTile("lowerCoolSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
			state "coolingSetpoint", action:"lowerCoolSetpoint", icon:"st.thermostat.thermostat-left"
		}
		valueTile("coolingSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
			state "coolingSetpoint", label:'${currentValue}° cool', backgroundColor:"#ffffff"
		}
		standardTile("raiseCoolSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
			state "heatingSetpoint", action:"raiseCoolSetpoint", icon:"st.thermostat.thermostat-right"
		}
		standardTile("mode", "device.thermostatMode", width:2, height:2, inactiveLabel: false, decoration: "flat") {
			state "off", action:"switchMode", nextState: "updating", icon: "st.thermostat.heating-cooling-off"
			state "heat", action:"switchMode",  nextState: "updating", icon: "st.thermostat.heat"
			state "cool", action:"switchMode",  nextState: "updating", icon: "st.thermostat.cool"
			state "auto", action:"switchMode",  nextState: "updating", icon: "st.thermostat.auto"
			state "emergency heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.emergency-heat"
			state "updating", label:"Updating...", icon: "st.secondary.secondary"
		}
		standardTile("fanMode", "device.thermostatFanMode", width:2, height:2, inactiveLabel: false, decoration: "flat") {
			state "auto", action:"switchFanMode", nextState: "updating", icon: "st.thermostat.fan-auto"
			state "on", action:"switchFanMode", nextState: "updating", icon: "st.thermostat.fan-on"
			state "updating", label:"Updating...", icon: "st.secondary.secondary"
		}
		valueTile("thermostat", "device.thermostat", width:2, height:1, decoration: "flat") {
			state "thermostat", label:'${currentValue}', backgroundColor:"#ffffff"
		}
		standardTile("refresh", "device.thermostatMode", width:2, height:1, inactiveLabel: false, decoration: "flat") {
			state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
		}
		standardTile("resumeProgram", "device.resumeProgram", width:2, height:1, inactiveLabel: false, decoration: "flat") {
			state "resume", action:"resumeProgram", nextState: "updating", label:'Resume', icon:"st.samsung.da.oven_ic_send"
			state "updating", label:"Working", icon: "st.secondary.secondary"
		}
        valueTile("thermostatSchedule", "device.thermostatSchedule", width:2, height:1, decoration: "flat") {
			state "thermostatSchedule", label:'${currentValue}', backgroundColor:"#ffffff"
		}
		main "temperature"
		details(["temperature",  "lowerHeatingSetpoint", "heatingSetpoint", "raiseHeatingSetpoint",
				"lowerCoolSetpoint", "coolingSetpoint", "raiseCoolSetpoint", "mode", "fanMode",
				"thermostat", "thermostatSchedule", "refresh", "resumeProgram"])
	}

	preferences {
		input "holdType", "enum", title: "Hold Type",
				description: "When changing temperature, use Temporary (Until next transition) or Permanent hold (default)",
				required: false, options:["Temporary", "Permanent"]
		input "deadbandSetting", "number", title: "Minimum temperature difference between the desired Heat and Cool " +
				"temperatures in Auto mode:\nNote! This must be the same as configured on the thermostat",
				description: "temperature difference °F", defaultValue: 5,
				required: false
	}

}

void installed() {
    // The device refreshes every 5 minutes by default so if we miss 2 refreshes we can consider it offline
    // Using 12 minutes because in testing, device health team found that there could be "jitter"
    sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "cloud"], displayed: false)
}

// Device Watch will ping the device to proactively determine if the device has gone offline
// If the device was online the last time we refreshed, trigger another refresh as part of the ping.
def ping() {
    def isAlive = device.currentValue("deviceAlive") == "true" ? true : false
    if (isAlive) {
        refresh()
    }
}

// parse events into attributes
def parse(String description) {
	log.debug "Parsing '${description}'"
}

def refresh() {
	log.debug "refresh"
	sendEvent([name: "thermostat", value: "updating"])
	poll2()
}
void poll() {
}
void poll2() {
	log.debug "Executing poll using parent SmartApp"
    //log.debug "Id: " + device.id + ", Name: " + device.name + ", Label: " + device.label + ", NetworkId: " + device.deviceNetworkId
	parent.refreshChild(device.deviceNetworkId)
}

def zUpdate(temp, systemStatus, hum, hsp, csp, fan, currSched){
	log.debug "zupdate: " + temp + ", "  + systemStatus + ", "  + hum + ", "  + hsp + ", "  + csp + ", "  + fan + ", "  + currSched
    sendEvent([name: "temperature", value: temp, unit: "F"])
    sendEvent([name: "thermostat", value: systemStatus])
    sendEvent([name: "humidity", value: hum])
    sendEvent([name: "heatingSetpoint", value: hsp])
    sendEvent([name: "coolingSetpoint", value: csp])
    sendEvent([name: "thermostatFanMode", value: fan])
	sendEvent([name: "thermostatSchedule", value: currSched])
}
def generateEvent(Map results) {
	if(results) {
		def linkText = getLinkText(device)
		def supportedThermostatModes = ["off"]
		def thermostatMode = null
		def locationScale = getTemperatureScale()

		results.each { name, value ->
			def event = [name: name, linkText: linkText, handlerName: name]
			def sendValue = value

			if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint" ) {
				sendValue =  getTempInLocalScale(value, "F")  // API return temperature values in F
				event << [value: sendValue, unit: locationScale]
			} else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") {
				// Old attributes, keeping for backward compatibility
				sendValue =  getTempInLocalScale(value, "F")  // API return temperature values in F
				event << [value: sendValue, unit: locationScale, displayed: false]
				// Store min/max setpoint in device unit to avoid conversion rounding error when updating setpoints
				device.updateDataValue(name+"Fahrenheit", "${value}")
			} else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){
				if (value == true) {
					supportedThermostatModes << ((name == "auxHeatMode") ? "emergency heat" : name - "Mode")
				}
				return // as we don't want to send this event here, proceed to next name/value pair
			} else if (name=="thermostatFanMode"){
				sendEvent(name: "supportedThermostatFanModes", value: fanModes(), displayed: false)
				event << [value: value, data:[supportedThermostatFanModes: fanModes()]]
			} else if (name=="humidity") {
				event << [value: value, displayed: false, unit: "%"]
			} else if (name == "deviceAlive") {
				event['displayed'] = false
			} else if (name == "thermostatMode") {
				thermostatMode = (value == "auxHeatOnly") ? "emergency heat" : value.toLowerCase()
				return // as we don't want to send this event here, proceed to next name/value pair
			} else {
				event << [value: value.toString()]
			}
			event << [descriptionText: getThermostatDescriptionText(name, sendValue, linkText)]
			sendEvent(event)
		}
		if (state.supportedThermostatModes != supportedThermostatModes) {
			state.supportedThermostatModes = supportedThermostatModes
			sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false)
		}
		if (thermostatMode) {
			sendEvent(name: "thermostatMode", value: thermostatMode, data:[supportedThermostatModes:state.supportedThermostatModes], linkText: linkText,
					descriptionText: getThermostatDescriptionText("thermostatMode", thermostatMode, linkText), handlerName: "thermostatMode")
		}
		generateSetpointEvent ()
		generateStatusEvent ()
	}
}

//return descriptionText to be shown on mobile activity feed
private getThermostatDescriptionText(name, value, linkText) {
	if(name == "temperature") {
		return "temperature is ${value}°${location.temperatureScale}"

	} else if(name == "heatingSetpoint") {
		return "heating setpoint is ${value}°${location.temperatureScale}"

	} else if(name == "coolingSetpoint"){
		return "cooling setpoint is ${value}°${location.temperatureScale}"

	} else if (name == "thermostatMode") {
		return "thermostat mode is ${value}"

	} else if (name == "thermostatFanMode") {
		return "thermostat fan mode is ${value}"

	} else if (name == "humidity") {
		return "humidity is ${value} %"
	} else {
		return "${name} = ${value}"
	}
}

void setHeatingSetpoint(setpoint) {
log.debug "***setHeatingSetpoint($setpoint)"
	if (setpoint) {
		state.heatingSetpoint = setpoint.toDouble()
		runIn(2, "updateSetpoints", [overwrite: true])
	}
}

def setCoolingSetpoint(setpoint) {
log.debug "***setCoolingSetpoint($setpoint)"
	if (setpoint) {
		state.coolingSetpoint = setpoint.toDouble()
		runIn(2, "updateSetpoints", [overwrite: true])
	}
}

def updateSetpoints() {
	def deviceScale = "F" //API return/expects temperature values in F
	def data = [targetHeatingSetpoint: null, targetCoolingSetpoint: null]
	def heatingSetpoint = getTempInLocalScale("heatingSetpoint")
	def coolingSetpoint = getTempInLocalScale("coolingSetpoint")
	if (state.heatingSetpoint) {
		data = enforceSetpointLimits("heatingSetpoint", [targetValue: state.heatingSetpoint,
				heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint])
	}
	if (state.coolingSetpoint) {
		heatingSetpoint = data.targetHeatingSetpoint ? getTempInLocalScale(data.targetHeatingSetpoint, deviceScale) : heatingSetpoint
		coolingSetpoint = data.targetCoolingSetpoint ? getTempInLocalScale(data.targetCoolingSetpoint, deviceScale) : coolingSetpoint
		data = enforceSetpointLimits("coolingSetpoint", [targetValue: state.coolingSetpoint,
				heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint])
	}
	state.heatingSetpoint = null
	state.coolingSetpoint = null
	updateSetpoint(data)
}

void resumeProgram() {
	log.debug "resumeProgram() is called"

	sendEvent("name":"thermostat", "value":"resuming schedule", "description":statusText, displayed: false)
	def deviceId = device.deviceNetworkId.split(/\./).last()
	if (parent.resumeProgram(deviceId)) {
		sendEvent("name":"thermostat", "value":"setpoint is updating", "description":statusText, displayed: false)
		sendEvent("name":"resumeProgram", "value":"resume", descriptionText: "resumeProgram is done", displayed: false, isStateChange: true)
	} else {
		sendEvent("name":"thermostat", "value":"failed resume click refresh", "description":statusText, displayed: false)
		log.error "Error resumeProgram() check parent.resumeProgram(deviceId)"
	}
	//xyzrunIn(5, "refresh", [overwrite: true])
}

def modes() {
	return state.supportedThermostatModes
}

def fanModes() {
	// Ecobee does not report its supported fanModes; use hard coded values
	["on", "auto"]
}

def switchSchedule() {
	//TODO
}
def switchMode() {
	def currentMode = device.currentValue("thermostatMode")
	def modeOrder = modes()
	if (modeOrder) {
		def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
		def nextMode = next(currentMode)
		switchToMode(nextMode)
	} else {
		log.warn "supportedThermostatModes not defined"
	}
}

def switchToMode(mode) {
	log.debug "switchToMode: ${mode}"
	def deviceId = device.deviceNetworkId.split(/\./).last()
	// Thermostat's mode for "emergency heat" is "auxHeatOnly"
	if (!(parent.setMode(((mode == "emergency heat") ? "auxHeatOnly" : mode), deviceId))) {
		log.warn "Error setting mode:$mode"
		// Ensure the DTH tile is reset
		generateModeEvent(device.currentValue("thermostatMode"))
	}
	//XYZ runIn(5, "refresh", [overwrite: true])
}

def switchFanMode() {
	def currentFanMode = device.currentValue("thermostatFanMode")
	def fanModeOrder = fanModes()
	def next = { fanModeOrder[fanModeOrder.indexOf(it) + 1] ?: fanModeOrder[0] }
	switchToFanMode(next(currentFanMode))
}

def switchToFanMode(fanMode) {
	log.debug "switchToFanMode: $fanMode"
	def heatingSetpoint = getTempInDeviceScale("heatingSetpoint")
	def coolingSetpoint = getTempInDeviceScale("coolingSetpoint")
	def deviceId = device.deviceNetworkId.split(/\./).last()
	def sendHoldType = holdType ? ((holdType=="Temporary") ? "nextTransition" : "indefinite") : "indefinite"

	if (!(parent.setFanMode(heatingSetpoint, coolingSetpoint, deviceId, sendHoldType, fanMode))) {
		log.warn "Error setting fanMode:fanMode"
		// Ensure the DTH tile is reset
		generateFanModeEvent(device.currentValue("thermostatFanMode"))
	}
	//XYZ runIn(5, "refresh", [overwrite: true])
}

def getDataByName(String name) {
	state[name] ?: device.getDataValue(name)
}

def setThermostatMode(String mode) {
	log.debug "setThermostatMode($mode)"
	def supportedModes = modes()
	if (supportedModes) {
		mode = mode.toLowerCase()
		def modeIdx = supportedModes.indexOf(mode)
		if (modeIdx < 0) {
			log.warn("Thermostat mode $mode not valid for this thermostat")
			return
		}
		mode = supportedModes[modeIdx]
		switchToMode(mode)
	} else {
		log.warn "supportedThermostatModes not defined"
	}
}

def setThermostatFanMode(String mode) {
	log.debug "setThermostatFanMode($mode)"
	mode = mode.toLowerCase()
	def supportedFanModes = fanModes()
	def modeIdx = supportedFanModes.indexOf(mode)
	if (modeIdx < 0) {
		log.warn("Thermostat fan mode $mode not valid for this thermostat")
		return
	}
	mode = supportedFanModes[modeIdx]
	switchToFanMode(mode)
}

def generateModeEvent(mode) {
	sendEvent(name: "thermostatMode", value: mode, data:[supportedThermostatModes: device.currentValue("supportedThermostatModes")],
			isStateChange: true, descriptionText: "$device.displayName is in ${mode} mode")
}

def generateFanModeEvent(fanMode) {
	sendEvent(name: "thermostatFanMode", value: fanMode, data:[supportedThermostatFanModes: device.currentValue("supportedThermostatFanModes")],
			isStateChange: true, descriptionText: "$device.displayName fan is in ${fanMode} mode")
}

def generateOperatingStateEvent(operatingState) {
	sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true)
}

def off() { setThermostatMode("off") }
def heat() { setThermostatMode("heat") }
def emergencyHeat() { setThermostatMode("emergency heat") }
def cool() { setThermostatMode("cool") }
def auto() { setThermostatMode("auto") }

def fanOn() { setThermostatFanMode("on") }
def fanAuto() { setThermostatFanMode("auto") }
def fanCirculate() { setThermostatFanMode("circulate") }

// =============== Setpoints ===============
def generateSetpointEvent() {
	def mode = device.currentValue("thermostatMode")
	def setpoint = getTempInLocalScale("heatingSetpoint")  // (mode == "heat") || (mode == "emergency heat")
	def coolingSetpoint = getTempInLocalScale("coolingSetpoint")

	if (mode == "cool") {
		setpoint = coolingSetpoint
	} else if ((mode == "auto") || (mode == "off")) {
		setpoint = roundC((setpoint + coolingSetpoint) / 2)
	} // else (mode == "heat") || (mode == "emergency heat")
	sendEvent("name":"thermostatSetpoint", "value":setpoint, "unit":location.temperatureScale)
}

def raiseHeatingSetpoint() {
	alterSetpoint(true, "heatingSetpoint")
}

def lowerHeatingSetpoint() {
	alterSetpoint(false, "heatingSetpoint")
}

def raiseCoolSetpoint() {
	alterSetpoint(true, "coolingSetpoint")
}

def lowerCoolSetpoint() {
	alterSetpoint(false, "coolingSetpoint")
}

// Adjusts nextHeatingSetpoint either .5° C/1° F) if raise true/false
def alterSetpoint(raise, setpoint) {
	// don't allow setpoint change if thermostat is off
	if (device.currentValue("thermostatMode") == "off") {
		return
	}
	def locationScale = getTemperatureScale()
	def deviceScale = "F"
	def heatingSetpoint = getTempInLocalScale("heatingSetpoint")
	def coolingSetpoint = getTempInLocalScale("coolingSetpoint")
	def targetValue = (setpoint == "heatingSetpoint") ? heatingSetpoint : coolingSetpoint
	def delta = (locationScale == "F") ? 1 : 0.5
	targetValue += raise ? delta : - delta

	def data = enforceSetpointLimits(setpoint,
			[targetValue: targetValue, heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint], raise)
	// update UI without waiting for the device to respond, this to give user a smoother UI experience
	// also, as runIn's have to overwrite and user can change heating/cooling setpoint separately separate runIn's have to be used
	if (data.targetHeatingSetpoint) {
		sendEvent("name": "heatingSetpoint", "value": getTempInLocalScale(data.targetHeatingSetpoint, deviceScale),
				unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false)
	}
	if (data.targetCoolingSetpoint) {
		sendEvent("name": "coolingSetpoint", "value": getTempInLocalScale(data.targetCoolingSetpoint, deviceScale),
				unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false)
	}
	runIn(5, "updateSetpoint", [data: data, overwrite: true])
}

def enforceSetpointLimits(setpoint, data, raise = null) {
	def locationScale = getTemperatureScale()
	def minSetpoint = (setpoint == "heatingSetpoint") ? device.getDataValue("minHeatingSetpointFahrenheit") : device.getDataValue("minCoolingSetpointFahrenheit")
	def maxSetpoint = (setpoint == "heatingSetpoint") ? device.getDataValue("maxHeatingSetpointFahrenheit") : device.getDataValue("maxCoolingSetpointFahrenheit")
	minSetpoint = minSetpoint ? Double.parseDouble(minSetpoint) : ((setpoint == "heatingSetpoint") ? 45 : 65)  // default 45 heat, 65 cool
	maxSetpoint = maxSetpoint ? Double.parseDouble(maxSetpoint) : ((setpoint == "heatingSetpoint") ? 79 : 92)  // default 79 heat, 92 cool
	def deadband = deadbandSetting ? deadbandSetting : 5 // °F
	def delta = (locationScale == "F") ? 1 : 0.5
	def targetValue = getTempInDeviceScale(data.targetValue, locationScale)
	def heatingSetpoint = getTempInDeviceScale(data.heatingSetpoint, locationScale)
	def coolingSetpoint = getTempInDeviceScale(data.coolingSetpoint, locationScale)
	// Enforce min/mix for setpoints
	if (targetValue > maxSetpoint) {
		targetValue = maxSetpoint
	} else if (targetValue < minSetpoint) {
		targetValue = minSetpoint
	} else if ((raise != null) && ((setpoint == "heatingSetpoint" && targetValue == heatingSetpoint) ||
				(setpoint == "coolingSetpoint" && targetValue == coolingSetpoint))) {
		// Ensure targetValue differes from old. When location scale differs from device,
		// converting between C -> F -> C may otherwise result in no change.
		targetValue += raise ? delta : - delta
	}
	// Enforce deadband between setpoints
	if (setpoint == "heatingSetpoint") {
		heatingSetpoint = targetValue
		coolingSetpoint = (heatingSetpoint + deadband > coolingSetpoint) ? heatingSetpoint + deadband : coolingSetpoint
	}
	if (setpoint == "coolingSetpoint") {
		coolingSetpoint = targetValue
		heatingSetpoint = (coolingSetpoint - deadband < heatingSetpoint) ? coolingSetpoint - deadband : heatingSetpoint
	}
	return [targetHeatingSetpoint: heatingSetpoint, targetCoolingSetpoint: coolingSetpoint]
}

def updateSetpoint(data) {
	def deviceId = device.deviceNetworkId.split(/\./).last()
	def sendHoldType = holdType ? ((holdType=="Temporary") ? "nextTransition" : "indefinite") : "indefinite"

	if (parent.setHold(data.targetHeatingSetpoint, data.targetCoolingSetpoint, deviceId, sendHoldType)) {
		log.debug "alterSetpoint succeed to change setpoints:${data}"
	} else {
		log.error "Error alterSetpoint"
	}
	//XYZ runIn(5, "refresh", [overwrite: true])
}

def generateStatusEvent() {
	def mode = device.currentValue("thermostatMode")
	def heatingSetpoint = device.currentValue("heatingSetpoint")
	def coolingSetpoint = device.currentValue("coolingSetpoint")
	def temperature = device.currentValue("temperature")
	def statusText = "Right Now: Idle"
	def operatingState = "idle"

	if (mode == "heat" || mode == "emergency heat") {
		if (temperature < heatingSetpoint) {
			statusText = "Heating to ${heatingSetpoint}°${location.temperatureScale}"
			operatingState = "heating"
		}
	} else if (mode == "cool") {
		if (temperature > coolingSetpoint) {
			statusText = "Cooling to ${coolingSetpoint}°${location.temperatureScale}"
			operatingState = "cooling"
		}
	} else if (mode == "auto") {
		if (temperature < heatingSetpoint) {
			statusText = "Heating to ${heatingSetpoint}°${location.temperatureScale}"
			operatingState = "heating"
		} else if (temperature > coolingSetpoint) {
			statusText = "Cooling to ${coolingSetpoint}°${location.temperatureScale}"
			operatingState = "cooling"
		}
	} else if (mode == "off") {
		statusText = "Right Now: Off"
	} else {
		statusText = "?"
	}

	sendEvent("name":"thermostat", "value":statusText, "description":statusText, displayed: true)
	sendEvent("name":"thermostatOperatingState", "value":operatingState, "description":operatingState, displayed: false)
}

def generateActivityFeedsEvent(notificationMessage) {
	sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true)
}

// Get stored temperature from currentState in current local scale
def getTempInLocalScale(state) {
	def temp = device.currentState(state)
	def scaledTemp = convertTemperatureIfNeeded(temp.value.toBigDecimal(), temp.unit).toDouble()
	return (getTemperatureScale() == "F" ? scaledTemp.round(0).toInteger() : roundC(scaledTemp))
}

// Get/Convert temperature to current local scale
def getTempInLocalScale(temp, scale) {
	def scaledTemp = convertTemperatureIfNeeded(temp.toBigDecimal(), scale).toDouble()
	return (getTemperatureScale() == "F" ? scaledTemp.round(0).toInteger() : roundC(scaledTemp))
}

// Get stored temperature from currentState in device scale
def getTempInDeviceScale(state) {
	def temp = device.currentState(state)
	if (temp && temp.value && temp.unit) {
		return getTempInDeviceScale(temp.value.toBigDecimal(), temp.unit)
	}
	return 0
}

def getTempInDeviceScale(temp, scale) {
	if (temp && scale) {
		//API return/expects temperature values in F
		return ("F" == scale) ? temp : celsiusToFahrenheit(temp).toDouble().round(0).toInteger()
	}
	return 0
}

def roundC (tempC) {
	return (Math.round(tempC.toDouble() * 2))/2
}

Well this is excellent, thank you @zraken! I was able to install your smartapp and device handled and connect to my system showing status of all zones. Are you planning to finish the configuration capability or looking for help?

To get started I’m mostly interested in turning the system (or zone) off/on (or away/home) based on location events in smartthings. I don’t have much use for adjusting the temp but I’m sure we’ll get there eventually. Second priority would be adjusting the fan state to auto/off/low/high by zone.

Hope this helps. Thanks for your work, we’re close!

turning the system (or zone) off/on (or away/home) based on location events in smartthings

That is my ultimate goal as well.
I did what I could with a couple of hours of play time I got this weekend. My initial quick attempt at the POST API to push an update failed with an “Unauthorized” message, don’t know when I might get more time to work on this. I’m hoping someone else can take it from here.

I’ve added the skill and the handler and everything is working. Only problem I’ve had is the device is not updating unless I go in and manually refresh. Any ideas?

Just as soon as I hit the refresh button it immediately pulls all the information.

Yes, I did not code the part where it updates automatically on install and polls periodically. I was focusing more on getting the change controls to work which I’m afraid I haven’t been successful yet partly because of lack of time and mostly because the config API is failing authorization for some reason.

Understood. Thanks so much for the work you did, at least I don’t have through a separate app now. The only reason I see the automatic updates useful is for ActionTiles, which I use. Something is better than nothing!!

When I found this today it truly made my day. Thanks again for sharing.

csummer2020 here you go…
This version automatically refreshes on install and will poll periodically based on the poll timer setting you specify during setup.

Smart App:

/**
 *  Bryant MyEvolution (Connect)
 *
 *  Copyright 2015 Jason Mok
 *
 *  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.
 *
 */
definition(
    name: "Carrier Infinity Remote",
    namespace: "copy-ninja-mod",
    author: "Jason Mok Modded",
    description: "Connect to Carrier MyInfinity to control your thermostats",
    category: "SmartThings Labs",
    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch.png",
    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png",
)


preferences {
    page(name: "prefLogIn", title: "Carrier MyInfinity")
    page(name: "prefListDevice", title: "Carrier MyInfinity")
}

/* Preferences */
def prefLogIn() {
    def showUninstall = username != null && password != null
    return dynamicPage(name: "prefLogIn", title: "Connect to MyInfinity", nextPage:"prefListDevice", uninstall:showUninstall, install: false) {
        section("Login Credentials"){
            input("username", "text", title: "Username", defaultValue: "", description: "MyInfinity Username (case sensitive)")
            input("password", "password", title: "Password", description: "MyInfinity password (case sensitive)")
        }
        section("Advanced Options"){
            input(name: "polling", title: "Server Polling (in Minutes)", type: "number", description: "in minutes", defaultValue: "15", range: "5..120" )
        }
    }
}

def prefListDevice() {
    if (login()) {
        setLookupInfo()
        def thermostatList = getThermostatList()
        log.debug "Query complete"
        if (thermostatList) {
        	log.debug "Got a list"
            return dynamicPage(name: "prefListDevice",  title: "Thermostats", install:true, uninstall:true) {
                section("Select which thermostat/zones to use"){
                    input(name: "thermostats", type: "enum", required:false, multiple:true, metadata:[values:thermostatList])
                }
            }
        } else {
        	log.debug "Empty list returned"
            return dynamicPage(name: "prefListDevice",  title: "Error!", install:false, uninstall:true) {
                section(""){ paragraph "Could not find any devices "  }
            }
        }
    } else {
        return dynamicPage(name: "prefListDevice",  title: "Error!", install:false, uninstall:true) {
            section(""){ paragraph "The username or password you entered is incorrect. Try again. " }
        }
    }
}

/* Initialization */
def installed() { initialize() }
def updated() {
    unsubscribe()
    initialize()
}
def uninstalled() {
    unschedule()
    unsubscribe()
    getAllChildDevices().each { deleteChildDevice(it.deviceNetworkId) }
}

def initialize() {
    // Set initial states
    /**
    state.polling = [ last: 0, rescheduler: now() ]
    state.data = [:]
    state.setData = [:]
    setLookupInfo()
    login()
    getThermostatList()
    **/
    state.data = [:]
    state.setData = [:]
    setLookupInfo()
    login()
    getThermostatList()
    log.debug "initialize"
	def devices = thermostats.collect { dni ->
    	//log.debug "Processing DNI: " + dni
		def d = getChildDevice(dni)
		if(!d) {
			d = addChildDevice("SmartThingsMod", "Carrier Thermostat", dni, null, ["label" : "Carrier Thermostat Zone " + dni.split("\\|")[2]])
			log.debug "----->created ${d.displayName} with id $dni"
		} else {
			log.debug "found ${d.displayName} with id $dni already exists"
		}
		return d
	}
    log.debug "Completerd creating devices"
	syncThermostats()
    runIn(60*settings.polling, pollTask)
}

def pollTask() {
	syncThermostats()
    runIn(60*settings.polling, pollTask)
}
def refreshChild(dni) {
	log.debug "Refreshing for child " + dni
	//def dni = [ app.id, systemID, zone.'@id'.text().toString() ].join('|')

    def d = getChildDevice(dni)
    if(d) {
    	d.zUpdate(state.data[dni].temperature,
            state.data[dni].thermostatOperatingState,
            state.data[dni].humidity,
            state.data[dni].heatingSetpoint,
            state.data[dni].coolingSetpoint,
            state.data[dni].thermostatFanMode,
            state.data[dni].thermostatActivityState)
		log.debug "Data sent to DH"
	}
    else {
    	log.debug "Somethig went wrong!"
    }
}

private syncThermostats() {
    log.debug "Syncing thermostats"
	if(!state.systemsList) {
    	log.error "System list missing"
        return
    }
    login()
    state.systemsList.each { systemName, systemID ->
        apiGet("/systems/" + systemID + "/status") { response ->
            if (response.status == 200) {
                response.data.zones.zone.each { zone ->
                    if (zone.enabled.text() == "on") {
                    	//DNI DNI: aaaaaaaa-bbbb-cccc-dddd-aaaaaaaaaaaa|zzzzzzzzzzz|1
                        def dni = [ app.id, systemID, zone.'@id'.text().toString() ].join('|')
                        //Get the current status of each device
                            state.data[dni] = [
                                temperature: zone.rt.toInteger(),
                                humidity: zone.rh.toInteger(),
                                coolingSetpoint: zone.clsp.toInteger(),
                                heatingSetpoint: zone.htsp.toInteger(),
                                thermostatFanMode: lookupInfo( "thermostatFanMode", zone.fan.text().toString(), true ),
                                thermostatOperatingState: lookupInfo( "thermostatOperatingState", response.data.mode.text().toString(), true ),
                                thermostatActivityState: zone.currentActivity.text().toString(),
                            ]
                            refreshChild(dni)
                            /***** DEBUG
                            log.debug "Temperature: " + state.data[dni].temperature
                            log.debug "Humidity: " + state.data[dni].humidity
                            log.debug "Heat set point: " + zone.htsp.toInteger()
                            log.debug "Fan: " + lookupInfo( "thermostatFanMode", zone.fan.text().toString(), true )
                            log.debug "Operating state: " + lookupInfo( "thermostatOperatingState", response.data.mode.text().toString(), true )
                            log.debug "Current schedule mode: " + zone.currentActivity.text().toString()
                            log.debug "=====Sync Done====="
                            *****/
                    }
                }
            }
        }
    }
}
// Listing all the thermostats you have in iComfort
private getThermostatList() {
    def thermostatList = [:]
    def systemsList = [:]
    state.data = [:]

    //Get Thermostat Mode lookups
    apiGet("/users/" + settings.username + "/locations") { response ->
        if (response.status == 200) {
            log.debug "APIRESP(users/USER/locations) " + response.data
            response.data.declareNamespace("atom":"http://www.w3.org/2005/Atom")
            response.data.location.each { location ->
                location.systems.system.each {
                    systemsList[it.'atom:link'.@title] = it.'atom:link'.@href.text().tokenize("/").last()
                }
            }
        }
    }
    state.systemsList = systemsList    
    systemsList.each { systemName, systemID ->
        apiGet("/systems/" + systemID + "/status") { response ->
            if (response.status == 200) {
                //log.debug "APIRESP(systems/id/status) " + response.data
                response.data.zones.zone.each { zone ->
                    if (zone.enabled.text() == "on") {
                    	//DNI DNI: AppId|SystemId|1
                        def dni = [ app.id, systemID, zone.'@id'.text().toString() ].join('|')
                        log.debug "APIRESP(systems/id/status) " + systemName.toString() + ": " + zone.name.text().toString() + " DNI: " + dni

                        thermostatList[dni] = systemName.toString() + ": " + zone.name.text().toString()

                        //Get the current status of each device
                            state.data[dni] = [
                                temperature: zone.rt.toInteger(),
                                humidity: zone.rh.toInteger(),
                                coolingSetpoint: zone.clsp.toInteger(),
                                heatingSetpoint: zone.htsp.toInteger(),
                                thermostatFanMode: lookupInfo( "thermostatFanMode", zone.fan.text().toString(), true ),
                                thermostatOperatingState: lookupInfo( "thermostatOperatingState", response.data.mode.text().toString(), true ),
                                thermostatActivityState: zone.currentActivity.text().toString(),
                            ]
                            log.debug "Temperature: " + state.data[dni].temperature
                            log.debug "Humidity: " + state.data[dni].humidity
                            log.debug "Heat set point: " + zone.htsp.toInteger()
                            log.debug "Fan: " + lookupInfo( "thermostatFanMode", zone.fan.text().toString(), true )
                            log.debug "Operating state: " + lookupInfo( "thermostatOperatingState", response.data.mode.text().toString(), true )
                            log.debug "Current schedule mode: " + zone.currentActivity.text().toString()
                            log.debug "=====Done====="
                    }
                }
            }
        }
        /*
        apiGet("/systems/" + systemID) { response ->
            if (response.status == 200) {
                log.debug "APIRESP(systems/id) " + response.data
                response.data.config.zones.zone.each { zone ->
                    if (zone.enabled.text() == "on") {
                        def dni = [ app.id, systemID, zone.'@id'.text().toString() ].join('|')
                        log.debug response.data.config.utilityEvent.maxLimit
						log.debug response.data.config.utilityEvent.minLimit
                        log.debug response.data.config.cfgdead
						log.debug "=====Done====="
                        //state.lookup.activity.putAt(dni, [:])
                        //state.lookup.coolingSetPointHigh.putAt(dni, (response.data.config.utilityEvent.maxLimit.toInteger() - response.data.config.cfgdead.toInteger()))
                        //state.lookup.coolingSetPointLow.putAt(dni, response.data.config.utilityEvent.minLimit.toInteger())
                        //state.lookup.heatingSetPointHigh.putAt(dni, response.data.config.utilityEvent.maxLimit.toInteger())
                        //state.lookup.heatingSetPointLow.putAt(dni, (response.data.config.utilityEvent.minLimit.toInteger() + response.data.config.cfgdead.toInteger()))
                        //state.lookup.differenceSetPoint.putAt(dni, response.data.config.cfgdead.toInteger())
                        //zone.activities.activity.each { activity -> state.lookup.activity[dni].putAt(activity.'@id'.toString(), "Activity :\n" + activity.'@id'.toString()) }
                        //if(state?.data) {
                        //    state.data[dni] = state.data[dni] + [ thermostatMode: lookupInfo( "thermostatMode", response.data.config.mode.text().toString(), true ) ]
                        //}
                    }
                }
            }
        }
        */
    }
    log.debug thermostatList
    return thermostatList
}

def setLookupInfo() {
    state.lookup = [
         thermostatOperatingState: [:],
         thermostatFanMode: [:],
         thermostatMode: [:],
         activity: [:],
         coolingSetPointHigh: [:],
         coolingSetPointLow: [:],
         heatingSetPointHigh: [:],
         heatingSetPointLow: [:],
         differenceSetPoint: [:],
         temperatureRangeF: [:]
     ]
    state.lookup.thermostatMode["off"] = "off"
    state.lookup.thermostatMode["cool"] = "cool"
    state.lookup.thermostatMode["heat"] = "heat"
    state.lookup.thermostatMode["fanonly"] = "off"
    state.lookup.thermostatMode["auto"] = "auto"
    state.lookup.thermostatOperatingState["heat"] = "heating"
    state.lookup.thermostatOperatingState["hpheat"] = "heating"
    state.lookup.thermostatOperatingState["cool"] = "cooling"
    state.lookup.thermostatOperatingState["off"] = "idle"
    state.lookup.thermostatOperatingState["fanonly"] = "fan only"
    state.lookup.thermostatFanMode["off"] = "auto"
    state.lookup.thermostatFanMode["low"] = "circulate"
    state.lookup.thermostatFanMode["med"] = "on"
    state.lookup.thermostatFanMode["high"] = "on"
}

// lookup value translation
def lookupInfo( lookupName, lookupValue, lookupMode ) {
    if (lookupName == "thermostatFanMode") {
        if (lookupMode) {
            return state.lookup.thermostatFanMode.getAt(lookupValue.toString())
        } else {
            return state.lookup.thermostatFanMode.find{it.value==lookupValue.toString()}?.key
        }
    }
    if (lookupName == "thermostatMode") {
        if (lookupMode) {
            return state.lookup.thermostatMode.getAt(lookupValue.toString())
        } else {
            return state.lookup.thermostatMode.find{it.value==lookupValue.toString()}?.key
        }
    }
    if (lookupName == "thermostatOperatingState") {
        if (lookupMode) {
            return state.lookup.thermostatOperatingState.getAt(lookupValue.toString())
        } else {
            return state.lookup.thermostatOperatingState.find{it.value==lookupValue.toString()}?.key
        }
    }
}

/* Access Management */
private login() {
    log.debug "Infinity: Attempting login"
    if (state.session?.expiration < now()) {
        state.session = [:]
        def apiBody = "data=" + URLEncoder.encode("<credentials><username><![CDATA[" + settings.username + "]]></username><password><![CDATA[" + settings.password + "]]></password></credentials>")
        apiPost("/users/authenticated", apiBody ) { response ->
            if (response.status == 200) {
                if (response.data.text() != "Failed") {
                    state.session = [
                        cookie: response.headers.getAt('Set-Cookie').toString().substring(11),
                        accessToken: response.data.text(),
                        expiration: now() + 600000
                    ]
                    //state.curlheader = getApiAuth("GET","/systems/2913W002173/status","")
                    //log.debug "CURL: " + "-H " + state.curlheader
                    return true
                } else {
                    return false
                }
            } else {
                return false
            }
        }
    } else {
        return true
    }
}

/* API Management */
// HTTP GET call
private apiGet(apiPath, callback = {}) {
    // set up parameters
    def apiParams = [
        uri: getApiURL(),
        path: apiPath,
        contentType: "text/xml",
        headers: getApiHeaders("GET",apiPath,"")
    ]
    try {
        
        httpGet(apiParams) { response -> callback(response) }
    }    catch (Exception e)    {
        log.error "API Error: $e"
    }
}



// HTTP POST call
private apiPost(apiPath, apiBody, callback = {}) {
    // set up parameters
    def apiParams = [
        uri: getApiURL(),
        path: apiPath,
        contentType: "text/xml",
        headers: getApiHeaders("POST",apiPath,apiBody),
        requestContentType: "application/x-www-form-urlencoded",
        body: apiBody
    ]
    try {
        httpPost(apiParams) { response -> callback(response) }
    }    catch (Error e)    {
        log.debug "API Error: $e"
    }
}
private getApiHeaders(apiMethod,apiPath,apiBody) {
    def apiHeaders = [
        "Authorization": getApiAuth(apiMethod,apiPath,apiBody),
        "User-Agent": "Mozilla/5.0 (Windows; U; en-US) AppleWebKit/533.19.4 (KHTML, like Gecko) AdobeAIR/18.0",
    ]
    if (state.session?.expiration > now()) apiHeaders = apiHeaders + ["Set-Cookie": state.session?.cookie]
    return apiHeaders
}
private getApiURL() { return "https://www.app-api.ing.carrier.com" }
private getApiURL(x) { return "https://www.app-api.ing.carrier.com" }
private getApiAuth(oauth_httpmethod,oauth_urlPath, oauth_param) {
    def oauth_url = getApiURL(true) + oauth_urlPath.replace("?", "")
    def oauth_version = "1.0"
    def oauth_consumer_key = "8j30j19aj103911h"
    def oauth_token = settings.username
    def oauth_signature_method = "HMAC-SHA1"
    def oauth_nonce = now().toString()
    def oauth_timestamp = oauth_nonce.substring(0,10)
    def oauth_base = getOAuthBase(oauth_httpmethod, oauth_url, oauth_param, oauth_consumer_key, oauth_nonce, oauth_signature_method, oauth_timestamp, oauth_token, oauth_version)
    //log.debug oauth_base
    def oauth_signature = URLEncoder.encode(getOAuthSignature(oauth_base).toString())
    return "OAuth realm=\"" + oauth_url + "\",oauth_consumer_key=\"" + oauth_consumer_key + "\",oauth_nonce=\"" + oauth_nonce + "\",oauth_signature=\"" + oauth_signature + "\",oauth_signature_method=\"" + oauth_signature_method + "\",oauth_timestamp=\"" + oauth_timestamp + "\",oauth_token=\"" + oauth_token + "\",oauth_version=\"" + oauth_version + "\""
}
private getOAuthBase(oauth_httpmethod, oauth_url, oauth_param, oauth_consumer_key, oauth_nonce, oauth_signature_method, oauth_timestamp, oauth_token, oauth_version) {
    return oauth_httpmethod + "&" + URLEncoder.encode(oauth_url) + "&" + URLEncoder.encode(((!oauth_param?.isEmpty())?(oauth_param + "&"):"") + "oauth_consumer_key=" + oauth_consumer_key + "&oauth_nonce=" + oauth_nonce + "&oauth_signature_method=" + oauth_signature_method + "&oauth_timestamp=" + oauth_timestamp + "&oauth_token=" + oauth_token + "&oauth_version=" + oauth_version)
}
private getOAuthSignature(oauth_base) {
    def oauth_key = "0f5ur7d89sjv8d45&"
    oauth_key = (state.session?.accessToken)? (oauth_key + URLEncoder.encode(state.session?.accessToken)) : oauth_key
    javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA1")
    mac.init(new javax.crypto.spec.SecretKeySpec(oauth_key.getBytes(), "HmacSHA1"))
    return mac.doFinal(oauth_base.getBytes()).encodeAsBase64()
}

Device Handler:

/*
 *  Copyright 2015 SmartThings
 *
 *  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.
 *
 *	(Based on) Ecobee Thermostat
 *
 *	Author: SmartThings
 *	Date: 2013-06-13
 */
metadata {
	definition (name: "Carrier Thermostat", namespace: "SmartThingsMod", author: "SmartThingsMod") {
		capability "Actuator"
		capability "Thermostat"
		capability "Temperature Measurement"
		capability "Sensor"
		capability "Refresh"
		capability "Relative Humidity Measurement"
		capability "Health Check"

		command "generateEvent"
		command "resumeProgram"
		command "switchMode"
		command "switchFanMode"
		command "lowerHeatingSetpoint"
		command "raiseHeatingSetpoint"
		command "lowerCoolSetpoint"
		command "raiseCoolSetpoint"
		// To satisfy some SA/rules that incorrectly using poll instead of Refresh
		command "poll"

		attribute "thermostat", "string"
		attribute "maxHeatingSetpoint", "number"
		attribute "minHeatingSetpoint", "number"
		attribute "maxCoolingSetpoint", "number"
		attribute "minCoolingSetpoint", "number"
		attribute "deviceTemperatureUnit", "string"
		attribute "deviceAlive", "enum", ["true", "false"]
		attribute "thermostatSchedule", "string"
	}

	tiles {
		multiAttributeTile(name:"temperature", type:"generic", width:3, height:2, canChangeIcon: true) {
			tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
				attributeState("temperature", label:'${currentValue}°', icon: "st.alarm.temperature.normal",
					backgroundColors:[
							// Celsius
							[value: 0, color: "#153591"],
							[value: 7, color: "#1e9cbb"],
							[value: 15, color: "#90d2a7"],
							[value: 23, color: "#44b621"],
							[value: 28, color: "#f1d801"],
							[value: 35, color: "#d04e00"],
							[value: 37, color: "#bc2323"],
							// Fahrenheit
							[value: 40, color: "#153591"],
							[value: 44, color: "#1e9cbb"],
							[value: 59, color: "#90d2a7"],
							[value: 74, color: "#44b621"],
							[value: 84, color: "#f1d801"],
							[value: 95, color: "#d04e00"],
							[value: 96, color: "#bc2323"]
					]
				)
			}
			tileAttribute("device.humidity", key: "SECONDARY_CONTROL") {
				attributeState "humidity", label:'${currentValue}%', icon:"st.Weather.weather12"
			}
		}
		standardTile("lowerHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
			state "heatingSetpoint", action:"lowerHeatingSetpoint", icon:"st.thermostat.thermostat-left"
		}
		valueTile("heatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
			state "heatingSetpoint", label:'${currentValue}° heat', backgroundColor:"#ffffff"
		}
		standardTile("raiseHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
			state "heatingSetpoint", action:"raiseHeatingSetpoint", icon:"st.thermostat.thermostat-right"
		}
		standardTile("lowerCoolSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
			state "coolingSetpoint", action:"lowerCoolSetpoint", icon:"st.thermostat.thermostat-left"
		}
		valueTile("coolingSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
			state "coolingSetpoint", label:'${currentValue}° cool', backgroundColor:"#ffffff"
		}
		standardTile("raiseCoolSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") {
			state "heatingSetpoint", action:"raiseCoolSetpoint", icon:"st.thermostat.thermostat-right"
		}
		standardTile("mode", "device.thermostatMode", width:2, height:2, inactiveLabel: false, decoration: "flat") {
			state "off", action:"switchMode", nextState: "updating", icon: "st.thermostat.heating-cooling-off"
			state "heat", action:"switchMode",  nextState: "updating", icon: "st.thermostat.heat"
			state "cool", action:"switchMode",  nextState: "updating", icon: "st.thermostat.cool"
			state "auto", action:"switchMode",  nextState: "updating", icon: "st.thermostat.auto"
			state "emergency heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.emergency-heat"
			state "updating", label:"Updating...", icon: "st.secondary.secondary"
		}
		standardTile("fanMode", "device.thermostatFanMode", width:2, height:2, inactiveLabel: false, decoration: "flat") {
			state "auto", action:"switchFanMode", nextState: "updating", icon: "st.thermostat.fan-auto"
			state "on", action:"switchFanMode", nextState: "updating", icon: "st.thermostat.fan-on"
			state "updating", label:"Updating...", icon: "st.secondary.secondary"
		}
		valueTile("thermostat", "device.thermostat", width:2, height:1, decoration: "flat") {
			state "thermostat", label:'${currentValue}', backgroundColor:"#ffffff"
		}
		standardTile("refresh", "device.thermostatMode", width:2, height:1, inactiveLabel: false, decoration: "flat") {
			state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
		}
		standardTile("resumeProgram", "device.resumeProgram", width:2, height:1, inactiveLabel: false, decoration: "flat") {
			state "resume", action:"resumeProgram", nextState: "updating", label:'Resume', icon:"st.samsung.da.oven_ic_send"
			state "updating", label:"Working", icon: "st.secondary.secondary"
		}
        valueTile("thermostatSchedule", "device.thermostatSchedule", width:2, height:1, decoration: "flat") {
			state "thermostatSchedule", label:'${currentValue}', backgroundColor:"#ffffff"
		}
		main "temperature"
		details(["temperature",  "lowerHeatingSetpoint", "heatingSetpoint", "raiseHeatingSetpoint",
				"lowerCoolSetpoint", "coolingSetpoint", "raiseCoolSetpoint", "mode", "fanMode",
				"thermostat", "thermostatSchedule", "refresh", "resumeProgram"])
	}

	preferences {
		input "holdType", "enum", title: "Hold Type",
				description: "When changing temperature, use Temporary (Until next transition) or Permanent hold (default)",
				required: false, options:["Temporary", "Permanent"]
		input "deadbandSetting", "number", title: "Minimum temperature difference between the desired Heat and Cool " +
				"temperatures in Auto mode:\nNote! This must be the same as configured on the thermostat",
				description: "temperature difference °F", defaultValue: 5,
				required: false
	}

}

void installed() {
    // The device refreshes every 5 minutes by default so if we miss 2 refreshes we can consider it offline
    // Using 12 minutes because in testing, device health team found that there could be "jitter"
    sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "cloud"], displayed: false)
}

// Device Watch will ping the device to proactively determine if the device has gone offline
// If the device was online the last time we refreshed, trigger another refresh as part of the ping.
def ping() {
    def isAlive = device.currentValue("deviceAlive") == "true" ? true : false
    if (isAlive) {
        refresh()
    }
}

// parse events into attributes
def parse(String description) {
	log.debug "Parsing '${description}'"
}

def refresh() {
	log.debug "refresh"
	sendEvent([name: "thermostat", value: "updating"])
	poll2()
}
void poll() {
}
void poll2() {
	log.debug "Executing poll using parent SmartApp"
    //log.debug "Id: " + device.id + ", Name: " + device.name + ", Label: " + device.label + ", NetworkId: " + device.deviceNetworkId
	//parent.refreshChild(device.deviceNetworkId)
    parent.syncThermostats()
}

def zUpdate(temp, systemStatus, hum, hsp, csp, fan, currSched){
	log.debug "zupdate: " + temp + ", "  + systemStatus + ", "  + hum + ", "  + hsp + ", "  + csp + ", "  + fan + ", "  + currSched
    sendEvent([name: "temperature", value: temp, unit: "F"])
    sendEvent([name: "thermostat", value: systemStatus])
    sendEvent([name: "humidity", value: hum])
    sendEvent([name: "heatingSetpoint", value: hsp])
    sendEvent([name: "coolingSetpoint", value: csp])
    sendEvent([name: "thermostatFanMode", value: fan])
	sendEvent([name: "thermostatSchedule", value: currSched])
}
def generateEvent(Map results) {
	if(results) {
		def linkText = getLinkText(device)
		def supportedThermostatModes = ["off"]
		def thermostatMode = null
		def locationScale = getTemperatureScale()

		results.each { name, value ->
			def event = [name: name, linkText: linkText, handlerName: name]
			def sendValue = value

			if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint" ) {
				sendValue =  getTempInLocalScale(value, "F")  // API return temperature values in F
				event << [value: sendValue, unit: locationScale]
			} else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") {
				// Old attributes, keeping for backward compatibility
				sendValue =  getTempInLocalScale(value, "F")  // API return temperature values in F
				event << [value: sendValue, unit: locationScale, displayed: false]
				// Store min/max setpoint in device unit to avoid conversion rounding error when updating setpoints
				device.updateDataValue(name+"Fahrenheit", "${value}")
			} else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){
				if (value == true) {
					supportedThermostatModes << ((name == "auxHeatMode") ? "emergency heat" : name - "Mode")
				}
				return // as we don't want to send this event here, proceed to next name/value pair
			} else if (name=="thermostatFanMode"){
				sendEvent(name: "supportedThermostatFanModes", value: fanModes(), displayed: false)
				event << [value: value, data:[supportedThermostatFanModes: fanModes()]]
			} else if (name=="humidity") {
				event << [value: value, displayed: false, unit: "%"]
			} else if (name == "deviceAlive") {
				event['displayed'] = false
			} else if (name == "thermostatMode") {
				thermostatMode = (value == "auxHeatOnly") ? "emergency heat" : value.toLowerCase()
				return // as we don't want to send this event here, proceed to next name/value pair
			} else {
				event << [value: value.toString()]
			}
			event << [descriptionText: getThermostatDescriptionText(name, sendValue, linkText)]
			sendEvent(event)
		}
		if (state.supportedThermostatModes != supportedThermostatModes) {
			state.supportedThermostatModes = supportedThermostatModes
			sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false)
		}
		if (thermostatMode) {
			sendEvent(name: "thermostatMode", value: thermostatMode, data:[supportedThermostatModes:state.supportedThermostatModes], linkText: linkText,
					descriptionText: getThermostatDescriptionText("thermostatMode", thermostatMode, linkText), handlerName: "thermostatMode")
		}
		generateSetpointEvent ()
		generateStatusEvent ()
	}
}

//return descriptionText to be shown on mobile activity feed
private getThermostatDescriptionText(name, value, linkText) {
	if(name == "temperature") {
		return "temperature is ${value}°${location.temperatureScale}"

	} else if(name == "heatingSetpoint") {
		return "heating setpoint is ${value}°${location.temperatureScale}"

	} else if(name == "coolingSetpoint"){
		return "cooling setpoint is ${value}°${location.temperatureScale}"

	} else if (name == "thermostatMode") {
		return "thermostat mode is ${value}"

	} else if (name == "thermostatFanMode") {
		return "thermostat fan mode is ${value}"

	} else if (name == "humidity") {
		return "humidity is ${value} %"
	} else {
		return "${name} = ${value}"
	}
}

void setHeatingSetpoint(setpoint) {
log.debug "***setHeatingSetpoint($setpoint)"
	if (setpoint) {
		state.heatingSetpoint = setpoint.toDouble()
		runIn(2, "updateSetpoints", [overwrite: true])
	}
}

def setCoolingSetpoint(setpoint) {
log.debug "***setCoolingSetpoint($setpoint)"
	if (setpoint) {
		state.coolingSetpoint = setpoint.toDouble()
		runIn(2, "updateSetpoints", [overwrite: true])
	}
}

def updateSetpoints() {
	def deviceScale = "F" //API return/expects temperature values in F
	def data = [targetHeatingSetpoint: null, targetCoolingSetpoint: null]
	def heatingSetpoint = getTempInLocalScale("heatingSetpoint")
	def coolingSetpoint = getTempInLocalScale("coolingSetpoint")
	if (state.heatingSetpoint) {
		data = enforceSetpointLimits("heatingSetpoint", [targetValue: state.heatingSetpoint,
				heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint])
	}
	if (state.coolingSetpoint) {
		heatingSetpoint = data.targetHeatingSetpoint ? getTempInLocalScale(data.targetHeatingSetpoint, deviceScale) : heatingSetpoint
		coolingSetpoint = data.targetCoolingSetpoint ? getTempInLocalScale(data.targetCoolingSetpoint, deviceScale) : coolingSetpoint
		data = enforceSetpointLimits("coolingSetpoint", [targetValue: state.coolingSetpoint,
				heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint])
	}
	state.heatingSetpoint = null
	state.coolingSetpoint = null
	updateSetpoint(data)
}

void resumeProgram() {
	log.debug "resumeProgram() is called"

	sendEvent("name":"thermostat", "value":"resuming schedule", "description":statusText, displayed: false)
	def deviceId = device.deviceNetworkId.split(/\./).last()
	if (parent.resumeProgram(deviceId)) {
		sendEvent("name":"thermostat", "value":"setpoint is updating", "description":statusText, displayed: false)
		sendEvent("name":"resumeProgram", "value":"resume", descriptionText: "resumeProgram is done", displayed: false, isStateChange: true)
	} else {
		sendEvent("name":"thermostat", "value":"failed resume click refresh", "description":statusText, displayed: false)
		log.error "Error resumeProgram() check parent.resumeProgram(deviceId)"
	}
	//xyzrunIn(5, "refresh", [overwrite: true])
}

def modes() {
	return state.supportedThermostatModes
}

def fanModes() {
	// Ecobee does not report its supported fanModes; use hard coded values
	["on", "auto"]
}

def switchSchedule() {
	//TODO
}
def switchMode() {
	def currentMode = device.currentValue("thermostatMode")
	def modeOrder = modes()
	if (modeOrder) {
		def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] }
		def nextMode = next(currentMode)
		switchToMode(nextMode)
	} else {
		log.warn "supportedThermostatModes not defined"
	}
}

def switchToMode(mode) {
	log.debug "switchToMode: ${mode}"
	def deviceId = device.deviceNetworkId.split(/\./).last()
	// Thermostat's mode for "emergency heat" is "auxHeatOnly"
	if (!(parent.setMode(((mode == "emergency heat") ? "auxHeatOnly" : mode), deviceId))) {
		log.warn "Error setting mode:$mode"
		// Ensure the DTH tile is reset
		generateModeEvent(device.currentValue("thermostatMode"))
	}
	//XYZ runIn(5, "refresh", [overwrite: true])
}

def switchFanMode() {
	def currentFanMode = device.currentValue("thermostatFanMode")
	def fanModeOrder = fanModes()
	def next = { fanModeOrder[fanModeOrder.indexOf(it) + 1] ?: fanModeOrder[0] }
	switchToFanMode(next(currentFanMode))
}

def switchToFanMode(fanMode) {
	log.debug "switchToFanMode: $fanMode"
	def heatingSetpoint = getTempInDeviceScale("heatingSetpoint")
	def coolingSetpoint = getTempInDeviceScale("coolingSetpoint")
	def deviceId = device.deviceNetworkId.split(/\./).last()
	def sendHoldType = holdType ? ((holdType=="Temporary") ? "nextTransition" : "indefinite") : "indefinite"

	if (!(parent.setFanMode(heatingSetpoint, coolingSetpoint, deviceId, sendHoldType, fanMode))) {
		log.warn "Error setting fanMode:fanMode"
		// Ensure the DTH tile is reset
		generateFanModeEvent(device.currentValue("thermostatFanMode"))
	}
	//XYZ runIn(5, "refresh", [overwrite: true])
}

def getDataByName(String name) {
	state[name] ?: device.getDataValue(name)
}

def setThermostatMode(String mode) {
	log.debug "setThermostatMode($mode)"
	def supportedModes = modes()
	if (supportedModes) {
		mode = mode.toLowerCase()
		def modeIdx = supportedModes.indexOf(mode)
		if (modeIdx < 0) {
			log.warn("Thermostat mode $mode not valid for this thermostat")
			return
		}
		mode = supportedModes[modeIdx]
		switchToMode(mode)
	} else {
		log.warn "supportedThermostatModes not defined"
	}
}

def setThermostatFanMode(String mode) {
	log.debug "setThermostatFanMode($mode)"
	mode = mode.toLowerCase()
	def supportedFanModes = fanModes()
	def modeIdx = supportedFanModes.indexOf(mode)
	if (modeIdx < 0) {
		log.warn("Thermostat fan mode $mode not valid for this thermostat")
		return
	}
	mode = supportedFanModes[modeIdx]
	switchToFanMode(mode)
}

def generateModeEvent(mode) {
	sendEvent(name: "thermostatMode", value: mode, data:[supportedThermostatModes: device.currentValue("supportedThermostatModes")],
			isStateChange: true, descriptionText: "$device.displayName is in ${mode} mode")
}

def generateFanModeEvent(fanMode) {
	sendEvent(name: "thermostatFanMode", value: fanMode, data:[supportedThermostatFanModes: device.currentValue("supportedThermostatFanModes")],
			isStateChange: true, descriptionText: "$device.displayName fan is in ${fanMode} mode")
}

def generateOperatingStateEvent(operatingState) {
	sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true)
}

def off() { setThermostatMode("off") }
def heat() { setThermostatMode("heat") }
def emergencyHeat() { setThermostatMode("emergency heat") }
def cool() { setThermostatMode("cool") }
def auto() { setThermostatMode("auto") }

def fanOn() { setThermostatFanMode("on") }
def fanAuto() { setThermostatFanMode("auto") }
def fanCirculate() { setThermostatFanMode("circulate") }

// =============== Setpoints ===============
def generateSetpointEvent() {
	def mode = device.currentValue("thermostatMode")
	def setpoint = getTempInLocalScale("heatingSetpoint")  // (mode == "heat") || (mode == "emergency heat")
	def coolingSetpoint = getTempInLocalScale("coolingSetpoint")

	if (mode == "cool") {
		setpoint = coolingSetpoint
	} else if ((mode == "auto") || (mode == "off")) {
		setpoint = roundC((setpoint + coolingSetpoint) / 2)
	} // else (mode == "heat") || (mode == "emergency heat")
	sendEvent("name":"thermostatSetpoint", "value":setpoint, "unit":location.temperatureScale)
}

def raiseHeatingSetpoint() {
	alterSetpoint(true, "heatingSetpoint")
}

def lowerHeatingSetpoint() {
	alterSetpoint(false, "heatingSetpoint")
}

def raiseCoolSetpoint() {
	alterSetpoint(true, "coolingSetpoint")
}

def lowerCoolSetpoint() {
	alterSetpoint(false, "coolingSetpoint")
}

// Adjusts nextHeatingSetpoint either .5° C/1° F) if raise true/false
def alterSetpoint(raise, setpoint) {
	// don't allow setpoint change if thermostat is off
	if (device.currentValue("thermostatMode") == "off") {
		return
	}
	def locationScale = getTemperatureScale()
	def deviceScale = "F"
	def heatingSetpoint = getTempInLocalScale("heatingSetpoint")
	def coolingSetpoint = getTempInLocalScale("coolingSetpoint")
	def targetValue = (setpoint == "heatingSetpoint") ? heatingSetpoint : coolingSetpoint
	def delta = (locationScale == "F") ? 1 : 0.5
	targetValue += raise ? delta : - delta

	def data = enforceSetpointLimits(setpoint,
			[targetValue: targetValue, heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint], raise)
	// update UI without waiting for the device to respond, this to give user a smoother UI experience
	// also, as runIn's have to overwrite and user can change heating/cooling setpoint separately separate runIn's have to be used
	if (data.targetHeatingSetpoint) {
		sendEvent("name": "heatingSetpoint", "value": getTempInLocalScale(data.targetHeatingSetpoint, deviceScale),
				unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false)
	}
	if (data.targetCoolingSetpoint) {
		sendEvent("name": "coolingSetpoint", "value": getTempInLocalScale(data.targetCoolingSetpoint, deviceScale),
				unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false)
	}
	runIn(5, "updateSetpoint", [data: data, overwrite: true])
}

def enforceSetpointLimits(setpoint, data, raise = null) {
	def locationScale = getTemperatureScale()
	def minSetpoint = (setpoint == "heatingSetpoint") ? device.getDataValue("minHeatingSetpointFahrenheit") : device.getDataValue("minCoolingSetpointFahrenheit")
	def maxSetpoint = (setpoint == "heatingSetpoint") ? device.getDataValue("maxHeatingSetpointFahrenheit") : device.getDataValue("maxCoolingSetpointFahrenheit")
	minSetpoint = minSetpoint ? Double.parseDouble(minSetpoint) : ((setpoint == "heatingSetpoint") ? 45 : 65)  // default 45 heat, 65 cool
	maxSetpoint = maxSetpoint ? Double.parseDouble(maxSetpoint) : ((setpoint == "heatingSetpoint") ? 79 : 92)  // default 79 heat, 92 cool
	def deadband = deadbandSetting ? deadbandSetting : 5 // °F
	def delta = (locationScale == "F") ? 1 : 0.5
	def targetValue = getTempInDeviceScale(data.targetValue, locationScale)
	def heatingSetpoint = getTempInDeviceScale(data.heatingSetpoint, locationScale)
	def coolingSetpoint = getTempInDeviceScale(data.coolingSetpoint, locationScale)
	// Enforce min/mix for setpoints
	if (targetValue > maxSetpoint) {
		targetValue = maxSetpoint
	} else if (targetValue < minSetpoint) {
		targetValue = minSetpoint
	} else if ((raise != null) && ((setpoint == "heatingSetpoint" && targetValue == heatingSetpoint) ||
				(setpoint == "coolingSetpoint" && targetValue == coolingSetpoint))) {
		// Ensure targetValue differes from old. When location scale differs from device,
		// converting between C -> F -> C may otherwise result in no change.
		targetValue += raise ? delta : - delta
	}
	// Enforce deadband between setpoints
	if (setpoint == "heatingSetpoint") {
		heatingSetpoint = targetValue
		coolingSetpoint = (heatingSetpoint + deadband > coolingSetpoint) ? heatingSetpoint + deadband : coolingSetpoint
	}
	if (setpoint == "coolingSetpoint") {
		coolingSetpoint = targetValue
		heatingSetpoint = (coolingSetpoint - deadband < heatingSetpoint) ? coolingSetpoint - deadband : heatingSetpoint
	}
	return [targetHeatingSetpoint: heatingSetpoint, targetCoolingSetpoint: coolingSetpoint]
}

def updateSetpoint(data) {
	def deviceId = device.deviceNetworkId.split(/\./).last()
	def sendHoldType = holdType ? ((holdType=="Temporary") ? "nextTransition" : "indefinite") : "indefinite"

	if (parent.setHold(data.targetHeatingSetpoint, data.targetCoolingSetpoint, deviceId, sendHoldType)) {
		log.debug "alterSetpoint succeed to change setpoints:${data}"
	} else {
		log.error "Error alterSetpoint"
	}
	//XYZ runIn(5, "refresh", [overwrite: true])
}

def generateStatusEvent() {
	def mode = device.currentValue("thermostatMode")
	def heatingSetpoint = device.currentValue("heatingSetpoint")
	def coolingSetpoint = device.currentValue("coolingSetpoint")
	def temperature = device.currentValue("temperature")
	def statusText = "Right Now: Idle"
	def operatingState = "idle"

	if (mode == "heat" || mode == "emergency heat") {
		if (temperature < heatingSetpoint) {
			statusText = "Heating to ${heatingSetpoint}°${location.temperatureScale}"
			operatingState = "heating"
		}
	} else if (mode == "cool") {
		if (temperature > coolingSetpoint) {
			statusText = "Cooling to ${coolingSetpoint}°${location.temperatureScale}"
			operatingState = "cooling"
		}
	} else if (mode == "auto") {
		if (temperature < heatingSetpoint) {
			statusText = "Heating to ${heatingSetpoint}°${location.temperatureScale}"
			operatingState = "heating"
		} else if (temperature > coolingSetpoint) {
			statusText = "Cooling to ${coolingSetpoint}°${location.temperatureScale}"
			operatingState = "cooling"
		}
	} else if (mode == "off") {
		statusText = "Right Now: Off"
	} else {
		statusText = "?"
	}

	sendEvent("name":"thermostat", "value":statusText, "description":statusText, displayed: true)
	sendEvent("name":"thermostatOperatingState", "value":operatingState, "description":operatingState, displayed: false)
}

def generateActivityFeedsEvent(notificationMessage) {
	sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true)
}

// Get stored temperature from currentState in current local scale
def getTempInLocalScale(state) {
	def temp = device.currentState(state)
	def scaledTemp = convertTemperatureIfNeeded(temp.value.toBigDecimal(), temp.unit).toDouble()
	return (getTemperatureScale() == "F" ? scaledTemp.round(0).toInteger() : roundC(scaledTemp))
}

// Get/Convert temperature to current local scale
def getTempInLocalScale(temp, scale) {
	def scaledTemp = convertTemperatureIfNeeded(temp.toBigDecimal(), scale).toDouble()
	return (getTemperatureScale() == "F" ? scaledTemp.round(0).toInteger() : roundC(scaledTemp))
}

// Get stored temperature from currentState in device scale
def getTempInDeviceScale(state) {
	def temp = device.currentState(state)
	if (temp && temp.value && temp.unit) {
		return getTempInDeviceScale(temp.value.toBigDecimal(), temp.unit)
	}
	return 0
}

def getTempInDeviceScale(temp, scale) {
	if (temp && scale) {
		//API return/expects temperature values in F
		return ("F" == scale) ? temp : celsiusToFahrenheit(temp).toDouble().round(0).toInteger()
	}
	return 0
}

def roundC (tempC) {
	return (Math.round(tempC.toDouble() * 2))/2
}

Thank you for the update(s) @zraken, the refresh seems to be working well for me now. Looking forward to getting some of the configuration working. Thanks!

Likewise @zraken. Thanks for the updates. I’ve done the install and the app is still not updating unless I manually refresh. I’ve got mine updating every 5 minutes but its not going out to pull the points, any ideas?

@csummer, I double checked and polling is indeed working for me. I set the interval to 5 minutes and it missed the first one but after that it has consistently refreshed every 5 minutes. If it is still not working please post the log for the App and Device handler.

Nice work, any chance of getting the Bryant/Carrier toggle on this :slight_smile: I know im asking a lot hehe
I have the Bryant Evolution control…

Im using the Bryant control also, seems to work out of the box.

Looks like it is working for swerb73. If not, you can probably make this change in the smartapp to get it to work:

In the smartapp there are the “getApiURL() and getApiURL(x)” functions that return “https://www.app-api.ing.carrier.com”. All you need to do is replace that URL with “https://www.app-api.eng.bryant.com”.

I have no idea if that will work since I don’t have an account to test with.