watchforstock / evohome-client

Python client to access the Evohome web service
Apache License 2.0
88 stars 52 forks source link

Client authentication triggering server rate limiting #57

Closed DBMandrake closed 5 years ago

DBMandrake commented 5 years ago

In recent months Honeywell have made substantial changes to their API servers, one of these is that new, aggressive rate limiting has been applied for client connection authentication attempts.

This means that even polling the servers once every 5 minutes (performing a full username/password authentication) for the purposes of graphing zone temperatures is now intermittently triggering rate limiting which locks out the client for as much as 10 minutes or more.

Investigation by a few people on the Automated home forum including 'gordonb3' have discovered that it's the OAuth authentication attempts that are being rate limited and rejected, not the actual use of the API calls once the client has a client access token, which lasts (at least for now on the V2 API) for 30 minutes from the time of last use.

So for their own graphing systems (which don't use this python client library) they have adapted to the new rate limit restrictions by locally caching the client access token and keeping track of it's expiry time and only performing a full username/password re-authentication when absolutely needed.

From what I can see while this library caches the client access token within a given script instance it does not have any means to cache and reuse the token among multiple consecutive script instances, (eg on disk) instead performing a full username/password authentication when each new script instance calls the EvohomeClient() method.

I use the evohome-client library with evohome-munin which is a Munin plugin. The architecture of munin plugins is that each graph for each zone is generated by calling a separate instance of the script sequentially one at a time.

Evohome-munin already implements disk caching of the returned zone data so that only the first instance of the script actually calls EvohomeClient() and the following instances read the zone data from disk cache and therefore only one authentication attempt is made per 5 minutes, but even this is triggering the rate limiting at the servers.

To fully solve this problem evohome-client would need to cache the client access token for a given username/password pair to disk along with a token expiry time, and then when another instance of a script calls EvohomeClient() check to see if a recently used still valid access token already exists and use that directly, bypassing the username/password OAuth steps and only attempting OAuth authentication if the token is expired or is no longer working.

Is there any possibility that cross instance caching of the client access token can be added to this library ? Without it, it's semi-unusable now for any non-persistent client script, thanks to the rate limiting Honeywell have applied, not just for 5 minute polling for graphing, but for any application that may want to make arbitrarily timed connections in response to user queries or actions. I started to look at this myself but quickly realised my python skills are not up to the task of attempting this sort of rewrite of the library.

Some discussion about the details of this problem can be found on the following pages of the Automated Heating Forum thread:

https://www.automatedhome.co.uk/vbulletin/showthread.php?5723-Evohome-app-broken/page2

Posts by gordonb3 are particularly helpful. Both V1 and V2 API authentication are affected by rate limiting and it appears that authentication attempts to both API's may be counted together towards total rate limiting and that once you are rate limited, authentication attempts on both API's fail, so sharing the cached client access token between V1 and V2 API's may be necessary to fully support clients that use both API's. (As the evohome-munin script does)

DBMandrake commented 5 years ago

Ah my mistake. Never mind. Forgot to initialise refresh_token. Told you I was crap. :)

Testing... waiting for my rate limiting to expire first. Sigh.

zxdavb commented 5 years ago

... it will be all worth it in the end!

DBMandrake commented 5 years ago

Ok, still having problems. Here is my updated script to include refresh_token:

#!/usr/bin/python

import time
import json
import io
import datetime

from evohomeclient2 import EvohomeClient

try:
        f = io.open('V2_access_token', "rb")

        list = json.load(f)
        access_token = list[0]
        refresh_token = list[1]
        access_token_expires = datetime.datetime.strptime(list[2], "%Y-%m-%d %H:%M:%S.%f")

        f.close()

        print 'DEBUG: SAVED access_token: ' + str(access_token)
        print 'DEBUG: SAVED refresh_token: ' + str(refresh_token)
        print 'DEBUG: SAVED access_token_expires: ' + str(access_token_expires)
except:
        access_token = None
        refresh_token = None
        access_token_expires = None

client = EvohomeClient('********@******.com', '**********', refresh_token=refresh_token, access_token=access_token, access_token_expires=access_token_expires, debug=True)

print 'DEBUG: RECEIVED access_token: ' + str(client.access_token)
print 'DEBUG: RECEIVED refresh_token: ' + str(client.refresh_token)
print 'DEBUG: RECEIVED access_token_expires: ' + str(client.access_token_expires)

for device in client.temperatures():
    print device

f = io.open('V2_access_token', "wb")

list = [ client.access_token, client.refresh_token, str(client.access_token_expires) ]

json.dump(list, f)
f.close()

Now the issue seems to be that when I read the refresh token at the end of the script to save it, it is a null string. :(

Consequently on the second pass it is still using username and password authentication and subject to throttling.

reply: 'HTTP/1.1 200 OK\r\n'
header: Cache-Control: no-cache
header: Pragma: no-cache
header: Content-Type: application/json; charset=utf-8
header: Expires: -1
header: Server: Microsoft-IIS/8.5
header: Server: Web1
header: Date: Tue, 26 Feb 2019 16:17:34 GMT
header: Content-Length: 11808
header: Set-Cookie: NSC_UDDOB-XfcBqj-TTM-WT=ffffffff090ecc0045525d5f4f58455e445a4a42378b;expires=Tue, 26-Feb-2019 16:19:34 GMT;path=/;secure;httponly
DEBUG:requests.packages.urllib3.connectionpool:"GET /WebAPI/emea/api/v1/location/installationInfo?userId=1108128&includeTemperatureControlSystems=True HTTP/1.1" 200 11808
DEBUG: RECEIVED access_token: I80e37cEFiNvmh6N9qGmPqwLi4GYA6YH0bSGMHmbCeYLWFHYZf0pcgtjrfKLjir7KrvOpZ8a3wLRb17W7VjuafiQLpPq5TWdtAtxkOko7oaPKUYVr6B2BedLix6Z6jYXX3hoFAMYXko5tPOxp6Pk1ebxTMqmBnqt2EYUda7_Ju6iaSSlnjT6qWXxQNdZ7rJ6rV_9ddnPsl18MxHRotqOVsYqiSLITxmR1JYiGFhxwW7RF9EaTuxw6hkOjekk8ItXQuBZxvsQ0SNRe-e7B4vb8Zd_v0EScH6uiz1Jo2oB6mbX65--ZNpB3JSySBnRxZQBfilqCdzbay-8HQa7AV1TrIUe_j-ZRW_ua5efFlsMB3uj5qfyUtawLwMzZVh-rvyZZNI0-_ZcqiLBpZUMpVcdgm-ejOX2cgjOPCRfBV6r6Pc83PTwTpzHrtSRVXLEZ4JIW3N30IEBAFYawlNqq7G719xnkK8nZG1j6JZDdFh6mLgC-Uw7CZr8M04IzkZME3vkpURy-AZZ5QwM1rgl2_KWafpj1fqfYfLcmmbToCp7-vPOVnv5
DEBUG: RECEIVED refresh_token: None
DEBUG: RECEIVED access_token_expires: 2019-02-26 16:47:32.065027

Hmm... do you definitely get a refresh token if you try to save one ?

zxdavb commented 5 years ago

OK, I found the bug.

Apologies:I really only have done regression testing at my end.

I will push out a fix later on today when I am sitting in front of the computer again.

zxdavb commented 5 years ago

Actually, looking at the code again: I'm not sure now if the bug I found is the cause of your problem.

DBMandrake commented 5 years ago

Fix it anyway ? :-D

zxdavb commented 5 years ago

Done: #72 - if you can't wait fro @watchforstock, try: https://github.com/zxdavb/evohome-client/tree/exception_handling

>>> username = "<<snip>>"
>>> password = "<<snip>>"

>>> from evohomeclient2 import EvohomeClient
>>> x = EvohomeClient(username, password)

>>> print("refresh_token =", x.refresh_token)
refresh_token = dAHEoNKaE7dLr0jZ6WZUwY2vjOREz-RvAfgE-hacZFMjQQ0AOxkuRsWIS6x-xKFKgvJO_Ti53I8G1BskyDuWelsDkzPJRziVh5tfiMjzB6c2fRiETShCIOsMO-mb0ja4YX9cPLnB4jm1h0wz4Xu00gr5K3il068iQBKgep9msCGjffNXcnvdDDrePHTfLgdOll6GdH2tWBRahp-RN3FWwhxYqaubT5oXQIeRP5ThG9Hi5p59sbntllftlvRmDtYE6ChiRPD9vG7q8-dAHD3Gwdt4mt1mwiTOq7i3SmUFaVc9D3u-324IUL4BbmytymlwMbq5sXDPRWXDzF1mMnkCZB3WKrQy9k47NU5G5sUukJ6okyRNrQv7A7tRZHQAFl0uKd9VTmGrRn5qiKQ6h0BWvdYAAEjrjKJbJKLqB-JIHbeCF88NQaQOpCL57-zFnFJQSEPjX2FIKkDxp58TPHwasyF7Sd0rnI3-1VUk61Erpx3GIF-7tbGW7S8_MWb2b3UB79uX56bmoliTMasq_5CTxHJYJH1sGpCNLBOMJ1y1waul-rR_

>>> print("access_token =", x.access_token)
access_token = hxZdHudxMbTMEbxAyu_0aXE12y0RPiRkNOZ5W2ALFRHVaKbPT4kiz7qD4dlQSLo7BcSgEuElwf_dnFlIrG7aI3ZTRfsvxcTGAqXTUHDb-ujElPvBfpJi0HlDzfUA2vXmicQU_YR49O39L9XrMo8hPHohpthP9N4jCJhYhj8QbtBvsvtAuzFtolTkTWWzPU5KGTAqEuCbyngCRr46xjl4JqpRZNjP7QHtk5qzKariXJoqLgltozKjBPdeTXT8E8SUTH8P5PLWur5rkWo-t6kGs1QM_gzk4APRv1Intf-y883hiQx6N0CVQnBaDzXkNM4qoZrJLXmtzHyE5wWpQMNKIxgE0bIE4AVkTuZbzGdY4DnLBZRGyylOlh7dJ1war-uULB6fC-WSRVVo37VohOVlLEC_sZsdXVthvFiOGrmjH1R4qzphnCZYY0Q_lsajksMUPCEc3yFOLCkhrtOphQcAScwkAulgZEVc7k8Jf6kXrop-yfNJkgvnj80KxGEZ9aEAA0o8t4ZHvVzcu2GmhLxx1644LjueAqQdX-2iHAFV7FOLLnQw

>>> print("access_token_expires =", x.access_token_expires)
access_token_expires = 2019-02-26 18:07:33.322210

and then...

>>> refresh_token=x.refresh_token
>>> x = None
>>> c = EvohomeClient("bad_username", "bad_password", refresh_token=refresh_token)
>>> c = None
>>> c = EvohomeClient("bad_username", "bad_password", refresh_token=refresh_token)
>>> c = None
>>> c = EvohomeClient("bad_username", "bad_password", refresh_token=refresh_token)
>>> c = None
>>> c = EvohomeClient("bad_username", "bad_password", refresh_token=refresh_token)
>>> c = None
>>> c = EvohomeClient("bad_username", "bad_password", refresh_token=refresh_token)
>>> c = None
>>> c = EvohomeClient("bad_username", "bad_password", refresh_token=refresh_token)
>>> c = None

>>> c = EvohomeClient("bad_username", "bad_password", refresh_token="bad_refresh_token")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/srv/hass/lib/python3.6/site-packages/evohomeclient2/__init__.py", line 44, in __init__
    self._login()
  File "/srv/hass/lib/python3.6/site-packages/evohomeclient2/__init__.py", line 47, in _login
    self.user_account()
  File "/srv/hass/lib/python3.6/site-packages/evohomeclient2/__init__.py", line 166, in user_account
    response = requests.get(url, headers=self._headers())
  File "/srv/hass/lib/python3.6/site-packages/evohomeclient2/__init__.py", line 54, in _headers
    self._basic_login()
  File "/srv/hass/lib/python3.6/site-packages/evohomeclient2/__init__.py", line 86, in _basic_login
    raise ValueError("Bad Username/Password, unable to continue.")
ValueError: Bad Username/Password, unable to continue.
DBMandrake commented 5 years ago

That seemed to work - I now get a refresh token and subsequent script runs are skipping the OAuth stage as expected. :)

I had all kinds of trouble and inconsistent results trying to run my script in the git repo to test the checked out version so ended up having to fully uninstall the system copy of the library and re-install your version to be sure I was running the correct version.

Will do some further testing and then move on to testing the V1 API session restore, as that's the one I'm mainly interested in. (At least until Honeywell start providing high resolution temperatures on the V2 API! Hint, hint)

DBMandrake commented 5 years ago

I've been testing the token expiry time on the V2 client and noticed behaviour that I'm not sure is normal or not so thought I would ask.

To check that the library properly handles an expired token I tried manually editing the time in my json file between runs to artificially make the library think the token had almost run out or had run out. Sure enough, if the token expiry time had passed it performed OAuth authentication and got a new token/refresh token and a new token expiry time and everything was fine.

However if I made the expiry time say 1-2 minutes away it would use the token instead of OAuth as you would expect, however the "new" token expiry time that I saved at the end of the run would be the same expiry from before that was due to expire in a couple of minutes, and it would still be the same access token and refresh token as well.

Subsequently the next script run past the token expiry time would result in a full OAuth authentication. Is this the way it is expected to operate ? That a token lasts for 30 minutes and then after that an OAuth re-authentication is mandatory ?

I was under the impression that as long as you re-used the token within the expiry time that it would be refreshed or extended in some way for a new 30 minute period so that you could continue to use it indefinitely without going through OAuth again as long as you used it within the expiry time each iteration.

However that is not what is happening. Both tokens and token expiry time output by the library after a session do not change once a token is established, until the time is up and a new OAuth session is performed.

Ideally in a situation where we are say polling every 5 minutes it would be nice to avoid OAuth altogether apart from the initial round...

I still don't really understand the relationship between the access token and the refresh token and what the purpose of the refresh token is if you still just have to do a full OAuth re-authentication every 30 minutes.

zxdavb commented 5 years ago

Simon,

I am going to have to request you specifically say access token (or AT) or refresh token (or RT) rather than token. And clarify when you say OAuth authentication, do you mean a) authentication via username/password, or both that, and via an RT?

I still don't really understand the relationship between the access token and the refresh token and what the purpose of the refresh token is if you still just have to do a full OAuth re-authentication every 30 minutes.

My understanding is that Username/Password authentication is not required as long as a valid RT is maintained. And that the API limit is on username/password authentication and not using RTs to get new ATs.

One tip for testing: use a valid RT, but an invalid username/password pair & see what happens.

My recall is that this provided a new AT every 30 mins, as expected. As the RT was valid, and was refreshed with a new one every time a new AT was requested via the old one, it never needed to use the username/password.

BTW, I do not know what happens if your account has two sets of RTs on teh go at once (e.g. I run two instances of my smarthome platform, one production, one testbed).

And could you clarify the following statement?

however the "new" token expiry time that I saved at the end of the run would be the same expiry from before that was due to expire in a couple of minutes, and it would still be the same access token and refresh token as well.

To avoid confusion, maybe you could just edit your previous post a little?

DBMandrake commented 5 years ago

Ok I see where I was getting confused before. After thinking about your reply and studying the debug headers I finally understand the difference between the access token and the refresh token.

The access token is a token given by OAuth after authentication which can then be used to call on the API itself for up to 30 minutes without re-authenticating. After that 30 minutes a new access token must be obtained through OAuth.

When it comes time to re-authenticate there are two possibilities - use the refresh token or use username and password.

Sure enough, if I remove the username and password, set the access_token_expiry to the past and call, it performs an OAuth session using the refresh_token and is successful. In the process it gets a new access_token and updates the access_token_expiry.

However if I corrupt the refresh_token on purpose, fudge the expiry time and try again it fails and complains that there is no valid username / password. If I put the username and password back in, corrupt the refresh_token and fudge the expiry time it gracefully falls back to using the username and password.

So now that I think I understand how it should work my conclusion is that it is working as it should! :)

It does pose a couple of questions though.

1) If after the 30 minute period you always have to go back to OAuth anyway, what is the benefit of using a saved refresh_token over just using username/password ? It just seems like unnecessary extra complication.

In both cases the OAuth session is vulnerable to throttling for example if you have another parallel system running - as you do with your production and test system. (I also used to have both Domoticz and Evohome-munin running in parallel but had to stop due to the throttling issues)

2) Does the refresh_token ever expire ? I see that the code falls back gracefully if it attempts to use the refresh_token and it isn't valid anymore, but it seems odd that authenticating just once would let you re-authenticate using this refresh_token forever after even if you changed the password on your account...?

Anyway it seems to be working so I'll move on and have a look at the V1 API. Apart from that window of opportunity once every 30 minutes where you are still (presumably) vulnerable to throttling if you were running other completely separate instances, I can repeat my requests as often as I like without experiencing any failures or throttling, so it is definitely just the OAuth phase that is heavily throttled.

zxdavb commented 5 years ago

When it comes time to re-authenticate there are two possibilities - use the refresh token or use username and password.

It should say: a valid refresh token or the username & password.

However if I corrupt the refresh_token on purpose, fudge the expiry time and try again it fails and complains that there is no valid username / password.

Correct. It tried using the refresh token and that didn't work, so it then gracefully falls beck to the username and password.

It is unclear if the first OAuth attempt would count against your API limit. I suspect not, as I think the limit is per username (and not, for example, per IP address - I could be wrong though).

In both cases the OAuth session is vulnerable to throttling...

Actually, I'm not sure that this is the case. My testing gave me the impression that Oauth via refresh tokens is not throttled, where OAuth via user credentials is.

Question 1: this is a matter of architecture (other authentication/authorization systems do it the same way, e.g. Kerberos). In this case it doesn't seem like there's much in it, but imagine a scenario where the credentials have to be validated by (say) Google, and the access tokens managed by (in this case) Honeywell.

In any case, using refresh tokens are a lot 'cheaper' than revalidating user credentials.

Question 2: someone mentioned that they do, but I do not know either way. Imagine they do expire, so the question would be how long do they last.

The answer to the second half of this question is the same as the answer to question 1: specifically it's a balance between getting up-to-date information, pounding resources, and getting a timely response to authentication / authorisation requests...

DBMandrake commented 5 years ago

It should say: a valid refresh token or the username & password.

Presumably the only way to know if a previously saved refresh_token is valid is to try to use it and fall back to username/password if it doesn't work. One would hope that it has some sort of expiry of it's own, but we presumably don't know what it is.

It is unclear if the first OAuth attempt would count against your API limit. I suspect not, as I think the limit is per username (and not, for example, per IP address - I could be wrong though)

Ah.. so you think a failed use of the refresh_token (a very stale refresh_token for example) that results in having to fall back to the username/password could potentially be counted as two authentication attempts ? That's not ideal.

Is it possible to just not use the refresh_token if I choose - for example if I call EvohomeClient() without specifying a refresh_token, will it just fall immediately back to username/password authentication when the access_token expires ?

Perhaps that's a choice that some scripts may wish to make, especially if it simplifies the code and amount of data to be saved as well as minimising failed authentication attempts. I'm not completely sold on the benefits of using a refresh_token.

DBMandrake commented 5 years ago

I can confirm that just not passing a refresh_token works - caching of the access_token works as normal but it then falls back directly to username and password when the access_token expires. So for anyone wanting to do it this way it looks like they could.

zxdavb commented 5 years ago

I'll be grateful if you, or someone else, was able to do some testing to see where the two streams of refresh tokens would clash with each other

DBMandrake commented 5 years ago

Trying to test that just now, but OAuth throttling has completely stomped on me - I have two separate instances of my script with their own separate json disk file storage for their own session state. One is authenticated and using the access_token without issues, the other one can't get past the authentication stage for the last 5 minutes. Grr. I wonder if Honeywell is fed up with all my probing of their rate limiting today...

So you want me to see whether one session can re-authenticate with the refresh_token only (username/password removed and expiry time fudged) soon after another session has authenticated ?

I can see that both sessions get different refresh_token's I guess the question is whether the first one is invalidated when the second one is obtained.

zxdavb commented 5 years ago

@DBMandrake, seeing as I explained that so well, you wouldn't mind cutting and pasting some text from the previous few posts into this wiki page?

https://github.com/watchforstock/evohome-client/wiki/Access_tokens-(v2-client)

DBMandrake commented 5 years ago

Bad news. It looks like there can only be one valid refresh_token for a given account at the same time.

I authenticated instance 1, waited a few minutes, authenticated instance 2. Both had different refresh_token, access_token and access_token_expiry.

I waited a few more minutes then modified instance 1 to force it to try to re-authenticate with the refresh_token (fudge the date and remove the username and password) and it returned:

INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): tccna.honeywell.com
send: 'POST /Auth/OAuth/Token HTTP/1.1\r\nHost: tccna.honeywell.com\r\nContent-Length: 272\r\nAccept-Encoding: gzip, deflate\r\nAccept: application/json, application/xml, text/json, text/x-json, text/javascript, text/xml\r\nUser-Agent: python-requests/2.9.1\r\nConnection: keep-alive\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic NGEyMzEwODktZDJiNi00MWJkLWE1ZWItMTZhMGE0MjJiOTk5OjFhMTVjZGI4LTQyZGUtNDA3Yi1hZGQwLTA1OWY5MmM1MzBjYg==\r\n\r\nUsername=&Password=&Connection=Keep-Alive&Host=rs.alarmnet.com%2F&Pragma=no-cache&Cache-Control=no-store+no-cache&scope=EMEA-V1-Basic+EMEA-V1-Anonymous+EMEA-V1-Get-Current-User-Account&grant_type=password&Content-Type=application%2Fx-www-form-urlencoded%3B+charset%3Dutf-8'
reply: 'HTTP/1.1 400 Bad Request\r\n'
header: Cache-Control: no-cache
header: Pragma: no-cache
header: Content-Length: 25
header: Content-Type: application/json;charset=UTF-8
header: Expires: -1
header: Server: Microsoft-IIS/8.5
header: Set-Cookie: thlang=en-US; expires=Wed, 27-Feb-2069 13:08:15 GMT; path=/
header: Server: Web1
header: Date: Wed, 27 Feb 2019 13:08:15 GMT
header: Set-Cookie: NSC_UDDOB-TTM-WT=ffffffff090ecc1a45525d5f4f58455e445a4a42378b;expires=Wed, 27-Feb-2019 13:10:15 GMT;path=/;secure;httponly
DEBUG:requests.packages.urllib3.connectionpool:"POST /Auth/OAuth/Token HTTP/1.1" 400 25
Traceback (most recent call last):
  File "./evohome_V2_token", line 31, in <module>
    client = EvohomeClient('', '', refresh_token=refresh_token, access_token=access_token, access_token_expires=access_token_expires, debug=True)
  File "build/bdist.linux-armv6l/egg/evohomeclient2/__init__.py", line 44, in __init__
  File "build/bdist.linux-armv6l/egg/evohomeclient2/__init__.py", line 47, in _login
  File "build/bdist.linux-armv6l/egg/evohomeclient2/__init__.py", line 166, in user_account
  File "build/bdist.linux-armv6l/egg/evohomeclient2/__init__.py", line 54, in _headers
  File "build/bdist.linux-armv6l/egg/evohomeclient2/__init__.py", line 86, in _basic_login
ValueError: Bad Username/Password, unable to continue.

So it looks like if there is any chance of separate instances running on the same account that will be authenticating separately there is no point in trying to use a refresh_token as the refresh_token use will fail and it will fall back to username and password.

There can be multiple access_token's that are valid and in use across multiple instances at the same time however.

zxdavb commented 5 years ago

This may work: create a second evohome account, and send an invitation from the first account.

Then you have two accounts for the one installation!

DBMandrake commented 5 years ago

Yes that would probably work but I'm trying to test to see what the limitations of a single account are.

I think if there are independent instances running on the same system with a single account then there is always going to be a risk of the refresh_token being invalidated (but that should just cause a graceful fallback to username/password, or the alternative is just don't use a refresh_token) or two instances trying to refresh their access_token around the same time triggering OAuth rate limiting.

The only way to avoid that completely would be if the cross instance token caching was being done centrally in the library itself as I originally suggested, but as we've already discussed, that isn't going to happen.

Having a look at the V1 API now - I'm guessing I should just save the current date and time myself and check whether the 'user_data' is less than 15 minutes old (as found by @gordonb3 ) before trying to re-use it, if it is too old and/or it fails, re-call Evohomeclient() with a null user_data ?

zxdavb commented 5 years ago

It will definitely help those who are constantly restarting they're smart home system.

I'm afraid I don't know much about how the version 1 API authentication works, but let's see how we go.

DBMandrake commented 5 years ago

Throttling in the V1 API seems to be based on this URL:

requests.exceptions.HTTPError: 429 Client Error: Too Many Requests for url: https://tccna.honeywell.com/WebAPI/api/Session

As before the throttling is seriously hindering my efforts at testing. I need to be patient and let the throttling expire. :)

One thing I notice already is that 'user_data' that I am saving contains a lot of information not necessarily relevant to resuming a session. As well as a session id {u'sessionId': u'AED7F51F-336F-419B-965E-2F18696D38A2' which I presume is what we need, it also contains the username, userID, Street address and location, country, language and a few other things all wrapped in a dictionary. I wonder if it's necessary to save all this other stuff ?

DBMandrake commented 5 years ago

Ok I think there may be a bug in the evohomeclient library for session restoring using the V1 API. Here is my quickly hacked together test script:

#!/usr/bin/python

import time
import json
import io
import datetime

from evohomeclient import EvohomeClient

try:
        f = io.open('V1_user_data', "rb")

        list = json.load(f)
        user_data = list[0]
        last_access_date = datetime.datetime.strptime(list[1], "%Y-%m-%d %H:%M:%S.%f")

        f.close()

        print type(user_data)
        print 'DEBUG: SAVED user_data: ' + str(user_data)
        print 'DEBUG: SAVED last_access_date: ' + str(last_access_date)
except:
        user_data = None

client = EvohomeClient('*******@*****.com', '*********', user_data=user_data, debug=True)

for device in client.temperatures():
    print device

currentdatetime = datetime.datetime.now()

print 'DEBUG: ' + str(currentdatetime)
print 'DEBUG: RECEIVED user_data: ' + str(client.user_data)

f = io.open('V1_user_data', "wb")

list = [ client.user_data, str(currentdatetime) ]

json.dump(list, f)
f.close()

First time through it captures and saves the user_data dict and also saves the current time. I've verified the data in the json file looks correct. Second pass I get the following error: (personal data removed)

Traceback (most recent call last):
  File "./evohome_V1_token", line 29, in <module>
    for device in client.temperatures():
  File "build/bdist.linux-armv6l/egg/evohomeclient/__init__.py", line 135, in temperatures
  File "build/bdist.linux-armv6l/egg/evohomeclient/__init__.py", line 78, in _populate_full_data
TypeError: 'NoneType' object does not support item assignment

I've verified that user_data that I'm passing to EvohomeClient() is indeed a dictionary after being converted back from json, so I don't think the error is in my script.

Any ideas on this one ? Note: at the moment I save the current time to the json file but I'm not doing any date/time checks on re-use yet, but this won't be the issue when I'm testing it just a few seconds after it was saved.

zxdavb commented 5 years ago

Not from this smartphone, no.

I'm going to give @WatchForStock first opportunity on this one...

gordonb3 commented 5 years ago

One thing I notice already is that 'user_data' that I am saving contains a lot of information not necessarily relevant to resuming a session. As well as a session id {u'sessionId': u'AED7F51F-336F-419B-965E-2F18696D38A2' which I presume is what we need, it also contains the username, userID, Street address and location, country, language and a few other things all wrapped in a dictionary. I wonder if it's necessary to save all this other stuff ?

You require sessionId and userID - the latter being part of the uri for getting/setting information (also in the v2 API).

So it looks like if there is any chance of separate instances running on the same account that will be authenticating separately there is no point in trying to use a refresh_token as the refresh_token use will fail and it will fall back to username and password.

Was just working on my C++ implementation, adding save and load methods for auth tokens, and figured I could give that a try. Yet another "feature" that appears to behave differently for me.

David, did you ever check out the old app key?

DBMandrake commented 5 years ago

So it looks like if there is any chance of separate instances running on the same account that will be authenticating separately there is no point in trying to use a refresh_token as the refresh_token use will fail and it will fall back to username and password.

Was just working on my C++ implementation, adding save and load methods for auth tokens, and figured I could give that a try. Yet another "feature" that appears to behave differently for me.

So you're saying you can run two overlapping completely independent instances that have authenticated separately and thus have different access_token's and refresh_token's, and you're then still able to use the refresh_token of the first one to re-authenticate ?

Maybe I wasn't testing it properly. It's quite hard to test due to constantly being locked out by the rate limiting whenever the OAuth calls are involved... :/

I tried it a few times and always got HTTP/1.1 400 Bad Request when I tried to use the refresh_token of the first instance after the second instance had authenticated and got a refresh_token of it's own but I might try testing it again.

Of course if the instances shared the same refresh_token it wouldn't be a problem, but that's not the way it's being implemented. As I mentioned earlier I'm not sure whether the refresh_token is of particular benefit over just falling back to username/password when the access_token expires, especially if it doesn't do anything to help with authentication rate limiting.

gordonb3 commented 5 years ago

Correct. I altered the main demo application for my library to use the new save and load methods. After running it once I ran another demo application for setting the system mode which performed a second login and I verified that the command was accepted. I then forced the first demo app to use the refresh token and the on screen logging told me it successfully did so.

As mentioned before, I also have a different expiration time value. My guess is that Honeywell is deliberately making the portal behave differently depending on the provided application key (which of course has to be a key they registered in their system).

DBMandrake commented 5 years ago

I doubt that the refresh_token behaviour across multiple instances would be different with the different API key. Much more likely that I just made a mistake in my testing! :)

gordonb3 commented 5 years ago

If they can set an alternate expiration time on the access token, surely they can also set a different limit on the number of allowed access tokens and/or refresh tokens for a specific combination of app key and login.

I'll set up a test case , but I should note that between testing my new code I've also not seen any authentication errors from invalidated refresh keys in my running service based environment.

watchforstock commented 5 years ago

Ok I think there may be a bug in the evohomeclient library for session restoring using the V1 API. Here is my quickly hacked together test script:

Traceback (most recent call last):
  File "./evohome_V1_token", line 29, in <module>
    for device in client.temperatures():
  File "build/bdist.linux-armv6l/egg/evohomeclient/__init__.py", line 135, in temperatures
  File "build/bdist.linux-armv6l/egg/evohomeclient/__init__.py", line 78, in _populate_full_data
TypeError: 'NoneType' object does not support item assignment

I've verified that user_data that I'm passing to EvohomeClient() is indeed a dictionary after being converted back from json, so I don't think the error is in my script.

Any ideas on this one ? Note: at the moment I save the current time to the json file but I'm not doing any date/time checks on re-use yet, but this won't be the issue when I'm testing it just a few seconds after it was saved.

Yes - this is a bug in how some of the elements were initialised. I've just pushed a change which hopefully should fix this issue.

watchforstock commented 5 years ago

I have to admit to struggling to keep up on all of the great discussion today!

It's an interesting use case where multiple tools are simultaneously using the library to pull data. I still think that it needs to be the caller that manages the tokens - for the library to do it would add a lot of complications in terms of checking whether it's the same account or a different account being polled and as we've seen above, there might be multiple installations of the library on a system which further complicates things. That said, getting multiple different callers to share tokens equally sounds complicated.

Thanks both for the testing you've been doing - I couldn't do it on my own.

zxdavb commented 5 years ago

it needs to be the caller that manages the tokens

Completely agree.

DBMandrake commented 5 years ago

Great, I'll test the latest fix tomorrow.

It's an interesting use case where multiple tools are simultaneously using the library to pull data. I still think that it needs to be the caller that manages the tokens - for the library to do it would add a lot of complications in terms of checking whether it's the same account or a different account being polled

That's not difficult - just hash the username and password together, store the hash along with the returned access_token, refresh_token and access_token_expiry. (or user_data for V1)

That way you're not storing the username or password on disk (security risk) but the next time a client script makes a request using the same username and password pair (even a different instance running in a different application) you can hash it and find the hash matches the one previously saved and know that the tokens that go with that hash should be used.

And if you are able to store multiple username password hashes and sets of tokens it would be able to handle caching of more than one evohome account and select the correct tokens based on finding a match of the username/password hash.

So that part seems doable, the issue is really a case of where would you store that data persistently if it was the job of the library to do so, and any security implications of having the session data stored in the file system somewhere not under the direct control of the client script - even if you're only storing a hash of the username/password, the access_token, refresh_token or user_data could theoretically be abused by a malicious script to hijack a session without knowing the password if it can just look at a known location in the file system and read the tokens of a session that doesn't belong to it and then use them to resume a session started by some other caller without knowing the username and password.

Although by the same token - pun intended, my method of saving the tokens in a json file in a tmp directory is equally vulnerable to being targeted by another script running under the same user account if someone were to be bothered to attack my script.... so I'm not sure what the solution is there other than "don't run malicious scripts on your system under the same user account".

Another problem with the library maintaining a central hash/token database as a file is that the library runs in the calling scripts context which is not necessarily going to be the same user account on the system for all callers. For example when I used to run Domoticz I think it used the library under the regular 'pi' user while munin is running it as root. (I probably should change that...)

So the sharing of tokens would only work between client scripts that run under the same unix user account. Although if you are in control of all the scripts running on the system that may be good enough, for example storing a cache in $HOME/.evohomeclient/ for the user running the script and just make sure all your evohome client scripts are running as the same user. (Which is likely the case anyway)

Yet another complexity of doing it centrally in the library is that it would need to implement some sort of locking for the cache and authentication method so that simultaneous calls from different client scripts didn't result in either corrupting the cache by simultaneously modifying it, or attempting to perform simultaneous (within a few seconds of each other) authentication requests - which will almost certainly trigger rate limiting.

So I'm a bit torn between the approach we're currently following which puts a lot of onus on the client script (and defeats a lot of the ease of use of the library) and doesn't fully support multiple instances without a risk of occasional OAuth failures if timing is unlucky or trying to put all the session caching in the library and make it transparent but with more complexity in the library and having to store tokens in the filesystem that can be easily stolen and abused due to being in a known location.

DBMandrake commented 5 years ago

I've done some testing this morning, here's what I've found so far.

1) The latest commit does fix the issue of not being able to restore user_data for the V1 API. So it does basically work now - if I try to submit a recent, valid saved user_data then the initial authentication request phase is skipped and accessing client.temperatures() works as expected.

2) Once the session is saved and restored using user_data, I seem to be able to poll as much as I like without any rate limiting. Only the initial authentication step is rate limited, as we suspected. This is great news because it means once you successfully authenticate, as long as you keep using the session before it expires (thus extending it) there shouldn't be any failed requests, other than genuine server outages of course.... and we all know Honeywell never has those. ;-)

3) Even if I get myself rate limited on another test instance that doesn't support the new user_data restore process and always goes through the authentication phase, my instance that has cached user_data from a previous session is still able to successfully poll, as you'd expect. So even if you get rate limited for authentication any instances that are already authenticated don't get penalised for this.

4) Authentication rate limiting is definitely shared between V1 and V2 API's even though the exact URL differs. (It is the same server though) I turned my normal graphing system 5 minute poll off for a while to ensure I hadn't built up any rate limit credit, repeatedly polled the V1 API authentication mechanism until I was rate limited with HTTP 429 and then tried the V2 API - I was immediately rejected with the same HTTP 429 response. So this is something to keep in mind for any scripts or systems that poll both V1 and V2 API's within a short time of each other, as I do. If you rate limit yourself on one you'll be locked out of authenticating on the other for a while as well!

5) I did a couple of times see an "HTTP/1.1 503 Service Unavailable" response to V1 authentication instead of the usual "HTTP/1.1 429 Too Many Requests". Perhaps that was a server genuinely having an issue.

6) I haven't checked exactly how long the session remains valid without use (I'm assuming @gordonb3's 15 minutes still applies) but as expected, it fails if the session has expired. Here is a redacted version of what a session failure looks like due to the session having expired:

DEBUG: SAVED user_data: {u'sessionId': u'C93F8B26-7231-4A5D-8707-26B7644E33CB', u'userInfo': {u'username': u'********@*****.com', u'city': u'*******', u'isActivated': True, u'firstname': u'*****', u'lastname': u'*****', u'userID': ******, u'zipcode': u'******', u'telephone': u'', u'deviceCount': 0, u'streetAddress': u'**********', u'securityQuestion3': u'NotUsed', u'securityQuestion2': u'NotUsed', u'securityQuestion1': u'NotUsed', u'country': u'GB', u'userLanguage': u'en-GB', u'tenantID': 5, u'latestEulaAccepted': False}}
DEBUG: SAVED last_access_date: 2019-02-27 14:21:55.028727
INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): tccna.honeywell.com
send: 'GET /WebAPI/api/locations?userId=1108128&allData=True HTTP/1.1\r\nHost: tccna.honeywell.com\r\nContent-Length: 2\r\nAccept-Encoding: gzip, deflate\r\nsessionId: C93F8B26-7231-4A5D-8707-26B7644E33CB\r\nAccept: */*\r\nUser-Agent: python-requests/2.9.1\r\nConnection: keep-alive\r\n\r\n{}'
reply: 'HTTP/1.1 401 Unauthorized\r\n'
header: Cache-Control: no-cache
header: Pragma: no-cache
header: Content-Type: application/json; charset=utf-8
header: Expires: -1
header: Server: Microsoft-IIS/8.5
header: WWW-Authenticate: Bearer
header: Server: Web1
header: Date: Thu, 28 Feb 2019 08:57:52 GMT
header: Content-Length: 74
header: Set-Cookie: NSC_UDDOB-XfcBqj-TTM-WT=ffffffff090ecc1a45525d5f4f58455e445a4a42378b;expires=Thu, 28-Feb-2019 08:59:52 GMT;path=/;secure;httponly
DEBUG:requests.packages.urllib3.connectionpool:"GET /WebAPI/api/locations?userId=1108128&allData=True HTTP/1.1" 401 74
Traceback (most recent call last):
  File "./evohome_V1_token", line 28, in <module>
    for device in client.temperatures():
  File "build/bdist.linux-armv6l/egg/evohomeclient/__init__.py", line 135, in temperatures
  File "build/bdist.linux-armv6l/egg/evohomeclient/__init__.py", line 83, in _populate_full_data
  File "/usr/local/lib/python2.7/dist-packages/requests/models.py", line 840, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: https://tccna.honeywell.com/WebAPI/api/locations?userId=*******&allData=True

At the moment of course the library doesn't handle attempting to re-authenticate automatically. Because the error doesn't happen until a call is made it's also kind of messy and cumbersome for the calling script to detect this error and then try to re-call the EvohomeClient() function with a cleared out user_data field to force full re-authentication.

Is there anything we could do to improve and automate this process ? Perhaps if the library makes a note of whether it initially attempted to use a client provided session id, then if it receives an HTTP 401 Unauthorised response when actually trying to call the API, (not authenticate) automatically re-authenticate on the behalf of the client script ?

That hugely simplifies the tracking needed to be done manually by the client to simply saving user_data and then re-presenting it next time, without worrying about how old the session is. (Since we never truly know how long the session lasts anyway, today it is 15 minutes, but Honeywell could change it at any time in the future so hard coding anything in either the client scripts or library seems like a bad idea)

If the provided saved session id works, it works, if it fails the library could catch the exception and attempt a full re-authentication using the previously provided username and password and transparently resume the failed request for the client script without it ever knowing that the previous session had expired.

Because the failed use of the old session is not an authentication attempt it shouldn't count towards rate limiting so the failed use of an old session id should be harmless.

I don't know how feasible that is within the current framework of the library without a lot of work of course, but the simpler and cleaner the process is for the client the better. Non-persistent clients are already being expected to do a lot of extra legwork to work around the new authentication rate limits that they never had to do in the past.

watchforstock commented 5 years ago

@DBMandrake I agree that adding seamless retries on the v1 client would be really nice. I also suspect that we could wrap the current calls so it won't be that much effort. Will perhaps get a chance to look at this this evening

gordonb3 commented 5 years ago

As promised, I verified Simon's findings with the old app key by starting a script that calls for the evo-demo app I created every 6 minutes. During the last 5 hours I received a failure while calling for status (at 09:56) and 12 minutes later both the access token and the refresh token failed. Since 10:09 I've had 4 refreshes and the current refresh token is the same as the one from 10:09.

All this time I've had a second instance running in the form of Domoticz (an authored version actually which I've named Oikomaticz) which performed token refreshes at:

2019-02-28 09:47:10.868 Debug: (Evohome) refresh v2 session token
2019-02-28 10:48:08.880 Debug: (Evohome) refresh v2 session token
2019-02-28 11:49:03.599 Debug: (Evohome) refresh v2 session token
2019-02-28 12:50:03.597 Debug: (Evohome) refresh v2 session token
2019-02-28 13:51:04.313 Debug: (Evohome) refresh v2 session token 

The Domoticz instance calls for status every 2 minutes and I've not seen a single error being logged there during this time.

DBMandrake commented 5 years ago

As promised, I verified Simon's findings with the old app key by starting a script that calls for the evo-demo app I created every 6 minutes. During the last 5 hours I received a failure while calling for status (at 09:56) and 12 minutes later both the access token and the refresh token failed. Since 10:09 I've had 4 refreshes and the current refresh token is the same as the one from 10:09.

Sorry you've lost me a bit. Which finding have you confirmed ? That there can only be one valid refresh_token at a time ?

zxdavb commented 5 years ago

The issue is, more correctly: when an account creates a refresh token for any reason are all previous refresh tokens invalidated, (even if they're not due to expire).

If so, I think two different systems could share the same access tokens, but they could never share refresh tokens.

DBMandrake commented 5 years ago

The issue is, more correctly: when an account creates a refresh token for any reason are all previous refresh tokens invalidated, (even if they're not due to expire).

Yes that's exactly the question, my own (limited) testing suggested that the when one instance authenticated and received a new refresh_token, the refresh_token of a previous instance was no longer able to re-authenticate it and it had to fall back to username/password.

But I can't quite work out if that's what @gordonb3 is confirming or not. :)

If so, I think two different systems could share the same access tokens, but they could never share refresh tokens.

They could share the same access token, but I don't think there's any need for them to do so or that it provides any advantage. The Honeywell servers seem to be perfectly happy to have multiple (at least two) access_token's active at once with their own independent expiry times from my testing.

zxdavb commented 5 years ago

What you could do is:

Have one instance operating normally, initially with a set of user credentials, and then using a refresh token from that point on.

This would need to be a persistent session.

The other instance(s) could be provided with a access token only (and I think also invalid user credentials to avoid tipping the API limit).

Their access token (and expiry time) would need to be pulled from the first instance.

I guess this would work only where the subsequent instances fired up, interrogated Honeywell's servers and then stopped (which I know some home automation systems do).

The only other option is to have a facility to inject refresh tokens into running instances of the component. whichever component first used at refresh token, it would have to send a message to the other instances that are updated refresh token was available!

Maybe more realistic would be for the multiple instances all to have a handle to the same instance of evohome-client.

DBMandrake commented 5 years ago

@zxdavb I've just tried to retest this and I can no longer get re-authentication via refresh_token to work at all even with a single instance.

To do this I have two versions of the script, the second one shares the same on disk token storage file but connects with no username/password and with the date passed to EvohomeClient() deliberately fudged to be a little in the past. Thus the library is forced to try to re-authenticate and only has a refresh_token available to do so.

This was working for me a day or two ago but not now! This is what I get:

pi@pi1monitor:~ $ ./evohome_V2_token_refresh
INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): tccna.honeywell.com
send: 'POST /Auth/OAuth/Token HTTP/1.1\r\nHost: tccna.honeywell.com\r\nContent-Length: 272\r\nAccept-Encoding: gzip, deflate\r\nAccept: application/json, application/xml, text/json, text/x-json, text/javascript, text/xml\r\nUser-Agent: python-requests/2.9.1\r\nConnection: keep-alive\r\nContent-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic NGEyMzEwODktZDJiNi00MWJkLWE1ZWItMTZhMGE0MjJiOTk5OjFhMTVjZGI4LTQyZGUtNDA3Yi1hZGQwLTA1OWY5MmM1MzBjYg==\r\n\r\nUsername=&Password=&Connection=Keep-Alive&Host=rs.alarmnet.com%2F&Pragma=no-cache&Cache-Control=no-store+no-cache&scope=EMEA-V1-Basic+EMEA-V1-Anonymous+EMEA-V1-Get-Current-User-Account&grant_type=password&Content-Type=application%2Fx-www-form-urlencoded%3B+charset%3Dutf-8'
reply: 'HTTP/1.1 400 Bad Request\r\n'
header: Cache-Control: no-cache
header: Pragma: no-cache
header: Content-Type: application/json;charset=UTF-8
header: Expires: -1
header: Server: Microsoft-IIS/8.5
header: Set-Cookie: thlang=en-US; expires=Thu, 28-Feb-2069 14:34:03 GMT; path=/
header: Server: Web1
header: Date: Thu, 28 Feb 2019 14:34:02 GMT
header: Content-Length: 25
header: Set-Cookie: NSC_UDDOB-TTM-WT=ffffffff090ecc0345525d5f4f58455e445a4a42378b;expires=Thu, 28-Feb-2019 14:36:03 GMT;path=/;secure;httponly
DEBUG:requests.packages.urllib3.connectionpool:"POST /Auth/OAuth/Token HTTP/1.1" 400 25
Traceback (most recent call last):
  File "./evohome_V2_token_refresh", line 30, in <module>
    client = EvohomeClient('', '', refresh_token=refresh_token, access_token=access_token, access_token_expires=access_token_expires, debug=True)
  File "build/bdist.linux-armv6l/egg/evohomeclient2/__init__.py", line 44, in __init__
  File "build/bdist.linux-armv6l/egg/evohomeclient2/__init__.py", line 47, in _login
  File "build/bdist.linux-armv6l/egg/evohomeclient2/__init__.py", line 166, in user_account
  File "build/bdist.linux-armv6l/egg/evohomeclient2/__init__.py", line 54, in _headers
  File "build/bdist.linux-armv6l/egg/evohomeclient2/__init__.py", line 86, in _basic_login
ValueError: Bad Username/Password, unable to continue.

Notice that the initial request is trying to pass a blank username and password and not a refresh_token even though I have provided EvohomeClient() the refresh_token. Back when it was working I remember it showing the refresh_token in the request headers instead of username and password.

I am currently running from the allow_session_reuse branch from this repo, when I last tested it and it was working I was testing from your exception_handling branch.

I've got a feeling that the fixes you put in that branch have not found their way over to the allow_session_reuse branch. Can you double check that this branch has the same fixes and there hasn't been another merge problem relating to refresh_token handling ?

DBMandrake commented 5 years ago

Reverted to the exception_handling_branch and retested and the problem is still there - it's trying to send a blank username and password instead of the refresh_token. So I'm at a lost to understand why the refresh_token is not being used now when the access_token_expiry time is up, when I did see it working before. Unless the server responses have changed...

zxdavb commented 5 years ago

Hang on - there was a merge that went wrong somewhere, let me check.

gordonb3 commented 5 years ago

Sorry you've lost me a bit. Which finding have you confirmed ? That there can only be one valid refresh_token at a time ?

No. The opposite. I had two individual systems running with their own refresh token and neither one invalidated the other. Second: using a refresh token does not invalidate it.

Also, I don't believe I used the word confirm, but from these results and since you are apparently witnessing complete different behaviour I'd say we can safely conclude that it is confirmed that the new app key introduced in this project in commit bfad80c prompts for additional usage limits to be imposed by the portal.

DBMandrake commented 5 years ago

Hang on - there was a merge that went wrong somewhere, let me check.

Sorry, I've found a bug in my script that may explain the refresh_token not working. Testing.

DBMandrake commented 5 years ago

Apologies all. I had a stupid datetime bug in my script in a line that is usually commented out (the one fudging the date) and the resulting error was being swallowed up by the try/except handling so I didn't realise it was failing to pass a refresh_token. Whoops.

So the refresh_token is now working as expected, and furthermore I can confirm @gordonb3's finding that a new session generating a new refresh_token does not invalidate the refresh_token of an older session. I tried it many times and it continued to work. So I think we can stop worrying about the refresh_token!

zxdavb commented 5 years ago

Apologies all. [...] Whoops.

I was just about to post that the latest version of allow_session_reuse looks fine to me.

Mind you, it is only 5h old, and I've just pushed #75 🤣

So I think we can stop worrying about the refresh_token

Woot!

DBMandrake commented 5 years ago

I was just about to post that the latest version of allow_session_reuse looks fine to me.

Mind you, it is only 5h old, and I've just pushed #75

I've been testing 02f1b53d4867d6bea44b04ba26b72198f34bd9c2 but will update to the latest now.

DBMandrake commented 5 years ago

@zxdavb Is this amount of repetition of the new debug mode enabled/disabled debug message to be expected ?

reply: 'HTTP/1.1 200 OK\r\n'
header: Cache-Control: no-cache
header: Pragma: no-cache
header: Content-Type: application/json; charset=utf-8
header: Expires: -1
header: Server: Microsoft-IIS/8.5
header: Server: Web1
header: Date: Thu, 28 Feb 2019 15:39:08 GMT
header: Content-Length: 11808
header: Set-Cookie: NSC_UDDOB-XfcBqj-TTM-WT=ffffffff090ecc1f45525d5f4f58455e445a4a42378b;expires=Thu, 28-Feb-2019 15:41:08 GMT;path=/;secure;httponly
DEBUG:requests.packages.urllib3.connectionpool:"GET /WebAPI/emea/api/v1/location/installationInfo?userId=*******&includeTemperatureControlSystems=True HTTP/1.1" 200 11808
DEBUG:evohomeclient2.base:__init__(): Debug mode was not explicitly enabled.
DEBUG:evohomeclient2.base:__init__(): Debug mode was not explicitly enabled.
DEBUG:evohomeclient2.base:__init__(): Debug mode was not explicitly enabled.
DEBUG:evohomeclient2.base:__init__(): Debug mode was not explicitly enabled.
DEBUG:evohomeclient2.base:__init__(): Debug mode was not explicitly enabled.
DEBUG:evohomeclient2.base:__init__(): Debug mode was not explicitly enabled.
DEBUG:evohomeclient2.base:__init__(): Debug mode was not explicitly enabled.
DEBUG:evohomeclient2.base:__init__(): Debug mode was not explicitly enabled.
DEBUG:evohomeclient2.base:__init__(): Debug mode was not explicitly enabled.
DEBUG:evohomeclient2.base:__init__(): Debug mode was not explicitly enabled.
DEBUG:evohomeclient2.base:__init__(): Debug mode was not explicitly enabled.
INFO:requests.packages.urllib3.connectionpool:Starting new HTTPS connection (1): tccna.honeywell.com
gordonb3 commented 5 years ago

That hugely simplifies the tracking needed to be done manually by the client to simply saving user_data and then re-presenting it next time, without worrying about how old the session is. (Since we never truly know how long the session lasts anyway, today it is 15 minutes, but Honeywell could change it at any time in the future so hard coding anything in either the client scripts or library seems like a bad idea)

In essence correct, although to evaluate its usability I do currently include a timestamp value for the last web call to the portal when saving the session parameters. I actually do doubt that they might ever change the timeout on the v1 API sessions though, because this would also affect their original phone app (and possibly RFG100 and wifi controller connections as well).