Skip to content

fix: device flow client_id, form-encoded auth body, expired token fallback - #314

Merged
wmalgadey merged 4 commits into
wmalgadey:masterfrom
manW13-be:fix/device-flow-client-id-token-refresh
Jun 29, 2026
Merged

fix: device flow client_id, form-encoded auth body, expired token fallback#314
wmalgadey merged 4 commits into
wmalgadey:masterfrom
manW13-be:fix/device-flow-client-id-token-refresh

Conversation

@manW13-be

Copy link
Copy Markdown

Summary

Four bugs affecting the OAuth2 device flow and token refresh, confirmed against login.tado.com in production.

Bug 1 — Verification URL missing client_id

device_verification_url() returned a URL with only user_code. Opening it in a browser causes login.tado.com to reject the session with missing_client_id. Fixed: both user_code and client_id are now included.

Bug 2 — NOT_STARTED when saved token is expired

When a token file existed but the refresh failed (expired token), there was no fallback — status stayed NOT_STARTED indefinitely. Fixed: added the missing else branch to start a new device flow in that case.

Bug 3 — client_id as constructor parameter

The only way to pass a custom client_id was to monkey-patch PyTado.const.CLIENT_ID_DEVICE, a module-level global shared across all instances. Fixed: Http.__init__ and Tado.__init__ now accept client_id=None, defaulting to CLIENT_ID_DEVICE if not provided.

Bug 4 — Auth requests sent as query string + empty JSON body

_refresh_token, _login_device_flow, and device polling sent parameters via params= (query string) with data=json.dumps({}) and Content-Type: application/json. The Tado auth server requires application/x-www-form-urlencoded. Fixed on all three call sites.

Test plan

  • Device flow with a fresh token file: verification URL contains both user_code and client_id
  • Device flow with an expired token file: falls back to a new device flow instead of staying NOT_STARTED
  • Tado(client_id="custom-id") works without patching PyTado.const
  • Token refresh succeeds (form-encoded body accepted by login.tado.com)

@manW13-be
manW13-be force-pushed the fix/device-flow-client-id-token-refresh branch from 7b55319 to 34ae9cf Compare May 2, 2026 06:47
@wmalgadey

Copy link
Copy Markdown
Owner

@manW13-be willing to fix the CI/Lint and tests?

@gdepeute

Copy link
Copy Markdown

Probably related to Home Assistant issue: home-assistant/core#173222

@manW13-be

Copy link
Copy Markdown
Author

Hi @wmalgadey,

The CI/lint issue is now fixed — black was complaining about a brace style in _poll_device_activation in http.py. All 68 tests pass locally.

Could you take a look when you have a moment? Thanks!

@codecov

codecov Bot commented Jun 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 23.52941% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.71%. Comparing base (b8ad4ea) to head (77a00f1).
⚠️ Report is 1 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
PyTado/http.py 23.52% 13 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #314      +/-   ##
==========================================
- Coverage   76.11%   75.71%   -0.40%     
==========================================
  Files          37       37              
  Lines        2211     2224      +13     
  Branches      136      139       +3     
==========================================
+ Hits         1683     1684       +1     
- Misses        486      498      +12     
  Partials       42       42              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@wmalgadey

Copy link
Copy Markdown
Owner

Hi @wmalgadey,

The CI/lint issue is now fixed — black was complaining about a brace style in _poll_device_activation in http.py. All 68 tests pass locally.

Could you take a look when you have a moment? Thanks!

sorry to bother you again, but I already did merge changes related to the home id :/

Could you rebase your changes and add the variants again?

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes multiple OAuth2 device-flow and token-refresh issues when talking to login.tado.com, including correct form-encoded auth requests, a proper device-flow fallback when saved tokens are expired, and support for providing a per-instance client_id.

Changes:

  • Add client_id as an optional constructor parameter (threaded through TadoHttp) instead of relying on patching a module global.
  • Switch OAuth2 token/device-flow calls to application/x-www-form-urlencoded request bodies (not query params + empty JSON).
  • Improve device verification URL generation and token-expiration fallback behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
tests/test_http.py Updates the refresh-token request matcher to expect form-encoded body parameters.
PyTado/interface/interface.py Exposes optional client_id on the public Tado interface and passes it through to Http.
PyTado/http.py Implements client_id plumbing, device-flow fallback on refresh failure, form-encoded auth requests, and improved /me home-id extraction logic.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread PyTado/http.py Outdated
if response.get("homeIds"):
return int(response["homeIds"][0])

raise TadoException(f"Cannot extract home ID from /me response: {response}")
Comment thread PyTado/http.py Outdated
Comment on lines +600 to +604
token_response = self._session.request(
method="post",
url="https://login.tado.com/oauth2/token",
params={
"client_id": CLIENT_ID_DEVICE,
"device_code": self._device_flow_data["device_code"],
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
},
data=urlencode(
{
Comment thread PyTado/http.py
Comment on lines +569 to +573
visit_url = (
self._device_flow_data["verification_uri"]
+ "?"
+ urlencode({"user_code": self._user_code, "client_id": self._client_id})
)
manW13-be and others added 3 commits June 29, 2026 08:47
…lback

- Add `client_id` parameter to `Http.__init__` and `Tado.__init__`,
  defaulting to `CLIENT_ID_DEVICE`. Removes the need to monkey-patch
  the module global for custom client IDs.

- Fix verification URL to include `client_id` query param (required by
  login.tado.com — omitting it causes `missing_client_id` rejection).

- Fix `_refresh_token`, `_login_device_flow`, and device polling to use
  form-encoded body (`application/x-www-form-urlencoded`) instead of
  query params + empty JSON body. The Tado auth server requires a form
  body; the previous format silently failed.

- Add missing `else` branch in `__init__`: when a saved token exists but
  the refresh fails (expired), fall back to a new device flow instead of
  staying in `NOT_STARTED` indefinitely.
Token refresh now sends params as form-encoded body, not query string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@manW13-be
manW13-be force-pushed the fix/device-flow-client-id-token-refresh branch from 2bcafe3 to bfae767 Compare June 29, 2026 06:53
@manW13-be

Copy link
Copy Markdown
Author

Done — rebased on master (including #331). 69 tests pass, black is clean.

…device_ready() guard

1. _check_device_activation(): revert form-encoded body to query params.
   The Tado token endpoint requires params= for device_code polling —
   data=urlencode() returns authorization_pending regardless of auth state.

2. _get_id(): add home.id and homeIds[0] fallbacks after existing homes/homeId
   checks, and include the raw response in the TadoException message for
   easier debugging.

3. Http.__init__: wrap _device_ready() in try/except. If /me returns an
   empty body (e.g. Tado rate-limiting, 429) the constructor no longer
   crashes — it wipes the stale token and starts a fresh device flow.
   Note: _device_ready() in device_activation() (line 641) is intentionally
   left unwrapped so errors propagate to the caller.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@wmalgadey
wmalgadey merged commit f2a07f8 into wmalgadey:master Jun 29, 2026
8 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants