core: Ecobee integration unable to generate refresh token

The problem

Ecobee integration stopped working. Sensors and climate entities became undefined.

Environment


arch | x86_64
-- | --
dev | false
docker | true
hassio | true
os_name | Linux
os_version | 4.19.107
python_version | 3.7.7
timezone | America/Chicago
version | 0.108.3
virtualenv | false

Home Assistant 0.108.3 Supervisor 217 HassOS 3.12

  • Home Assistant Core release with the issue: 0.108.3
  • Last working Home Assistant Core release (if known): 0.108.3
  • Operating environment (Home Assistant/Supervised/Docker/venv): Supervised
  • Integration causing this issue: ecobee
  • Link to integration documentation on our website: https://www.home-assistant.io/integrations/ecobee/

Problem-relevant configuration.yaml

configured via configuration flow in ui.

Traceback/Error logs

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/pyecobee/__init__.py", line 518, in _request
    response.raise_for_status()
  File "/usr/local/lib/python3.7/site-packages/requests/models.py", line 941, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://api.ecobee.com/token?grant_type=refresh_token&refresh_token=xxx&client_id=xxx

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/config_entries.py", line 216, in async_setup
    hass, self
  File "/usr/src/homeassistant/homeassistant/components/ecobee/__init__.py", line 58, in async_setup_entry
    if not await data.refresh():
  File "/usr/src/homeassistant/homeassistant/components/ecobee/__init__.py", line 105, in refresh
    if await self._hass.async_add_executor_job(self.ecobee.refresh_tokens):
  File "/usr/local/lib/python3.7/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/usr/local/lib/python3.7/site-packages/pyecobee/__init__.py", line 153, in refresh_tokens
    auth_request=True,
  File "/usr/local/lib/python3.7/site-packages/pyecobee/__init__.py", line 533, in _request
    "ecobee tokens invalid; re-authentication required"
pyecobee.errors.InvalidTokenError: ecobee tokens invalid; re-authentication required```

Additional information

I’ve had this happen once before. I can remove the integration and reinstall it to solve the problem The link given above appears to be resolving properly when I ran a curl command to it so I am eliminating local network issues. Is this a transient error occurring randomly? Or is it a regular timeout associated with the ecobee api? Is there anything I can do to prevent it in the future?

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 5
  • Comments: 113 (47 by maintainers)

Most upvoted comments

@quielb This is actually a different issue (you are receiving the ExpiredTokenError, not the InvalidTokenError) and I will explain why it is happening for the benefit of the thread:

When you request tokens from ecobee you get two important tokens: an access token and a refresh token. The access token lasts for one hour. The refresh token lasts for one year OR until it is used to request a new access token.

In the normal course of things, the integration updates its values either every three minutes or when you take an action (i.e. set the temperature, change the mode, etc.). If the request indicates that the access token has expired, then the ExpiredTokenError is raised.

If the Error is raised from a regular every-three-minute update, the Error is caught and the tokens are refreshed and you get a new access token, which all happens transparently to the user.

Currently, other ExpiredTokenErrors are not caught (e.g. any action on the thermostat - setting e.g. temp or mode) and this Error is raised. The reason that ecobee goes on working for you through the day is that the next update refreshes the token.

This error should only show up rarely as you need to be right at the end of the access token validity period, and THEN take an action for the access token to have been expired but not-yet-refreshed.

Once I solve the InvalidTokenError that keeps popping up, I plan to introduce a mechanism to the integration that will catch all ExpiredTokenErrors, refresh, and then automatically retry the failed action so that the action completes transparently for the user.

As a workaround, is there a way to detect this and just start a re-auth workflow without having to delete and re-add the whole integration? Sorry if I’m messing up terminology. This is what used to happen. The front end (non-Lovelace, non-integration) would show that Ecobee isn’t authorized and you would get prompted to do it again.

Alrighty, that took longer to reproduce then I had hoped but here we are. I was able to reproduce by following these steps:

  1. Ensure ecobee is configured correctly. Update from 0.111.4 to 0.112.3
  2. (this step is likely not needed but I did it just to try) reboot HA a few times ensuring that ecobee continued to work.
  3. Wait for an hour for the access token provided by ecobee to expire.
  4. Reboot HA and note that the entities are now “unavailable”
  5. Reboot HA again because on step 4 it gets stuck in trying to start HA but it has the note about still starting for a long time (10+ minutes)
  6. Note that everything ecobee continues to be unavailable.

Now, I have debug logs of all those steps. Going to try and pull in the parts around pyecobee and homeassistant.components.ecobee. I will still have the full logs so we can search through them more but I don’t really want to try and sanitize the entire log.

2020-07-07 19:29:55 DEBUG (MainThread) [homeassistant.components.ecobee] Refreshing ecobee tokens and updating config entry
2020-07-07 19:29:55 DEBUG (SyncWorker_32) [pyecobee] Making request to token endpoint to refresh tokens: url: https://api.ecobee.com/token, headers: {}, params: {'grant_type': 'refresh_token', 'refresh_token': 'NXXXXXXXXXXXXXXXjbFS', 'client_id': '<MY CLIENT ID>'}, body: None
2020-07-07 19:29:55 DEBUG (SyncWorker_32) [pyecobee] Request response: 200: {'access_token': 'XXXXXXXXXXXXm1KR', 'token_type': 'Bearer', 'expires_in': 3599, 'refresh_token': 'xXXXXXXXXXXXXXXXlTM5', 'scope': 'smartWrite'}
2020-07-07 19:29:55 DEBUG (SyncWorker_32) [pyecobee] Refreshed tokens from ecobee: access XXXXXXXXXXXm1KR, refresh XXXXXXXXXXXXlTM5
2020-07-07 19:29:55 DEBUG (SyncWorker_12) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXm1KR'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 19:29:56 DEBUG (SyncWorker_12) [pyecobee] Request response: 200: <snip>
2020-07-07 19:29:56 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee

------------- REBOOT HERE ----------------
This is the reboot where nothing changed.
------------------------------------------

2020-07-07 19:32:49 DEBUG (MainThread) [homeassistant.components.ecobee] Refreshing ecobee tokens and updating config entry
2020-07-07 19:32:49 DEBUG (SyncWorker_34) [pyecobee] Making request to token endpoint to refresh tokens: url: https://api.ecobee.com/token, headers: {}, params: {'grant_type': 'refresh_token', 'refresh_token': 'xXXXXXXXXXXXXXXXlTM5', 'client_id': '<MY CLIENT ID>'}, body: None
2020-07-07 19:32:50 DEBUG (SyncWorker_34) [pyecobee] Request response: 200: {'access_token': 'XXXXXXXXXXXXTk0v', 'token_type': 'Bearer', 'expires_in': 3599, 'refresh_token': 'yXXXXXXXXXXXXXXXg3S7', 'scope': 'smartWrite'}
2020-07-07 19:32:50 DEBUG (SyncWorker_34) [pyecobee] Refreshed tokens from ecobee: access XXXXXXXXXXXTk0v, refresh XXXXXXXXXXXXg3S7
2020-07-07 19:32:50 DEBUG (SyncWorker_30) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXTk0v'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 19:32:50 DEBUG (SyncWorker_30) [pyecobee] Request response: 200: <snip>
2020-07-07 19:32:50 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 19:35:54 DEBUG (SyncWorker_10) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXTk0v'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 19:35:54 DEBUG (SyncWorker_10) [pyecobee] Request response: 200: <snip>
2020-07-07 19:35:54 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee

------------- REBOOT HERE ----------------
This is the reboot where nothing changed.
------------------------------------------

2020-07-07 19:37:31 DEBUG (MainThread) [homeassistant.components.ecobee] Refreshing ecobee tokens and updating config entry
2020-07-07 19:37:31 DEBUG (SyncWorker_30) [pyecobee] Making request to token endpoint to refresh tokens: url: https://api.ecobee.com/token, headers: {}, params: {'grant_type': 'refresh_token', 'refresh_token': 'yXXXXXXXXXXXXXXXg3S7', 'client_id': '<MY CLIENT ID>'}, body: None
2020-07-07 19:37:31 DEBUG (SyncWorker_30) [pyecobee] Request response: 200: {'access_token': 'XXXXXXXXXXXXGRZL', 'token_type': 'Bearer', 'expires_in': 3599, 'refresh_token': 'AXXXXXXXXXXXXXXXtpzX', 'scope': 'smartWrite'}
2020-07-07 19:37:31 DEBUG (SyncWorker_30) [pyecobee] Refreshed tokens from ecobee: access XXXXXXXXXXXGRZL, refresh XXXXXXXXXXXXtpzX
2020-07-07 19:37:31 DEBUG (SyncWorker_16) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXGRZL'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 19:37:32 DEBUG (SyncWorker_16) [pyecobee] Request response: 200: <snip>
2020-07-07 19:37:32 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 19:40:35 DEBUG (SyncWorker_33) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXGRZL'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 19:40:35 DEBUG (SyncWorker_33) [pyecobee] Request response: 200: <snip>
2020-07-07 19:40:35 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee

------------- REBOOT HERE ----------------
This is the reboot where nothing changed.
------------------------------------------

2020-07-07 19:42:38 DEBUG (MainThread) [homeassistant.components.ecobee] Refreshing ecobee tokens and updating config entry
2020-07-07 19:42:38 DEBUG (SyncWorker_33) [pyecobee] Making request to token endpoint to refresh tokens: url: https://api.ecobee.com/token, headers: {}, params: {'grant_type': 'refresh_token', 'refresh_token': 'AXXXXXXXXXXXXXXXtpzX', 'client_id': '<MY CLIENT ID>'}, body: None
2020-07-07 19:42:39 DEBUG (SyncWorker_33) [pyecobee] Request response: 200: {'access_token': 'XXXXXXXXXXXXqtNr', 'token_type': 'Bearer', 'expires_in': 3599, 'refresh_token': 'XXXXXXXXXXXXXXXXUeIZ', 'scope': 'smartWrite'}
2020-07-07 19:42:39 DEBUG (SyncWorker_33) [pyecobee] Refreshed tokens from ecobee: access XXXXXXXXXXXqtNr, refresh XXXXXXXXXXXXUeIZ
2020-07-07 19:42:39 DEBUG (SyncWorker_9) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 19:42:39 DEBUG (SyncWorker_9) [pyecobee] Request response: 200: <snip>
2020-07-07 19:42:39 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 19:45:43 DEBUG (SyncWorker_24) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 19:45:43 DEBUG (SyncWorker_24) [pyecobee] Request response: 200: <snip>
2020-07-07 19:45:43 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 19:48:46 DEBUG (SyncWorker_20) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 19:48:46 DEBUG (SyncWorker_20) [pyecobee] Request response: 200: <snip>
2020-07-07 19:48:46 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 19:51:49 DEBUG (SyncWorker_26) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 19:51:49 DEBUG (SyncWorker_26) [pyecobee] Request response: 200: <snip>
2020-07-07 19:51:49 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 19:54:52 DEBUG (SyncWorker_2) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 19:54:52 DEBUG (SyncWorker_2) [pyecobee] Request response: 200: <snip>
2020-07-07 19:54:52 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 19:57:55 DEBUG (SyncWorker_35) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 19:57:55 DEBUG (SyncWorker_35) [pyecobee] Request response: 200: <snip>
2020-07-07 19:57:55 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:00:58 DEBUG (SyncWorker_10) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:00:59 DEBUG (SyncWorker_10) [pyecobee] Request response: 200: <snip>
2020-07-07 20:00:59 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:04:01 DEBUG (SyncWorker_9) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:04:01 DEBUG (SyncWorker_9) [pyecobee] Request response: 200: <snip>
2020-07-07 20:04:01 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:07:04 DEBUG (SyncWorker_19) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:07:04 DEBUG (SyncWorker_19) [pyecobee] Request response: 200: <snip>
2020-07-07 20:07:04 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:10:07 DEBUG (SyncWorker_10) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:10:09 DEBUG (SyncWorker_10) [pyecobee] Request response: 200: <snip>
2020-07-07 20:10:09 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:13:10 DEBUG (SyncWorker_30) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:13:10 DEBUG (SyncWorker_30) [pyecobee] Request response: 200: <snip>
2020-07-07 20:13:10 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:16:11 DEBUG (SyncWorker_16) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:16:11 DEBUG (SyncWorker_16) [pyecobee] Request response: 200: <snip>
2020-07-07 20:16:11 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:19:15 DEBUG (SyncWorker_34) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:19:15 DEBUG (SyncWorker_34) [pyecobee] Request response: 200: <snip>
2020-07-07 20:19:15 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:22:19 DEBUG (SyncWorker_9) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:22:19 DEBUG (SyncWorker_9) [pyecobee] Request response: 200: <snip>
2020-07-07 20:22:19 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:25:22 DEBUG (SyncWorker_36) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:25:22 DEBUG (SyncWorker_36) [pyecobee] Request response: 200: <snip>
2020-07-07 20:25:22 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:28:25 DEBUG (SyncWorker_34) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:28:25 DEBUG (SyncWorker_34) [pyecobee] Request response: 200: <snip>
2020-07-07 20:28:25 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:31:28 DEBUG (SyncWorker_31) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:31:28 DEBUG (SyncWorker_31) [pyecobee] Request response: 200: <snip>
2020-07-07 20:31:28 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:34:31 DEBUG (SyncWorker_15) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:34:31 DEBUG (SyncWorker_15) [pyecobee] Request response: 200: <snip>
2020-07-07 20:34:31 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:37:34 DEBUG (SyncWorker_11) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:37:34 DEBUG (SyncWorker_11) [pyecobee] Request response: 200: <snip>
2020-07-07 20:37:34 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:40:37 DEBUG (SyncWorker_10) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:40:37 DEBUG (SyncWorker_10) [pyecobee] Request response: 200: <snip>
2020-07-07 20:40:37 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:43:40 DEBUG (SyncWorker_14) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXXqtNr'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:43:40 DEBUG (SyncWorker_14) [pyecobee] Request response: 500: {'status': {'code': 14, 'message': 'Authentication token has expired. Refresh your tokens. '}}
2020-07-07 20:43:40 DEBUG (MainThread) [homeassistant.components.ecobee] Refreshing expired ecobee tokens
2020-07-07 20:43:40 DEBUG (MainThread) [homeassistant.components.ecobee] Refreshing ecobee tokens and updating config entry
2020-07-07 20:43:40 DEBUG (SyncWorker_11) [pyecobee] Making request to token endpoint to refresh tokens: url: https://api.ecobee.com/token, headers: {}, params: {'grant_type': 'refresh_token', 'refresh_token': 'XXXXXXXXXXXXXXXXUeIZ', 'client_id': '<MY CLIENT ID>'}, body: None
2020-07-07 20:43:40 DEBUG (SyncWorker_11) [pyecobee] Request response: 200: {'access_token': 'XXXXXXXXXXXX7QJF', 'token_type': 'Bearer', 'expires_in': 3599, 'refresh_token': 'LXXXXXXXXXXXXXXX8aGD', 'scope': 'smartWrite'}
2020-07-07 20:43:40 DEBUG (SyncWorker_11) [pyecobee] Refreshed tokens from ecobee: access XXXXXXXXXXX7QJF, refresh XXXXXXXXXXXX8aGD
2020-07-07 20:46:43 DEBUG (SyncWorker_2) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXX7QJF'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:46:43 DEBUG (SyncWorker_2) [pyecobee] Request response: 200: <snip>
2020-07-07 20:46:43 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:49:44 DEBUG (SyncWorker_33) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXX7QJF'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:49:44 DEBUG (SyncWorker_33) [pyecobee] Request response: 200: <snip>
2020-07-07 20:49:44 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:51:38 DEBUG (SyncWorker_13) [pyecobee] Making request to thermostat endpoint to set climate hold: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXX7QJF'}, params: None, body: {'selection': {'selectionType': 'thermostats', 'selectionMatch': '521757101845'}, 'functions': [{'type': 'setHold', 'params': {'holdType': 'nextTransition', 'holdClimateRef': 'smart1'}}]}
2020-07-07 20:51:39 DEBUG (SyncWorker_13) [pyecobee] Request response: 200: {'status': {'code': 0, 'message': ''}}
2020-07-07 20:51:39 DEBUG (SyncWorker_8) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXX7QJF'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:51:39 DEBUG (SyncWorker_8) [pyecobee] Request response: 200: <snip>
2020-07-07 20:51:39 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:54:50 DEBUG (SyncWorker_33) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXX7QJF'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:54:50 DEBUG (SyncWorker_33) [pyecobee] Request response: 200: <snip>
2020-07-07 20:54:50 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:57:53 DEBUG (SyncWorker_10) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXX7QJF'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:57:53 DEBUG (SyncWorker_10) [pyecobee] Request response: 200: <snip>
2020-07-07 20:57:53 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee

------------- REBOOT HERE ----------------
This is the reboot where things start going bad even though it seems the requests are working.
------------------------------------------

2020-07-07 20:59:02 DEBUG (MainThread) [homeassistant.components.ecobee] Refreshing ecobee tokens and updating config entry
2020-07-07 20:59:02 DEBUG (SyncWorker_31) [pyecobee] Making request to token endpoint to refresh tokens: url: https://api.ecobee.com/token, headers: {}, params: {'grant_type': 'refresh_token', 'refresh_token': 'LXXXXXXXXXXXXXXX8aGD', 'client_id': '<MY CLIENT ID>'}, body: None
2020-07-07 20:59:03 DEBUG (SyncWorker_31) [pyecobee] Request response: 200: {'access_token': 'XXXXXXXXXXXX7H8c', 'token_type': 'Bearer', 'expires_in': 3598, 'refresh_token': '5XXXXXXXXXXXXXXXnNo0', 'scope': 'smartWrite'}
2020-07-07 20:59:03 DEBUG (SyncWorker_31) [pyecobee] Refreshed tokens from ecobee: access XXXXXXXXXXX7H8c, refresh XXXXXXXXXXXXnNo0
2020-07-07 20:59:03 DEBUG (SyncWorker_33) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXX7H8c'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:59:03 DEBUG (SyncWorker_33) [pyecobee] Request response: 200: <snip>
2020-07-07 20:59:04 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee
2020-07-07 20:59:06 DEBUG (SyncWorker_11) [pyecobee] Making request to thermostat endpoint to set climate hold: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXX7H8c'}, params: None, body: {'selection': {'selectionType': 'thermostats', 'selectionMatch': '521757101845'}, 'functions': [{'type': 'setHold', 'params': {'holdType': 'nextTransition', 'holdClimateRef': 'sleep'}}]}
2020-07-07 20:59:06 DEBUG (SyncWorker_11) [pyecobee] Request response: 200: {'status': {'code': 0, 'message': ''}}
2020-07-07 20:59:07 DEBUG (SyncWorker_35) [pyecobee] Making request to thermostat endpoint to get thermostats: url: https://api.ecobee.com/1/thermostat, headers: {'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer XXXXXXXXXXX7H8c'}, params: {'json': '{"selection": {"selectionType": "registered", "includeRuntime": "true", "includeSensors": "true", "includeProgram": "true", "includeEquipmentStatus": "true", "includeEvents": "true", "includeWeather": "true", "includeSettings": "true"}}'}, body: None
2020-07-07 20:59:07 DEBUG (SyncWorker_35) [pyecobee] Request response: 200: <snip>
2020-07-07 20:59:07 DEBUG (MainThread) [homeassistant.components.ecobee] Updating ecobee

------------- REBOOT HERE ----------------
This is a reboot where it actually looks like things are failing based upon the log.
------------------------------------------

2020-07-07 21:04:15 DEBUG (MainThread) [homeassistant.components.ecobee] Refreshing ecobee tokens and updating config entry
2020-07-07 21:04:15 DEBUG (SyncWorker_28) [pyecobee] Making request to token endpoint to refresh tokens: url: https://api.ecobee.com/token, headers: {}, params: 'refresh_token', 'refresh_token': 'XXXXXXXXXXXXXX8aGD', 'client_id': '<MY CLIENT ID>'}, body: None
2020-07-07 21:04:15 DEBUG (SyncWorker_28) [pyecobee] Request response: 400: {'error': 'invalid_grant', 'error_description': 'The authorization grant, token or credentials are invalid, expired, revoked, do not match the redirection URI used in the authorization request, or was issued to another client.', 'error_uri': 'https://tools.ietf.org/html/rfc6749#section-5.2'}
2020-07-07 21:04:15 ERROR (MainThread) [homeassistant.config_entries] Error setting up entry ecobee for ecobee
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/pyecobee/__init__.py", line 564, in _request
    response.raise_for_status()
  File "/usr/local/lib/python3.7/site-packages/requests/models.py", line 941, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://api.ecobee.com/token?grant_type=refresh_token&refresh_token=XXXXXXXXXXXXXX8aGD&client_id=<MY CLIENT ID>

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/config_entries.py", line 220, in async_setup
    hass, self
  File "/usr/src/homeassistant/homeassistant/components/ecobee/__init__.py", line 58, in async_setup_entry
    if not await data.refresh():
  File "/usr/src/homeassistant/homeassistant/components/ecobee/__init__.py", line 105, in refresh
    if await self._hass.async_add_executor_job(self.ecobee.refresh_tokens):
  File "/usr/local/lib/python3.7/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/usr/local/lib/python3.7/site-packages/pyecobee/__init__.py", line 164, in refresh_tokens
    auth_request=True,
  File "/usr/local/lib/python3.7/site-packages/pyecobee/__init__.py", line 579, in _request
    "ecobee tokens invalid; re-authentication required"
pyecobee.errors.InvalidTokenError: ecobee tokens invalid; re-authentication required

Note that I masked the tokens just incase any of them are still valid. I left the last 4 characters and tried to keep them consistant. If the last 4 are the same then you should be able to assume they are fully the same.

@vinniefalco that is exactly what I am saying. Home Assistant contacts ecobee.com, which then contacts your thermostat, which then relays data to ecobee.com, which Home Assistant then polls for data.

Your ecobee is logged in to your ecobee account when you first set it up (it won’t work otherwise). The fact that your ecobee is behind your firewall is irrelevant. You were under the wrong impression - your ecobee is connected to the cloud (that’s how the ecobee app on your phone works too).

I just pushed a PR that will make the debug logging for the dependency more explicit so that we can trace more precisely where the problem lies. @rsnodgrass may be correct that there is a race condition going on here but we can’t know for sure until we see where the failing lies, in HA or in the dependency. I’ve asked for this to get put into a point release but we will see. Savvy testers could update the dependency themselves to 0.2.7 to get the improved debug logging.

You don’t need to remove the app. Just delete and re-add the integration, put in the dev api key, add the app using the 4 character code and it will replace the existing app. I have to do this a lot and ecobee’s website runs like garbage so I do whatever I can to make the process quicker.

Is there a temporary work around for this that I’m missing?

While it’s not ideal, the only way I have been able to get around this issue is by removing the integration, removing the app from the ecobee portal, and then re-adding the ecobee integration (via the UI), including the whole pin authorization process.

Any updates on this? Should I start digging a little?

@MartinHjelmare Can I run something by you here?

As background: ecobee’s API uses an auth token and a refresh token. The auth token is good for 1 hour, the refresh token is good for 1 year, or until it is used to refresh tokens, when it is then expired (a new refresh token is issued along with the new auth token on a successful refresh). When the auth token expires, pyecobee raises an exception and the integration refreshes the tokens. The new refresh token gets stored in the config entry by the refresh method:

   async def refresh(self) -> bool:
        """Refresh ecobee tokens and update config entry."""
        _LOGGER.debug("Refreshing ecobee tokens and updating config entry")
        if await self._hass.async_add_executor_job(self.ecobee.refresh_tokens):
            self._hass.config_entries.async_update_entry(
                self._entry,
                data={
                    CONF_API_KEY: self.ecobee.config[ECOBEE_API_KEY],
                    CONF_REFRESH_TOKEN: self.ecobee.config[ECOBEE_REFRESH_TOKEN],
                },
            )
            return True
        _LOGGER.error("Error refreshing ecobee tokens")
        return False

At startup of HA, the integration loads the stored refresh token from the entry, then refreshes tokens to get a new access token. As long as it hasn’t been 1 year since HA was started, the refresh procedure should yield a new access token.

The problem (that has shown up in this issue and several other issues since the conversion to config entries) is that occasionally when a user restarts HA, the refresh token raises an invalid exception (meaning, either it has expired after 1 year or it has already been used to refresh tokens - this exception gets raised in pyecobee when the API response indicates that the tokens have become invalid).

I have been digging trying to figure out why this would be happening. One of the only things I can think of is that the following (from the refresh method above) is not always actually updating the entry with the new refresh token:

            self._hass.config_entries.async_update_entry(
                self._entry,
                data={
                    CONF_API_KEY: self.ecobee.config[ECOBEE_API_KEY],
                    CONF_REFRESH_TOKEN: self.ecobee.config[ECOBEE_REFRESH_TOKEN],
                },
            )

So, when HA restarts, it loads an old refresh token from the entry and the ecobee API flags it as invalid.

Any thoughts/suggestions?

Once I solve the InvalidTokenError that keeps popping up, I plan to introduce a mechanism to the integration that will catch all ExpiredTokenErrors, refresh, and then automatically retry the failed action so that the action completes transparently for the user.

Hi, has there been any progress or workarounds for this specific issue? Every now and then one of my automations will fail due to expired token. I think some kind of try/catch with a retry would be the easiest solution,

Any others in this thread still experiencing the InvalidTokenError? I’m wondering if this has been solved through PRs elsewhere in HA.

Thanks for the clarification. To be honest I didn’t really compare my stack trace to the one already posted. I think I just let my frustration with the expired token get to me. I think I must have the worst luck of anybody on the planet. You mention that the error should show up rarely, but for me it is almost a daily occurrence. I have 3 or 4 automations that adjust the thermostat throughout the day. It seems that one of them fails, almost daily. If I can get 2 days without the expired token error, I’ve gotten luck. Looking forward to your fix.

@lmatter Sorry, thanks for bearing with me, getting the kinks worked out. Try the version attached to this message please! ecobee_cc_20200120.tar.gz

@rsnodgrass @j4ys0n

If anyone is feeling adventurous and wants to help me by testing, I’ve attached a tar.gz of the ecobee integration that you should be able to drop into custom_components that includes a possible fix. Before you do the following, delete the existing config entry for ecobee (you will need to add the integration and do the config flow again after loading this custom code) and stop Home Assistant.

ecobee.tar.gz

cd custom_components
tar zxvf ecobee_cc.tar.gz

Then, start HA, and add the ecobee integration.

Technical details: instead of saving the API_KEY and REFRESH_TOKEN to the config entry, I’m using the inbuilt load_json and save_json utils to retrieve and save them from ecobee.conf in the config directory. Please post results of testing here. The code is super dirty at the moment and needs to be cleaned up before PR’ing, but I want to see if this generally solves the problem before going further.

another upgrade, another re-activate

was only a few revs back, not sure what exactly. current: Host Operating System | Home Assistant OS 5.8 Supervisor Version | 2020.12.6 Home Assistant 2020.12.0

deleted, recreated app on ecobee dev … all back, still annoyed. 😃

As you can see above, I think I’ve tracked this to an underlying issue in the way that the refreshed keys are saved to the config entry. It does not appear to have anything to do with the integration or pyecobee code as such. If someone with more knowledge of the workings of HA wants to take a run at tracking down the issue, I would be happy to assist.

I ended up just removing and re-adding the integration… not ideal since ecobee’s website is garbage… but it worked.

@marthoc is there anything else in the logs you may want to see around this? To your note that this indicates it is not the ecobee integration I would point out that although I did not consider my smartthings integration in this test it does exhibit the same problem especially when I upgrade to 0.112.x

Ok. It’s not clear why that happens.

@rmevans9 Thank you for this, I think it is the clearest indication that my theory is correct: the code to update the config entry isn’t (always) doing so.

At 20:59:03 we see that the tokens are successfully refreshed (extra debug code I added to pyecobee confirms that step succeeds and returns True) - refresh token retrieved is nNo0.

After the reboot, at 21:04:15 when a refresh is attempted, refresh token 8aGD is used, which is the token from the previous refresh.

@MartinHjelmare I hate to keep tagging you on this issue, but I think this is the clearest indication I have yet that it’s not the ecobee integration code that is failing here…

@rmevans9 you need to have manually updated python-ecobee-api to get the debug logging, since it has not been included into a released version (even as of 0.112.3 released today, as far as I can tell).

It’s definitely the same issue as in the previous issue linked. That last issue was closed after things became less than friendly.

Here’s what I need from anyone experiencing the issue: enable debug logging for both the integration and pyecobee. Wait until the error happens. Then send me your home-assistant.log. I need to see the sequence of events leading up to the error occurring to trace where the problem lies.

Your help is needed!