Skip to content

Instantly share code, notes, and snippets.

@ghostbitmeta
Last active January 24, 2024 18:23
Show Gist options
  • Star 27 You must be signed in to star a gist
  • Fork 23 You must be signed in to fork a gist
  • Save ghostbitmeta/694934062c0814680d52 to your computer and use it in GitHub Desktop.
Save ghostbitmeta/694934062c0814680d52 to your computer and use it in GitHub Desktop.
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()
@kylepaddock
Copy link

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
Copy link

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
Copy link

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.

Copy link

ghost 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
Copy link

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

@Breakerz
Copy link

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
Copy link

amandamana 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
Copy link

kevdel 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)

@wburnett01
Copy link

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)

Kevdel would you mind would you mind uploading a fork of your code with your logout def ??

@kevdel
Copy link

kevdel commented Jun 11, 2019

@wburnett01
@wburnett01

i put an updated script on pastebin with my changes

https://pastebin.com/raw/ntP4xM2v

@ricch-dhsr
Copy link

Wow... I've been looking for a way to do this for a long time now. Just spent the last few days with all kinds of other approaches. Your script worked perfectly first time no issues.. can't thank you enough.... Rich....

@thevickypedia
Copy link

May I know what type of thermostats are supported? Model names or reference links would be much helpful. Thank you.

@famewolf
Copy link

famewolf commented Jun 5, 2023

I updated the script to python3 and fixed an index error because the argument count check was under the check for status and cancel instead of being first. https://gist.github.com/famewolf/3b9e9a602378204d3c7270fed729c69f

@famewolf
Copy link

famewolf commented Jun 5, 2023

May I know what type of thermostats are supported? Model names or reference links would be much helpful. Thank you.

ANY honeywell thermostat that uses the total connect app for control. You can probably find the manual for a particular model online somewhere like http://www.manualsonline.com/ and see what app it says to use. [Yes I realize this question is over a year old but others probably have same question]

@hhss801
Copy link

hhss801 commented Sep 24, 2023

The code works but you can not switch from cool to heat or heat to cool. E.g. if you live in an area when the overnight temp. difference is large so you need cool in day time and heat at night, the code won't work.
You need add payload["SystemSwitch"] = 3 for cool and payload["SystemSwitch"] = 1 for heat as below:

if (action == "cool"):
payload["CoolSetpoint"] = value
payload["StatusCool"] = 1
payload["StatusHeat"] = 1
payload["CoolNextPeriod"] = stop_time
payload["SystemSwitch"] = 3

if (action == "heat"):
  payload["HeatSetpoint"] = value
  payload["StatusCool"] = 1
  payload["StatusHeat"] = 1
  payload["HeatNextPeriod"] = stop_time
  payload["SystemSwitch"] = 1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment