python-tado 0.19.0__tar.gz → 0.19.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-tado
3
- Version: 0.19.0
3
+ Version: 0.19.1
4
4
  Summary: PyTado from chrism0dwk, modified by w.malgadey, diplix, michaelarnauts, LenhartStephan, splifter, syssi, andersonshatch, Yippy, p0thi, Coffee2CodeNL, chiefdragon, FilBr, nikilase, albertomontesg, Moritz-Schmidt, palazzem
5
5
  License: GPL-3.0-or-later
6
6
  Keywords: tado
@@ -21,7 +21,7 @@ Provides-Extra: all
21
21
  Provides-Extra: dev
22
22
  Provides-Extra: lint
23
23
  Provides-Extra: test
24
- Requires-Dist: black (>=24.3) ; extra == "lint" or extra == "all"
24
+ Requires-Dist: black (>=24.10) ; extra == "lint" or extra == "all"
25
25
  Requires-Dist: pre-commit ; extra == "dev" or extra == "all"
26
26
  Requires-Dist: pylint ; extra == "lint" or extra == "all"
27
27
  Requires-Dist: pytest ; extra == "test" or extra == "all"
@@ -69,6 +69,7 @@ the GNU Public Licence version 3, and should therefore not be used where it may
69
69
  cause discomfort and inconvenience to others.
70
70
 
71
71
  ## Usage
72
+
72
73
  As of the 15th of March 2025, Tado has updated their OAuth2 authentication flow. It will now use the device flow, instead of a username/password flow. This means that the user will have to authenticate the device using a browser, and then enter the code that is displayed on the browser into the terminal.
73
74
 
74
75
  PyTado handles this as following:
@@ -81,11 +82,45 @@ PyTado handles this as following:
81
82
  The `device_verification_url()` will be reset to `None` and the `device_activation_status()` will return `COMPLETED`.
82
83
 
83
84
  ### Screenshots of the device flow
85
+
84
86
  ![Tado device flow: invoking](/screenshots/tado-device-flow-0.png)
85
87
  ![Tado device flow: browser](/screenshots/tado-device-flow-1.png)
86
88
  ![Tado device flow: complete](/screenshots/tado-device-flow-2.png)
87
89
 
90
+ ### How to not authenticate the device again
91
+
92
+ It is possible to save the refresh token and reuse to skip the next login.
93
+
94
+ The following code will use the `refresh_token` file to save the refresh-token after login, and load the refresh-token if you create the Tado interface class again.
95
+
96
+ If the file doesn't exists, the webbrowser is started and the device authentication url is automatically opened. You can activate the device in the browser. When you restart the program, the refresh-token is reused and no webbrowser will be opened.
97
+
98
+ ```python
99
+ import webbrowser # only needed for direct web browser access
100
+
101
+ from PyTado.interface.interface import Tado
102
+
103
+ tado = Tado(token_file_path="/var/tado/refresh_token")
104
+
105
+ status = tado.device_activation_status()
106
+
107
+ if status == "PENDING":
108
+ url = tado.device_verification_url()
109
+
110
+ webbrowser.open_new_tab(url)
111
+
112
+ tado.device_activation()
113
+
114
+ status = tado.device_activation_status()
115
+
116
+ if status == "COMPLETED":
117
+ print("Login successful")
118
+ else:
119
+ print(f"Login status is {status}")
120
+ ```
121
+
88
122
  ## Example code
123
+
89
124
  ```python
90
125
  """Example client for PyTado"""
91
126
 
@@ -9,37 +9,78 @@ import sys
9
9
  from PyTado.interface import Tado
10
10
 
11
11
 
12
- def log_in(email, password):
13
- t = Tado(email, password)
12
+ def log_in(args):
13
+ """
14
+ Log in to the Tado API by activating the current device.
15
+
16
+ Add --token_file_path to the command line arguments to store the
17
+ refresh token in a file.
18
+
19
+ Args:
20
+ args (argparse.Namespace): The parsed command-line arguments.
21
+
22
+ Returns:
23
+ Tado: An instance of the Tado interface.
24
+ """
25
+ t = Tado(token_file_path=args.token_file_path)
26
+ t.device_activation()
14
27
  return t
15
28
 
16
29
 
17
30
  def get_me(args):
18
- t = Tado(args.email, args.password)
31
+ """
32
+ Retrieve and print home information from the Tado API.
33
+
34
+ Args:
35
+ args (argparse.Namespace): The parsed command-line arguments.
36
+ """
37
+ t = log_in(args)
19
38
  me = t.get_me()
20
39
  print(me)
21
40
 
22
41
 
23
42
  def get_state(args):
24
- t = Tado(args.email, args.password)
43
+ """
44
+ Retrieve and print the state of a specific zone from the Tado API.
45
+
46
+ Args:
47
+ args (argparse.Namespace): The parsed command-line arguments.
48
+ """
49
+ t = log_in(args)
25
50
  zone = t.get_state(int(args.zone))
26
51
  print(zone)
27
52
 
28
53
 
29
54
  def get_states(args):
30
- t = Tado(args.email, args.password)
31
- zone = t.get_zone_states()
32
- print(zone)
55
+ """
56
+ Retrieve and print the states of all zones from the Tado API.
57
+
58
+ Args:
59
+ args (argparse.Namespace): The parsed command-line arguments.
60
+ """
61
+ t = log_in(args)
62
+ zones = t.get_zone_states()
63
+ print(zones)
33
64
 
34
65
 
35
66
  def get_capabilities(args):
36
- t = Tado(args.email, args.password)
67
+ """
68
+ Retrieve and print the capabilities of a specific zone from the Tado API.
69
+
70
+ Args:
71
+ args (argparse.Namespace): The parsed command-line arguments.
72
+ """
73
+ t = log_in(args)
37
74
  capabilities = t.get_capabilities(int(args.zone))
38
75
  print(capabilities)
39
76
 
40
77
 
41
78
  def main():
42
- """Main method for the script."""
79
+ """
80
+ Main method for the script.
81
+
82
+ Sets up the argument parser, handles subcommands, and executes the appropriate function.
83
+ """
43
84
  parser = argparse.ArgumentParser(
44
85
  description="Pytado - Tado thermostat device control",
45
86
  formatter_class=argparse.ArgumentDefaultsHelpFormatter,
@@ -49,11 +90,10 @@ def main():
49
90
 
50
91
  # Required flags go here.
51
92
  required_flags.add_argument(
52
- "--email",
93
+ "--token_file_path",
53
94
  required=True,
54
- help=("Tado username in the form of an email address."),
95
+ help="Path to the file where the refresh token should be stored.",
55
96
  )
56
- required_flags.add_argument("--password", required=True, help="Tado password.")
57
97
 
58
98
  # Flags with default values go here.
59
99
  log_levels = {logging.getLevelName(level): level for level in [10, 20, 30, 40, 50]}
@@ -96,4 +96,4 @@ DEFAULT_TADO_PRECISION = 0.1
96
96
  DEFAULT_TADOX_PRECISION = 0.01
97
97
 
98
98
  HOME_DOMAIN = "homes"
99
- DEVICE_DOMAIN = "devices"
99
+ DEVICE_DOMAIN = "devices"
@@ -5,9 +5,13 @@ Do all the API HTTP heavy lifting in this file
5
5
  import enum
6
6
  import json
7
7
  import logging
8
+ import os
8
9
  import pprint
9
10
  import time
10
11
  from datetime import datetime, timedelta
12
+ from json import dump as json_dump
13
+ from json import load as json_load
14
+ from pathlib import Path
11
15
  from typing import Any
12
16
  from urllib.parse import urlencode
13
17
 
@@ -57,8 +61,9 @@ class Mode(enum.Enum):
57
61
  PLAIN = 2
58
62
 
59
63
 
60
- class DeviceActivationStatus(enum.Enum):
61
- """Device Activation Status Enum"""
64
+ class DeviceActivationStatus(enum.StrEnum):
65
+ """Device Activation Status Enum"""
66
+
62
67
  NOT_STARTED = "NOT_STARTED"
63
68
  PENDING = "PENDING"
64
69
  COMPLETED = "COMPLETED"
@@ -143,16 +148,34 @@ class Http:
143
148
 
144
149
  def __init__(
145
150
  self,
151
+ token_file_path: str | None = None,
152
+ saved_refresh_token: str | None = None,
146
153
  http_session: requests.Session | None = None,
147
154
  debug: bool = False,
148
155
  ) -> None:
156
+ """
157
+ Initialize the HTTP client for interacting with the Tado API.
158
+
159
+ Args:
160
+ token_file_path (str | None): Path to the file where the token is stored.
161
+ If None, the token will not be saved to a file.
162
+ saved_refresh_token (str | None): A previously saved refresh token to use for
163
+ authentication. If None, a new token will be requested.
164
+ http_session (requests.Session | None): An optional pre-configured HTTP session.
165
+ If None, a new session will be created.
166
+ debug (bool): If True, enables debug logging. Defaults to False.
167
+
168
+ Returns:
169
+ None
170
+ """
171
+
149
172
  if debug:
150
173
  _LOGGER.setLevel(logging.DEBUG)
151
174
  else:
152
175
  _LOGGER.setLevel(logging.WARNING)
153
176
 
154
177
  self._refresh_at = datetime.now() + timedelta(minutes=10)
155
- self._session = http_session or requests.Session()
178
+ self._session = http_session or self._create_session()
156
179
  self._session.hooks["response"].append(self._log_response)
157
180
  self._headers = {"Referer": "https://app.tado.com/"}
158
181
 
@@ -164,24 +187,71 @@ class Http:
164
187
  self._id: int | None = None
165
188
  self._token_refresh: str | None = None
166
189
  self._x_api: bool | None = None
167
- self._device_activation_status = self._login_device_flow()
190
+ self._token_file_path = token_file_path
191
+
192
+ if saved_refresh_token or self._load_token():
193
+ if self._refresh_token(refresh_token=saved_refresh_token, force_refresh=True):
194
+ self._device_ready()
195
+ else:
196
+ self._device_activation_status = self._login_device_flow()
168
197
 
169
198
  @property
170
199
  def is_x_line(self) -> bool | None:
200
+ """
201
+ Check if the current line is an X line.
202
+
203
+ Returns:
204
+ bool | None: True if the current line is an X line, False otherwise.
205
+ None if the api is not ready yet.
206
+ """
171
207
  return self._x_api
172
208
 
173
209
  @property
174
210
  def user_code(self) -> str | None:
211
+ """
212
+ Retrieve the user code.
213
+
214
+ Returns:
215
+ str | None: The user code if available, otherwise None.
216
+ """
175
217
  return self._user_code
176
218
 
177
219
  @property
178
220
  def device_activation_status(self) -> DeviceActivationStatus:
221
+ """
222
+ Retrieve the activation status of the device.
223
+
224
+ Returns:
225
+ DeviceActivationStatus: The current activation status of the device.
226
+ """
179
227
  return self._device_activation_status
180
228
 
181
229
  @property
182
230
  def device_verification_url(self) -> str | None:
231
+ """
232
+ Retrieve the url to activate the device.
233
+
234
+ Returns:
235
+ str | None: The current url for device activation or none if
236
+ authentication is not started.
237
+ """
183
238
  return self._device_verification_url
184
239
 
240
+ @property
241
+ def refresh_token(self) -> str | None:
242
+ """
243
+ Retrieve the current refresh token for the tado api connection.
244
+
245
+ Returns:
246
+ str | None: The current refresh token, or None if not available.
247
+ """
248
+ return self._token_refresh
249
+
250
+ def _create_session(self) -> requests.Session:
251
+ session = requests.Session()
252
+ session.hooks["response"].append(self._log_response)
253
+ return session
254
+
185
255
  def _log_response(self, response: requests.Response, *args, **kwargs) -> None:
186
256
  og_request_method = response.request.method
187
257
  og_request_url = response.request.url
@@ -226,8 +296,7 @@ class Http:
226
296
  if retries > 0:
227
297
  _LOGGER.warning("Connection error: %s", e)
228
298
  self._session.close()
229
- self._session = requests.Session()
230
- self._session.hooks["response"].append(self._log_response)
299
+ self._session = self._create_session()
231
300
  retries -= 1
232
301
  else:
233
302
  _LOGGER.error(
@@ -284,22 +353,60 @@ class Http:
284
353
  self._refresh_at = self._refresh_at - timedelta(seconds=30)
285
354
 
286
355
  self._headers["Authorization"] = f"Bearer {access_token}"
356
+
357
+ self._save_token()
358
+
287
359
  return refresh_token
288
360
 
289
- def _refresh_token(self) -> None:
290
- """Refresh the token if it is about to expire"""
291
- if self._refresh_at >= datetime.now():
292
- return
361
+ def _load_token(self) -> bool:
362
+ """Load the refresh token from a file."""
363
+
364
+ if not self._token_file_path or not os.path.exists(self._token_file_path):
365
+ return False
366
+
367
+ try:
368
+ with open(self._token_file_path, encoding="utf-8") as f:
369
+ data = json_load(f)
370
+ self._token_refresh = data.get("refresh_token")
371
+
372
+ _LOGGER.debug("Refresh token loaded from %s", self._token_file_path)
373
+
374
+ return True
375
+ except (OSError, json.JSONDecodeError) as e:
376
+ _LOGGER.error("Failed to load refresh token: %s", e)
377
+ raise TadoException(e) from e
378
+
379
+ def _refresh_token(self, refresh_token: str | None = None, force_refresh: bool = False) -> bool:
380
+ """
381
+ Refresh the OAuth token if it is about to expire or if forced.
382
+
383
+ Args:
384
+ refresh_token (str | None, optional): The refresh token to use for obtaining a new
385
+ access token.
386
+ force_refresh (bool, optional): If True, forces a token refresh regardless of
387
+ expiration. Defaults to False.
388
+
389
+ Returns:
390
+ bool: True if the token was successfully refreshed, False if the refresh failed due
391
+ to invalid credentials.
392
+
393
+ Raises:
394
+ TadoException: If a connection error occurs during the token refresh process.
395
+ TadoWrongCredentialsException: If the token refresh fails due to invalid credentials
396
+ and force_refresh is False.
397
+ """
398
+
399
+ if self._refresh_at >= datetime.now() and not force_refresh:
400
+ return True
293
401
 
294
402
  url = "https://login.tado.com/oauth2/token"
295
403
  data = {
296
404
  "client_id": CLIENT_ID_DEVICE,
297
405
  "grant_type": "refresh_token",
298
- "refresh_token": self._token_refresh,
406
+ "refresh_token": refresh_token or self._token_refresh,
299
407
  }
300
408
  self._session.close()
301
- self._session = requests.Session()
302
- self._session.hooks["response"].append(self._log_response)
409
+ self._session = self._create_session()
303
410
 
304
411
  try:
305
412
  response = self._session.request(
@@ -313,17 +420,49 @@ class Http:
313
420
  "Referer": "https://app.tado.com/",
314
421
  },
315
422
  )
423
+
316
424
  except requests.exceptions.ConnectionError as e:
317
425
  _LOGGER.error("Connection error: %s", e)
318
- raise TadoException(e)
426
+ raise TadoException(e) from e
319
427
 
320
428
  if response.status_code != 200:
429
+ if force_refresh:
430
+ _LOGGER.error(
431
+ "Failed to refresh token, probably wrong credentials. Status code: %s",
432
+ response.status_code,
433
+ )
434
+ return False
435
+
321
436
  raise TadoWrongCredentialsException(
322
- "Failed to refresh token, probably wrong credentials. " f"Status code: {response.status_code}"
437
+ "Failed to refresh token, probably wrong credentials. "
438
+ f"Status code: {response.status_code}"
323
439
  )
324
440
 
325
441
  self._set_oauth_header(response.json())
326
442
 
443
+ return True
444
+
445
+ def _save_token(self):
446
+ """Save the refresh token to a file."""
447
+ if not self._token_file_path or not self._token_refresh:
448
+ return
449
+
450
+ try:
451
+ token_dir = os.path.dirname(self._token_file_path)
452
+ if token_dir and not os.path.exists(token_dir):
453
+ Path(token_dir).mkdir(parents=True, exist_ok=True)
454
+
455
+ with open(self._token_file_path, "w", encoding="utf-8") as f:
456
+ json_dump(
457
+ {"refresh_token": self._token_refresh},
458
+ f,
459
+ )
460
+
461
+ _LOGGER.debug("Refresh token saved to %s", self._token_file_path)
462
+ except Exception as e:
463
+ _LOGGER.error("Failed to save refresh token: %s", e)
464
+ raise TadoException(e) from e
465
+
327
466
  def _login_device_flow(self) -> DeviceActivationStatus:
328
467
  """Start the login to the API using the device flow"""
329
468
 
@@ -352,7 +491,9 @@ class Http:
352
491
  raise TadoException(e) from e
353
492
 
354
493
  if response.status_code != 200:
355
- raise TadoException(f"Login failed. Status code: {response.status_code} and reason: {response.reason}")
494
+ raise TadoException(
495
+ f"Login failed. Status code: {response.status_code} and reason: {response.reason}"
496
+ )
356
497
 
357
498
  self._device_flow_data = response.json()
358
499
  _LOGGER.debug("Device flow response: %s", self._device_flow_data)
@@ -375,7 +516,9 @@ class Http:
375
516
  return DeviceActivationStatus.PENDING
376
517
 
377
518
  def _check_device_activation(self) -> bool:
378
- if self._expires_at is not None and datetime.timestamp(datetime.now()) > datetime.timestamp(self._expires_at):
519
+ if self._expires_at is not None and datetime.timestamp(datetime.now()) > datetime.timestamp(
520
+ self._expires_at
521
+ ):
379
522
  raise TadoException("User took too long to enter key")
380
523
 
381
524
  # Await the desired interval, before polling the API again
@@ -399,7 +542,10 @@ class Http:
399
542
  return True
400
543
 
401
544
  # The user has not yet authorized the device, let's continue
402
- if token_response.status_code == 400 and token_response.json()["error"] == "authorization_pending":
545
+ if (
546
+ token_response.status_code == 400
547
+ and token_response.json()["error"] == "authorization_pending"
548
+ ):
403
549
  _LOGGER.info("Authorization pending, waiting for user to authorize. Continue polling.")
404
550
  return False
405
551
 
@@ -415,6 +561,10 @@ class Http:
415
561
  if self._check_device_activation():
416
562
  break
417
563
 
564
+ self._device_ready()
565
+
566
+ def _device_ready(self):
567
+ """after device refresh code has been obtained"""
418
568
  self._id = self._get_id()
419
569
  self._x_api = self._check_x_line_generation()
420
570
  self._user_code = None
@@ -439,4 +589,4 @@ class Http:
439
589
 
440
590
  home_ = self.request(request)
441
591
 
442
- return "generation" in home_ and home_["generation"] == "LINE_X"
592
+ return "generation" in home_ and home_["generation"] == "LINE_X"
@@ -30,7 +30,8 @@ _LOGGER = Logger(__name__)
30
30
  class TadoX(Tado):
31
31
  """Interacts with a Tado thermostat via hops.tado.com (Tado X) API.
32
32
 
33
- Example usage: http = Http('me@somewhere.com', 'mypasswd')
33
+ Example usage: http = Http()
34
+ http.device_activation() # Activate the device
34
35
  t = TadoX(http)
35
36
  t.get_climate(1) # Get climate, room 1.
36
37
  """
@@ -34,7 +34,8 @@ _LOGGER = Logger(__name__)
34
34
  class Tado:
35
35
  """Interacts with a Tado thermostat via public my.tado.com API.
36
36
 
37
- Example usage: http = Http('me@somewhere.com', 'mypasswd')
37
+ Example usage: http = Http()
38
+ http.device_activation() # Activate the device
38
39
  t = Tado(http)
39
40
  t.get_climate(1) # Get climate, zone 1.
40
41
  """
@@ -619,4 +620,4 @@ class Tado:
619
620
  request.endpoint = Endpoint.MINDER
620
621
  request.params = {"from": date}
621
622
 
622
- return self._http.request(request)
623
+ return self._http.request(request)
@@ -6,12 +6,25 @@ import datetime
6
6
  import functools
7
7
  import warnings
8
8
 
9
+ import requests
10
+
9
11
  import PyTado.interface.api as API
10
12
  from PyTado.exceptions import TadoException
11
13
  from PyTado.http import DeviceActivationStatus, Http
12
14
 
13
15
 
14
16
  def deprecated(new_func_name):
17
+ """
18
+ A decorator to mark functions as deprecated. It will result in a warning being emitted
19
+ when the function is used, advising the user to use the new function instead.
20
+
21
+ Args:
22
+ new_func_name (str): The name of the new function that should be used instead.
23
+
24
+ Returns:
25
+ function: A decorator that wraps the deprecated function and emits a warning.
26
+ """
27
+
15
28
  def decorator(func):
16
29
  @functools.wraps(func)
17
30
  def wrapper(*args, **kwargs):
@@ -31,18 +44,34 @@ def deprecated(new_func_name):
31
44
  class Tado:
32
45
  """Interacts with a Tado thermostat via public API.
33
46
 
34
- Example usage: t = Tado('me@somewhere.com', 'mypasswd')
47
+ Example usage: t = Tado()
48
+ t.device_activation() # Activate device
35
49
  t.get_climate(1) # Get climate, zone 1.
36
50
  """
37
51
 
38
52
  def __init__(
39
53
  self,
40
- http_session=None,
54
+ token_file_path: str | None = None,
55
+ saved_refresh_token: str | None = None,
56
+ http_session: requests.Session | None = None,
41
57
  debug: bool = False,
42
58
  ):
43
- """Class Constructor"""
59
+ """
60
+ Initializes the interface class.
61
+
62
+ Args:
63
+ token_file_path (str | None, optional): Path to a file which will be used to persist
64
+ the refresh_token token. Defaults to None.
65
+ saved_refresh_token (str | None, optional): A previously saved refresh token.
66
+ Defaults to None.
67
+ http_session (requests.Session | None, optional): An optional HTTP session to use for
68
+ requests (can be used in unit tests). Defaults to None.
69
+ debug (bool, optional): Flag to enable or disable debug mode. Defaults to False.
70
+ """
44
71
 
45
72
  self._http = Http(
73
+ token_file_path=token_file_path,
74
+ saved_refresh_token=saved_refresh_token,
46
75
  http_session=http_session,
47
76
  debug=debug,
48
77
  )
@@ -50,16 +79,12 @@ class Tado:
50
79
  self._debug = debug
51
80
 
52
81
  def __getattr__(self, name):
53
- """Delegiert den Aufruf von Methoden an die richtige API-Client-Implementierung."""
82
+ """Delegate the called method to api implementation (hops_tado.py or my_tado.py)."""
54
83
 
55
- if self._api is None:
56
- raise TadoException("API is not initialized. Please complete device authentication first.")
84
+ self._ensure_api_initialized()
57
85
 
58
86
  return getattr(self._api, name)
59
87
 
60
- # region Deprecated Methods
61
- # pylint: disable=invalid-name
62
-
63
88
  def device_verification_url(self) -> str | None:
64
89
  """Returns the URL for device verification."""
65
90
  return self._http.device_verification_url
@@ -71,12 +96,32 @@ class Tado:
71
96
  def device_activation(self) -> None:
72
97
  """Activates the device."""
73
98
  self._http.device_activation()
99
+ self._ensure_api_initialized()
100
+
101
+ def get_refresh_token(self) -> str | None:
102
+ """
103
+ Retrieve the refresh token from the current api connection.
104
+
105
+ Returns:
106
+ str | None: The current refresh token, or None if not available.
107
+ """
108
+ return self._http.refresh_token
74
109
 
75
- if self._http.device_activation_status == DeviceActivationStatus.COMPLETED:
76
- if self._http.is_x_line:
77
- self._api = API.TadoX(http=self._http, debug=self._debug)
110
+ def _ensure_api_initialized(self):
111
+ """Ensures the API client is initialized."""
112
+ if self._api is None:
113
+ if self._http.device_activation_status == DeviceActivationStatus.COMPLETED:
114
+ if self._http.is_x_line:
115
+ self._api = API.TadoX(http=self._http, debug=self._debug)
116
+ else:
117
+ self._api = API.Tado(http=self._http, debug=self._debug)
78
118
  else:
79
- self._api = API.Tado(http=self._http, debug=self._debug)
119
+ raise TadoException(
120
+ "API is not initialized. Please complete device authentication first."
121
+ )
122
+
123
+ # region Deprecated Methods
124
+ # pylint: disable=invalid-name
80
125
 
81
126
  @deprecated("get_me")
82
127
  def getMe(self):
@@ -317,4 +362,4 @@ class Tado:
317
362
  )
318
363
 
319
364
  # pylint: enable=invalid-name
320
- # endregion
365
+ # endregion
@@ -30,6 +30,7 @@ the GNU Public Licence version 3, and should therefore not be used where it may
30
30
  cause discomfort and inconvenience to others.
31
31
 
32
32
  ## Usage
33
+
33
34
  As of the 15th of March 2025, Tado has updated their OAuth2 authentication flow. It will now use the device flow, instead of a username/password flow. This means that the user will have to authenticate the device using a browser, and then enter the code that is displayed on the browser into the terminal.
34
35
 
35
36
  PyTado handles this as following:
@@ -42,11 +43,45 @@ PyTado handles this as following:
42
43
  The `device_verification_url()` will be reset to `None` and the `device_activation_status()` will return `COMPLETED`.
43
44
 
44
45
  ### Screenshots of the device flow
46
+
45
47
  ![Tado device flow: invoking](/screenshots/tado-device-flow-0.png)
46
48
  ![Tado device flow: browser](/screenshots/tado-device-flow-1.png)
47
49
  ![Tado device flow: complete](/screenshots/tado-device-flow-2.png)
48
50
 
51
+ ### How to not authenticate the device again
52
+
53
+ It is possible to save the refresh token and reuse to skip the next login.
54
+
55
+ The following code will use the `refresh_token` file to save the refresh-token after login, and load the refresh-token if you create the Tado interface class again.
56
+
57
+ If the file doesn't exists, the webbrowser is started and the device authentication url is automatically opened. You can activate the device in the browser. When you restart the program, the refresh-token is reused and no webbrowser will be opened.
58
+
59
+ ```python
60
+ import webbrowser # only needed for direct web browser access
61
+
62
+ from PyTado.interface.interface import Tado
63
+
64
+ tado = Tado(token_file_path="/var/tado/refresh_token")
65
+
66
+ status = tado.device_activation_status()
67
+
68
+ if status == "PENDING":
69
+ url = tado.device_verification_url()
70
+
71
+ webbrowser.open_new_tab(url)
72
+
73
+ tado.device_activation()
74
+
75
+ status = tado.device_activation_status()
76
+
77
+ if status == "COMPLETED":
78
+ print("Login successful")
79
+ else:
80
+ print(f"Login status is {status}")
81
+ ```
82
+
49
83
  ## Example code
84
+
50
85
  ```python
51
86
  """Example client for PyTado"""
52
87
 
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "python-tado"
7
- version = "0.19.0"
7
+ version = "0.19.1"
8
8
  description = "PyTado from chrism0dwk, modified by w.malgadey, diplix, michaelarnauts, LenhartStephan, splifter, syssi, andersonshatch, Yippy, p0thi, Coffee2CodeNL, chiefdragon, FilBr, nikilase, albertomontesg, Moritz-Schmidt, palazzem"
9
9
  authors = [
10
10
  "Chris Jewell <chrism0dwk@gmail.com>",
@@ -32,7 +32,7 @@ repository = "https://github.com/wmalgadey/PyTado"
32
32
  [tool.poetry.dependencies]
33
33
  python = ">=3.11"
34
34
  requests = "*"
35
- black = ">=24.3"
35
+ black = ">=24.10"
36
36
  pylint = "*"
37
37
  pre-commit = "*"
38
38
  pytype = "*"
@@ -66,7 +66,7 @@ source = ["PyTado"]
66
66
  [tool.black]
67
67
  line-length = 100
68
68
  target-version = ["py311"]
69
- required-version = "24.3.0"
69
+ required-version = "24.10.0"
70
70
 
71
71
  [tool.bandit]
72
72
  exclude_dirs = ["tests"]
File without changes
File without changes