Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Python script to control Honeywell Thermostat's through My Total Connect
#!/usr/bin/python
# By Brad Goodman
# http://www.bradgoodman.com/
# brad@bradgoodman.com
####################### Fill in settings below #######################
USERNAME="your@emailaddress.com"
PASSWORD="your_total_comfort_password"
DEVICE_ID=012345
############################ End settings ############################
import urllib2
import urllib
import json
import datetime
import re
import time
import math
import base64
import time
import httplib
import sys
import getopt
import os
import stat
import subprocess
import string
AUTH="https://mytotalconnectcomfort.com/portal"
cookiere=re.compile('\s*([^=]+)\s*=\s*([^;]*)\s*')
def client_cookies(cookiestr,container):
if not container: container={}
toks=re.split(';|,',cookiestr)
for t in toks:
k=None
v=None
m=cookiere.search(t)
if m:
k=m.group(1)
v=m.group(2)
if (k in ['path','Path','HttpOnly']):
k=None
v=None
if k:
#print k,v
container[k]=v
return container
def export_cookiejar(jar):
s=""
for x in jar:
s+='%s=%s;' % (x,jar[x])
return s
def get_login(action, value=None, hold_time=1):
cookiejar=None
#print
#print
#print "Run at ",datetime.datetime.now()
headers={"Content-Type":"application/x-www-form-urlencoded",
"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding":"sdch",
"Host":"mytotalconnectcomfort.com",
"DNT":"1",
"Origin":"https://mytotalconnectcomfort.com/portal",
"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36"
}
conn = httplib.HTTPSConnection("mytotalconnectcomfort.com")
conn.request("GET", "/portal/",None,headers)
r0 = conn.getresponse()
#print r0.status, r0.reason
for x in r0.getheaders():
(n,v) = x
#print "R0 HEADER",n,v
if (n.lower() == "set-cookie"):
cookiejar=client_cookies(v,cookiejar)
#cookiejar = r0.getheader("Set-Cookie")
location = r0.getheader("Location")
retries=5
params=urllib.urlencode({"timeOffset":"240",
"UserName":USERNAME,
"Password":PASSWORD,
"RememberMe":"false"})
#print params
newcookie=export_cookiejar(cookiejar)
#print "Cookiejar now",newcookie
headers={"Content-Type":"application/x-www-form-urlencoded",
"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding":"sdch",
"Host":"mytotalconnectcomfort.com",
"DNT":"1",
"Origin":"https://mytotalconnectcomfort.com/portal/",
"Cookie":newcookie,
"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36"
}
conn = httplib.HTTPSConnection("mytotalconnectcomfort.com")
conn.request("POST", "/portal/",params,headers)
r1 = conn.getresponse()
#print r1.status, r1.reason
for x in r1.getheaders():
(n,v) = x
#print "GOT2 HEADER",n,v
if (n.lower() == "set-cookie"):
cookiejar=client_cookies(v,cookiejar)
cookie=export_cookiejar(cookiejar)
#print "Cookiejar now",cookie
location = r1.getheader("Location")
if ((location == None) or (r1.status != 302)):
#raise BaseException("Login fail" )
print("ErrorNever got redirect on initial login status={0} {1}".format(r1.status,r1.reason))
return
# Skip second query - just go directly to our device_id, rather than letting it
# redirect us to it.
code=str(DEVICE_ID)
t = datetime.datetime.now()
utc_seconds = (time.mktime(t.timetuple()))
utc_seconds = int(utc_seconds*1000)
#print "Code ",code
location="/portal/Device/CheckDataSession/"+code+"?_="+str(utc_seconds)
#print "THIRD"
headers={
"Accept":"*/*",
"DNT":"1",
#"Accept-Encoding":"gzip,deflate,sdch",
"Accept-Encoding":"plain",
"Cache-Control":"max-age=0",
"Accept-Language":"en-US,en,q=0.8",
"Connection":"keep-alive",
"Host":"mytotalconnectcomfort.com",
"Referer":"https://mytotalconnectcomfort.com/portal/",
"X-Requested-With":"XMLHttpRequest",
"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36",
"Cookie":cookie
}
conn = httplib.HTTPSConnection("mytotalconnectcomfort.com")
#conn.set_debuglevel(999);
#print "LOCATION R3 is",location
conn.request("GET", location,None,headers)
r3 = conn.getresponse()
if (r3.status != 200):
print("Error Didn't get 200 status on R3 status={0} {1}".format(r3.status,r3.reason))
return
# Print thermostat information returned
if (action == "status"):
#print r3.status, r3.reason
rawdata=r3.read()
j = json.loads(rawdata)
#print "R3 Dump"
#print json.dumps(j,indent=2)
#print json.dumps(j,sort_keys=True,indent=4, separators=(',', ': '))
#print "Success:",j['success']
#print "Live",j['deviceLive']
print "Indoor Temperature:",j['latestData']['uiData']["DispTemperature"]
print "Indoor Humidity:",j['latestData']['uiData']["IndoorHumidity"]
print "Cool Setpoint:",j['latestData']['uiData']["CoolSetpoint"]
print "Heat Setpoint:",j['latestData']['uiData']["HeatSetpoint"]
print "Hold Until :",j['latestData']['uiData']["TemporaryHoldUntilTime"]
print "Status Cool:",j['latestData']['uiData']["StatusCool"]
print "Status Heat:",j['latestData']['uiData']["StatusHeat"]
print "Status Fan:",j['latestData']['fanData']["fanMode"]
return
headers={
"Accept":'application/json; q=0.01',
"DNT":"1",
"Accept-Encoding":"gzip,deflate,sdch",
'Content-Type':'application/json; charset=UTF-8',
"Cache-Control":"max-age=0",
"Accept-Language":"en-US,en,q=0.8",
"Connection":"keep-alive",
"Host":"mytotalconnectcomfort.com",
"Referer":"https://mytotalconnectcomfort.com/portal/",
"X-Requested-With":"XMLHttpRequest",
"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36",
'Referer':"/TotalConnectComfort/Device/CheckDataSession/"+code,
"Cookie":cookie
}
# Data structure with data we will send back
payload = {
"CoolNextPeriod": None,
"CoolSetpoint": None,
"DeviceID": DEVICE_ID,
"FanMode": None,
"HeatNextPeriod": None,
"HeatSetpoint": None,
"StatusCool": 0,
"StatusHeat": 0,
"SystemSwitch": None
}
# Calculate the hold time for cooling/heating
t = datetime.datetime.now();
stop_time = ((t.hour+hold_time)%24) * 60 + t.minute
stop_time = stop_time/15
# Modify payload based on user input
if (action == "cool"):
payload["CoolSetpoint"] = value
payload["StatusCool"] = 1
payload["StatusHeat"] = 1
payload["CoolNextPeriod"] = stop_time
if (action == "heat"):
payload["HeatSetpoint"] = value
payload["StatusCool"] = 1
payload["StatusHeat"] = 1
payload["HeatNextPeriod"] = stop_time
if (action == "cancel"):
payload["StatusCool"] = 0
payload["StatusHeat"] = 0
if (action == "fan"):
payload["FanMode"] = value
# Prep and send payload
location="/portal/Device/SubmitControlScreenChanges"
rawj=json.dumps(payload)
conn = httplib.HTTPSConnection("mytotalconnectcomfort.com");
#conn.set_debuglevel(999);
#print "R4 will send"
#print rawj
conn.request("POST", location,rawj,headers)
r4 = conn.getresponse()
if (r4.status != 200):
print("Error Didn't get 200 status on R4 status={0} {1}".format(r4.status,r4.reason))
return
else:
print "Success in configuring thermostat!"
# print "R4 got 200"
def printUsage():
print
print "Cooling: -c temperature -t hold_time"
print "Heating: -h temperature -t hold_time"
print "Status: -s"
print "Cancel: -x"
print "Fan: -f [0=auto|1=on]"
print
print "Example: Set temperature to cool to 80f for 1 hour: \n\t therm.py -c 80 -t 1"
print
print "If no -t hold_time is provided, it will default to one hour from command time."
print
def main():
if sys.argv[1] == "-s":
get_login("status")
sys.exit()
if sys.argv[1] == "-x":
get_login("cancel")
sys.exit()
if (len(sys.argv) < 3) or (sys.argv[1] == "-help"):
printUsage()
sys.exit()
if sys.argv[1] == "-c":
get_login("cool", sys.argv[2])
sys.exit()
if sys.argv[1] == "-h":
get_login("heat", sys.argv[2])
sys.exit()
if sys.argv[1] == "-f":
get_login("fan", sys.argv[2])
sys.exit()
if __name__ == "__main__":
main()
@Stavr0

This comment has been minimized.

Copy link

commented Dec 6, 2015

A few improvements I made to make this work for me:

  1. Moved arg check to beginning line 283-286 to 275

  2. comment out line 26: # import tty,termios which doesn't exist in Windows

  3. Add Fan control:
    line 183:
    print "Status Fan:",j['latestData']['fanData']["fanMode"]

line 239:
if (action == "fan"):
payload["FanMode"] = value

line 270:
print "Fan: -f [0=auto|1=on]"

line 307:
if sys.argv[1] == "-f":
get_login("fan", sys.argv[2])
sys.exit()

@geoffharpergit

This comment has been minimized.

Copy link

commented Mar 27, 2016

This is pretty fantastic! Thanks!

The status (-s) works.

However, this is what happens for me:

c:>python thermostat.py -h 70
Error Didn't get 200 status on R4 status=500 Internal Server Error

Any clues?

Thanks!

-Geoff

@kylepaddock

This comment has been minimized.

Copy link

commented Apr 12, 2016

I ran this and I get this error:
Traceback (most recent call last):
File "C:/Users/paddockk9448/Desktop/tstat.py", line 304, in
main()
File "C:/Users/paddockk9448/Desktop/tstat.py", line 279, in main
if sys.argv[1] == "-s":
IndexError: list index out of range

Geoffharpergit did you do anything to make yours work.

@geoffharpergit

This comment has been minimized.

Copy link

commented Apr 24, 2016

The script is not expecting to be run without parameters.

To fix that, after "def main():" and before these lines:

if sys.argv[1] == "-s":
    get_login("status")
    sys.exit()

add these lines:

if (len(sys.argv) < 2):
    printUsage()
    sys.exit()
@geoffharpergit

This comment has been minimized.

Copy link

commented Apr 24, 2016

I still cannot get the script to actually work, though. I can set a new temperature and I don't get errors back, but nothing is actually changed.

@ghost

This comment has been minimized.

Copy link

commented Aug 24, 2016

Pardon me, I'm new to GitHub. I don't know if previous posters here will be notified of my response or not.

I'm needing some assistance, coming up with a way to "steer" this script based on data pulled from a webserver by way of xml. My python knowledge is non-existent.

In brief, I want to run my air conditioners more when my solar array is over-producing our consumption, so that the AC won't run as much when clouds are overhead (we pay more to the utility for power than they pay for our excess).

If this is something anyone here would like to help with, please drop me a line!

I'm also curious, is the script working for anyone? It sounds like I'm not going to get it to work for me, with my limited knowledge, if others more in-the-know can't get it going.

Thanks!!!

@zs19930830

This comment has been minimized.

Copy link

commented Jul 24, 2017

is there a way to ignore the thermostat temperature reading? or maybe set it to a fixed value?

@Breakerz

This comment has been minimized.

Copy link

commented Dec 29, 2017

https://gist.github.com/Breakerz/a54821b32e829f2696fb24df8926d987
i added "-j " parameter, to output json, and "-m" to change the mode (cool,heat,off,emergy heat)...
I also did a mqtt/python wrapper, to use me termostat in Home Assistant...

@amandamana

This comment has been minimized.

Copy link

commented Oct 6, 2018

Looks like -t hold_time flag is always used as the default of 1. main() doesn't check for that flag and the argument is not passed from the command line to the get_login function (see lines 296-300).

It would be good to import argparse and clean up the argument parsing. Passing a string for the hold_time breaks the math at line 221. I suggest having argparse use a float for -t and changing line 222 to stop_time = int(round(stop_time / 15)) so that decimal hours can be input (and the sum with the current time is rounded to the nearest 15 minute multiple used by the server).

I found this link describing how to keep the -h for heat instead of the default help when using argparse.

And instead of having raw minutes displayed for the TemporaryHoldUntilTime, I suggest converting to HH:MM AM/PM by replacing line 178 with:
holdUntil = datetime.datetime.strptime(str(datetime.timedelta(minutes=j['latestData']['uiData']["TemporaryHoldUntilTime"]))[:-3], "%H:%M")
print "Hold Until:",holdUntil.strftime("%I:%M %p")

@kevdel

This comment has been minimized.

Copy link

commented Dec 8, 2018

if you have issues with unauthorized login attempts, I found that using the following code to logout any successful sessions as per the website has helped close off sessions correctly to enable the next login to succeed..

Because of the way the action == "status" block returns early, i just created a new def and called it before the return there and again at the very end of get_login(...) def. I passed along the headers generated early in the get_login() to correctly reference the session

    def logout(headers):
     # https://mytotalconnectcomfort.com/portal/Account/LogOff   
     conn = httplib.HTTPSConnection("mytotalconnectcomfort.com");
     conn.request("GET", "https://mytotalconnectcomfort.com/portal/Account/LogOff",None,headers)
     r5 = conn.getresponse()
     print "logged out: " +  str(r5.status) + str(r5.reason)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.