-
-
Save ghostbitmeta/694934062c0814680d52 to your computer and use it in GitHub Desktop.
#!/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 "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 "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 "Example: Set temperature to cool to 80f for 1 hour: \n\t therm.py -c 80 -t 1" | |
print "If no -t hold_time is provided, it will default to one hour from command time." | |
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() |
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
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.
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()
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.
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!!!
is there a way to ignore the thermostat temperature reading? or maybe set it to a fixed value?
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...
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")
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)
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 ??
i put an updated script on pastebin with my changes
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....
May I know what type of thermostats are supported? Model names or reference links would be much helpful. Thank you.
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
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]
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
A few improvements I made to make this work for me:
Moved arg check to beginning line 283-286 to 275
comment out line 26: # import tty,termios which doesn't exist in Windows
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()