python-tado 0.18.6__tar.gz → 0.18.7__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.
Files changed (34) hide show
  1. {python_tado-0.18.6/python_tado.egg-info → python_tado-0.18.7}/PKG-INFO +1 -1
  2. {python_tado-0.18.6 → python_tado-0.18.7}/PyTado/__main__.py +11 -27
  3. {python_tado-0.18.6 → python_tado-0.18.7}/PyTado/const.py +4 -11
  4. python_tado-0.18.7/PyTado/http.py +441 -0
  5. {python_tado-0.18.6 → python_tado-0.18.7}/PyTado/interface/api/my_tado.py +14 -44
  6. {python_tado-0.18.6 → python_tado-0.18.7}/PyTado/interface/interface.py +37 -20
  7. {python_tado-0.18.6 → python_tado-0.18.7}/PyTado/zone/my_zone.py +30 -91
  8. {python_tado-0.18.6 → python_tado-0.18.7}/pyproject.toml +1 -1
  9. {python_tado-0.18.6 → python_tado-0.18.7/python_tado.egg-info}/PKG-INFO +1 -1
  10. {python_tado-0.18.6 → python_tado-0.18.7}/tests/test_hops_zone.py +8 -3
  11. {python_tado-0.18.6 → python_tado-0.18.7}/tests/test_http.py +45 -40
  12. {python_tado-0.18.6 → python_tado-0.18.7}/tests/test_my_tado.py +7 -4
  13. {python_tado-0.18.6 → python_tado-0.18.7}/tests/test_my_zone.py +8 -4
  14. python_tado-0.18.7/tests/test_tado_interface.py +81 -0
  15. python_tado-0.18.6/PyTado/http.py +0 -351
  16. python_tado-0.18.6/tests/test_tado_interface.py +0 -84
  17. {python_tado-0.18.6 → python_tado-0.18.7}/AUTHORS +0 -0
  18. {python_tado-0.18.6 → python_tado-0.18.7}/LICENSE +0 -0
  19. {python_tado-0.18.6 → python_tado-0.18.7}/PyTado/__init__.py +0 -0
  20. {python_tado-0.18.6 → python_tado-0.18.7}/PyTado/exceptions.py +0 -0
  21. {python_tado-0.18.6 → python_tado-0.18.7}/PyTado/interface/__init__.py +0 -0
  22. {python_tado-0.18.6 → python_tado-0.18.7}/PyTado/interface/api/__init__.py +0 -0
  23. {python_tado-0.18.6 → python_tado-0.18.7}/PyTado/interface/api/hops_tado.py +0 -0
  24. {python_tado-0.18.6 → python_tado-0.18.7}/PyTado/logger.py +0 -0
  25. {python_tado-0.18.6 → python_tado-0.18.7}/PyTado/zone/__init__.py +0 -0
  26. {python_tado-0.18.6 → python_tado-0.18.7}/PyTado/zone/hops_zone.py +0 -0
  27. {python_tado-0.18.6 → python_tado-0.18.7}/README.md +0 -0
  28. {python_tado-0.18.6 → python_tado-0.18.7}/python_tado.egg-info/SOURCES.txt +0 -0
  29. {python_tado-0.18.6 → python_tado-0.18.7}/python_tado.egg-info/dependency_links.txt +0 -0
  30. {python_tado-0.18.6 → python_tado-0.18.7}/python_tado.egg-info/entry_points.txt +0 -0
  31. {python_tado-0.18.6 → python_tado-0.18.7}/python_tado.egg-info/not-zip-safe +0 -0
  32. {python_tado-0.18.6 → python_tado-0.18.7}/python_tado.egg-info/requires.txt +0 -0
  33. {python_tado-0.18.6 → python_tado-0.18.7}/python_tado.egg-info/top_level.txt +0 -0
  34. {python_tado-0.18.6 → python_tado-0.18.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: python-tado
3
- Version: 0.18.6
3
+ Version: 0.18.7
4
4
  Summary: PyTado from chrism0dwk, modfied by w.malgadey, diplix, michaelarnauts, LenhartStephan, splifter, syssi, andersonshatch, Yippy, p0thi, Coffee2CodeNL, chiefdragon, FilBr, nikilase, albertomontesg, Moritz-Schmidt, palazzem
5
5
  Author-email: Chris Jewell <chrism0dwk@gmail.com>, "w.malgadey" <w.malgadey@gmail.com>, FilBr <filippo.barba@protonmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
 
4
3
  """Module for querying and controlling Tado smart thermostats."""
5
4
 
@@ -10,8 +9,9 @@ import sys
10
9
  from PyTado.interface import Tado
11
10
 
12
11
 
13
- def log_in(email, password):
14
- t = Tado(email, password)
12
+ def log_in(username, password):
13
+ t = Tado()
14
+ t.device_activation()
15
15
  return t
16
16
 
17
17
 
@@ -54,14 +54,10 @@ def main():
54
54
  required=True,
55
55
  help=("Tado username in the form of an email address."),
56
56
  )
57
- required_flags.add_argument(
58
- "--password", required=True, help="Tado password."
59
- )
57
+ required_flags.add_argument("--password", required=True, help="Tado password.")
60
58
 
61
59
  # Flags with default values go here.
62
- log_levels = dict(
63
- (logging.getLevelName(level), level) for level in [10, 20, 30, 40, 50]
64
- )
60
+ log_levels = {logging.getLevelName(level): level for level in [10, 20, 30, 40, 50]}
65
61
  parser.add_argument(
66
62
  "--loglevel",
67
63
  default="INFO",
@@ -71,30 +67,18 @@ def main():
71
67
 
72
68
  subparsers = parser.add_subparsers()
73
69
 
74
- show_config_parser = subparsers.add_parser(
75
- "get_me", help="Get home information."
76
- )
70
+ show_config_parser = subparsers.add_parser("get_me", help="Get home information.")
77
71
  show_config_parser.set_defaults(func=get_me)
78
72
 
79
- start_activity_parser = subparsers.add_parser(
80
- "get_state", help="Get state of zone."
81
- )
82
- start_activity_parser.add_argument(
83
- "--zone", help="Zone to get the state of."
84
- )
73
+ start_activity_parser = subparsers.add_parser("get_state", help="Get state of zone.")
74
+ start_activity_parser.add_argument("--zone", help="Zone to get the state of.")
85
75
  start_activity_parser.set_defaults(func=get_state)
86
76
 
87
- start_activity_parser = subparsers.add_parser(
88
- "get_states", help="Get states of all zones."
89
- )
77
+ start_activity_parser = subparsers.add_parser("get_states", help="Get states of all zones.")
90
78
  start_activity_parser.set_defaults(func=get_states)
91
79
 
92
- start_activity_parser = subparsers.add_parser(
93
- "get_capabilities", help="Get capabilities of zone."
94
- )
95
- start_activity_parser.add_argument(
96
- "--zone", help="Zone to get the capabilities of."
97
- )
80
+ start_activity_parser = subparsers.add_parser("get_capabilities", help="Get capabilities of zone.")
81
+ start_activity_parser.add_argument("--zone", help="Zone to get the capabilities of.")
98
82
  start_activity_parser.set_defaults(func=get_capabilities)
99
83
 
100
84
  args = parser.parse_args()
@@ -1,10 +1,7 @@
1
1
  """Constant values for the Tado component."""
2
2
 
3
- # Api credentials
4
- CLIENT_ID = "tado-web-app"
5
- CLIENT_SECRET = (
6
- "wZaRN7rpjn3FoNyF5IFuxg9uMzYJcvOoQ8QWiIqS3hfk6gLhVlG57j5YNoZL2Rtc"
7
- )
3
+ # API client ID
4
+ CLIENT_ID_DEVICE = "1bb50063-6b0c-4d11-bd99-387f4a91cc46" # nosec B105
8
5
 
9
6
  # Types
10
7
  TYPE_AIR_CONDITIONING = "AIR_CONDITIONING"
@@ -49,12 +46,8 @@ CONST_HORIZONTAL_SWING_MID_RIGHT = "MID_RIGHT"
49
46
  CONST_HORIZONTAL_SWING_RIGHT = "RIGHT"
50
47
 
51
48
  # When we change the temperature setting, we need an overlay mode
52
- CONST_OVERLAY_TADO_MODE = (
53
- "NEXT_TIME_BLOCK" # wait until tado changes the mode automatic
54
- )
55
- CONST_OVERLAY_MANUAL = (
56
- "MANUAL" # the user has changed the temperature or mode manually
57
- )
49
+ CONST_OVERLAY_TADO_MODE = "NEXT_TIME_BLOCK" # wait until tado changes the mode automatic
50
+ CONST_OVERLAY_MANUAL = "MANUAL" # the user has changed the temperature or mode manually
58
51
  CONST_OVERLAY_TIMER = "TIMER" # the temperature will be reset after a timespan
59
52
 
60
53
  # Heat always comes first since we get the
@@ -0,0 +1,441 @@
1
+ """
2
+ Do all the API HTTP heavy lifting in this file
3
+ """
4
+
5
+ import enum
6
+ import json
7
+ import logging
8
+ import pprint
9
+ import time
10
+ from datetime import datetime, timedelta
11
+ from typing import Any
12
+ from urllib.parse import urlencode
13
+
14
+ import requests
15
+
16
+ from PyTado.const import CLIENT_ID_DEVICE
17
+ from PyTado.exceptions import TadoException, TadoWrongCredentialsException
18
+ from PyTado.logger import Logger
19
+
20
+ _LOGGER = Logger(__name__)
21
+
22
+
23
+ class Endpoint(enum.StrEnum):
24
+ """Endpoint URL Enum"""
25
+
26
+ MY_API = "https://my.tado.com/api/v2/"
27
+ HOPS_API = "https://hops.tado.com/"
28
+ MOBILE = "https://my.tado.com/mobile/1.9/"
29
+ EIQ = "https://energy-insights.tado.com/api/"
30
+ TARIFF = "https://tariff-experience.tado.com/api/"
31
+ GENIE = "https://genie.tado.com/api/v2/"
32
+ MINDER = "https://minder.tado.com/v1/"
33
+
34
+
35
+ class Domain(enum.StrEnum):
36
+ """API Request Domain Enum"""
37
+
38
+ HOME = "homes"
39
+ DEVICES = "devices"
40
+ ME = "me"
41
+ HOME_BY_BRIDGE = "homeByBridge"
42
+
43
+
44
+ class Action(enum.StrEnum):
45
+ """API Request Action Enum"""
46
+
47
+ GET = "GET"
48
+ SET = "POST"
49
+ RESET = "DELETE"
50
+ CHANGE = "PUT"
51
+
52
+
53
+ class Mode(enum.Enum):
54
+ """API Response Format Enum"""
55
+
56
+ OBJECT = 1
57
+ PLAIN = 2
58
+
59
+
60
+ class DeviceActivationStatus(enum.StrEnum):
61
+ NOT_STARTED = "NOT_STARTED"
62
+ PENDING = "PENDING"
63
+ COMPLETED = "COMPLETED"
64
+
65
+
66
+ class TadoRequest:
67
+ """Data Container for my.tado.com API Requests"""
68
+
69
+ def __init__(
70
+ self,
71
+ endpoint: Endpoint = Endpoint.MY_API,
72
+ command: str | None = None,
73
+ action: Action | str = Action.GET,
74
+ payload: dict[str, Any] | None = None,
75
+ domain: Domain = Domain.HOME,
76
+ device: int | str | None = None,
77
+ mode: Mode = Mode.OBJECT,
78
+ params: dict[str, Any] | None = None,
79
+ ) -> None:
80
+ self.endpoint = endpoint
81
+ self.command = command
82
+ self.action = action
83
+ self.payload = payload
84
+ self.domain = domain
85
+ self.device = device
86
+ self.mode = mode
87
+ self.params = params
88
+
89
+
90
+ class TadoXRequest(TadoRequest):
91
+ """Data Container for hops.tado.com (Tado X) API Requests"""
92
+
93
+ def __init__(
94
+ self,
95
+ endpoint: Endpoint = Endpoint.HOPS_API,
96
+ command: str | None = None,
97
+ action: Action | str = Action.GET,
98
+ payload: dict[str, Any] | None = None,
99
+ domain: Domain = Domain.HOME,
100
+ device: int | str | None = None,
101
+ mode: Mode = Mode.OBJECT,
102
+ params: dict[str, Any] | None = None,
103
+ ) -> None:
104
+ super().__init__(
105
+ endpoint=endpoint,
106
+ command=command,
107
+ action=action,
108
+ payload=payload,
109
+ domain=domain,
110
+ device=device,
111
+ mode=mode,
112
+ params=params,
113
+ )
114
+ self._action = action
115
+
116
+ @property
117
+ def action(self) -> Action | str:
118
+ """Get request action for Tado X"""
119
+ if self._action == Action.CHANGE:
120
+ return "PATCH"
121
+ return self._action
122
+
123
+ @action.setter
124
+ def action(self, value: Action | str) -> None:
125
+ """Set request action"""
126
+ self._action = value
127
+
128
+
129
+ class TadoResponse:
130
+ """Unimplemented Response Container
131
+ todo: implement response parser"""
132
+
133
+ pass
134
+
135
+
136
+ _DEFAULT_TIMEOUT = 10
137
+ _DEFAULT_RETRIES = 5
138
+
139
+
140
+ class Http:
141
+ """API Request Class"""
142
+
143
+ def __init__(
144
+ self,
145
+ http_session: requests.Session | None = None,
146
+ debug: bool = False,
147
+ ) -> None:
148
+ if debug:
149
+ _LOGGER.setLevel(logging.DEBUG)
150
+ else:
151
+ _LOGGER.setLevel(logging.WARNING)
152
+
153
+ self._refresh_at = datetime.now() + timedelta(minutes=10)
154
+ self._session = http_session or requests.Session()
155
+ self._session.hooks["response"].append(self._log_response)
156
+ self._headers = {"Referer": "https://app.tado.com/"}
157
+
158
+ self._user_code: str | None = None
159
+ self._device_verification_url: str | None = None
160
+ self._device_activation_status = DeviceActivationStatus.NOT_STARTED
161
+ self._expires_at: datetime | None = None
162
+
163
+ self._id: int | None = None
164
+ self._token_refresh: str | None = None
165
+ self._x_api: bool | None = None
166
+ self._device_activation_status = self._login_device_flow()
167
+
168
+ @property
169
+ def is_x_line(self) -> bool | None:
170
+ return self._x_api
171
+
172
+ @property
173
+ def user_code(self) -> str | None:
174
+ return self._user_code
175
+
176
+ @property
177
+ def device_activation_status(self) -> DeviceActivationStatus:
178
+ return self._device_activation_status
179
+
180
+ @property
181
+ def device_verification_url(self) -> str | None:
182
+ return self._device_verification_url
183
+
184
+ def _log_response(self, response: requests.Response, *args, **kwargs) -> None:
185
+ og_request_method = response.request.method
186
+ og_request_url = response.request.url
187
+ og_request_headers = response.request.headers
188
+ response_status = response.status_code
189
+
190
+ if response.text is None or response.text == "":
191
+ response_data = {}
192
+ else:
193
+ response_data = response.json()
194
+
195
+ _LOGGER.debug(
196
+ f"\nRequest:\n\tMethod:{og_request_method}"
197
+ f"\n\tURL: {og_request_url}"
198
+ f"\n\tHeaders: {pprint.pformat(og_request_headers)}"
199
+ f"\nResponse:\n\tStatusCode: {response_status}"
200
+ f"\n\tData: {response_data}"
201
+ )
202
+
203
+ def request(self, request: TadoRequest) -> dict[str, Any]:
204
+ """Request something from the API with a TadoRequest"""
205
+ self._refresh_token()
206
+
207
+ headers = self._headers
208
+ data = self._configure_payload(headers, request)
209
+ url = self._configure_url(request)
210
+
211
+ http_request = requests.Request(method=request.action, url=url, headers=headers, data=data)
212
+ prepped = http_request.prepare()
213
+ prepped.hooks["response"].append(self._log_response)
214
+
215
+ retries = _DEFAULT_RETRIES
216
+
217
+ while retries >= 0:
218
+ try:
219
+ response = self._session.send(prepped)
220
+ break
221
+ except TadoWrongCredentialsException as e:
222
+ _LOGGER.error("Credentials Exception: %s", e)
223
+ raise e
224
+ except requests.exceptions.ConnectionError as e:
225
+ if retries > 0:
226
+ _LOGGER.warning("Connection error: %s", e)
227
+ self._session.close()
228
+ self._session = requests.Session()
229
+ self._session.hooks["response"].append(self._log_response)
230
+ retries -= 1
231
+ else:
232
+ _LOGGER.error(
233
+ "Connection failed after %d retries: %s",
234
+ _DEFAULT_RETRIES,
235
+ e,
236
+ )
237
+ raise TadoException(e) from e
238
+
239
+ if response.text is None or response.text == "":
240
+ return {}
241
+
242
+ return response.json()
243
+
244
+ def _configure_url(self, request: TadoRequest) -> str:
245
+ if request.endpoint == Endpoint.MOBILE:
246
+ url = f"{request.endpoint}{request.command}"
247
+ elif request.domain == Domain.DEVICES or request.domain == Domain.HOME_BY_BRIDGE:
248
+ url = f"{request.endpoint}{request.domain}/{request.device}/{request.command}"
249
+ elif request.domain == Domain.ME:
250
+ url = f"{request.endpoint}{request.domain}"
251
+ else:
252
+ url = f"{request.endpoint}{request.domain}/{self._id:d}/{request.command}"
253
+
254
+ if request.params is not None:
255
+ params = request.params
256
+ url += f"?{urlencode(params)}"
257
+
258
+ return url
259
+
260
+ def _configure_payload(self, headers: dict[str, str], request: TadoRequest) -> bytes:
261
+ if request.payload is None:
262
+ return b""
263
+
264
+ if request.mode == Mode.PLAIN:
265
+ headers["Content-Type"] = "text/plain;charset=UTF-8"
266
+ else:
267
+ headers["Content-Type"] = "application/json;charset=UTF-8"
268
+ headers["Mime-Type"] = "application/json;charset=UTF-8"
269
+ return json.dumps(request.payload).encode("utf8")
270
+
271
+ def _set_oauth_header(self, data: dict[str, Any]) -> str:
272
+ """Set the OAuth header and return the refresh token"""
273
+
274
+ access_token = data["access_token"]
275
+ expires_in = float(data["expires_in"])
276
+ refresh_token = data["refresh_token"]
277
+
278
+ self._token_refresh = refresh_token
279
+ self._refresh_at = datetime.now()
280
+ self._refresh_at = self._refresh_at + timedelta(seconds=expires_in)
281
+ # We subtract 30 seconds from the correct refresh time.
282
+ # Then we have a 30 seconds timespan to get a new refresh_token
283
+ self._refresh_at = self._refresh_at - timedelta(seconds=30)
284
+
285
+ self._headers["Authorization"] = f"Bearer {access_token}"
286
+ return refresh_token
287
+
288
+ def _refresh_token(self) -> None:
289
+ """Refresh the token if it is about to expire"""
290
+ if self._refresh_at >= datetime.now():
291
+ return
292
+
293
+ url = "https://login.tado.com/oauth2/token"
294
+ data = {
295
+ "client_id": CLIENT_ID_DEVICE,
296
+ "grant_type": "refresh_token",
297
+ "refresh_token": self._token_refresh,
298
+ }
299
+ self._session.close()
300
+ self._session = requests.Session()
301
+ self._session.hooks["response"].append(self._log_response)
302
+
303
+ try:
304
+ response = self._session.request(
305
+ "post",
306
+ url,
307
+ params=data,
308
+ timeout=_DEFAULT_TIMEOUT,
309
+ data=json.dumps({}).encode("utf8"),
310
+ headers={
311
+ "Content-Type": "application/json",
312
+ "Referer": "https://app.tado.com/",
313
+ },
314
+ )
315
+ except requests.exceptions.ConnectionError as e:
316
+ _LOGGER.error("Connection error: %s", e)
317
+ raise TadoException(e)
318
+
319
+ if response.status_code != 200:
320
+ raise TadoWrongCredentialsException(
321
+ "Failed to refresh token, probably wrong credentials. " f"Status code: {response.status_code}"
322
+ )
323
+
324
+ self._set_oauth_header(response.json())
325
+
326
+ def _login_device_flow(self) -> DeviceActivationStatus:
327
+ """Start the login to the API using the device flow"""
328
+
329
+ if self._device_activation_status != DeviceActivationStatus.NOT_STARTED:
330
+ raise TadoException("The device has been started already")
331
+
332
+ url = "https://login.tado.com/oauth2/device_authorize"
333
+ data = {
334
+ "client_id": CLIENT_ID_DEVICE,
335
+ "scope": "offline_access",
336
+ }
337
+
338
+ try:
339
+ response = self._session.request(
340
+ method="post",
341
+ url=url,
342
+ params=data,
343
+ timeout=_DEFAULT_TIMEOUT,
344
+ data=json.dumps({}).encode("utf8"),
345
+ headers={
346
+ "Content-Type": "application/json",
347
+ "Referer": "https://app.tado.com/",
348
+ },
349
+ )
350
+ except requests.exceptions.ConnectionError as e:
351
+ raise TadoException(e) from e
352
+
353
+ if response.status_code != 200:
354
+ raise TadoException(f"Login failed. Status code: {response.status_code} and reason: {response.reason}")
355
+
356
+ self._device_flow_data = response.json()
357
+ _LOGGER.debug("Device flow response: %s", self._device_flow_data)
358
+
359
+ user_code = urlencode({"user_code": self._device_flow_data["user_code"]})
360
+ visit_url = f"{self._device_flow_data['verification_uri']}?{user_code}"
361
+ self._user_code = self._device_flow_data["user_code"]
362
+ self._device_verification_url = visit_url
363
+
364
+ _LOGGER.info("Please visit the following URL: %s", visit_url)
365
+
366
+ expires_in_seconds = self._device_flow_data["expires_in"]
367
+ self._expires_at = datetime.now() + timedelta(seconds=expires_in_seconds)
368
+
369
+ _LOGGER.info(
370
+ "Waiting for user to authorize the device. Expires at %s",
371
+ self._expires_at.strftime("%Y-%m-%d %H:%M:%S"),
372
+ )
373
+
374
+ return DeviceActivationStatus.PENDING
375
+
376
+ def _check_device_activation(self) -> bool:
377
+ if self._expires_at is not None and datetime.timestamp(datetime.now()) > datetime.timestamp(self._expires_at):
378
+ raise TadoException("User took too long to enter key")
379
+
380
+ # Await the desired interval, before polling the API again
381
+ time.sleep(self._device_flow_data["interval"])
382
+
383
+ try:
384
+ token_response = self._session.request(
385
+ method="post",
386
+ url="https://login.tado.com/oauth2/token",
387
+ params={
388
+ "client_id": CLIENT_ID_DEVICE,
389
+ "device_code": self._device_flow_data["device_code"],
390
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
391
+ },
392
+ )
393
+ except requests.exceptions.ConnectionError as e:
394
+ raise TadoException(e) from e
395
+
396
+ if token_response.status_code == 200:
397
+ self._set_oauth_header(token_response.json())
398
+ return True
399
+
400
+ # The user has not yet authorized the device, let's continue
401
+ if token_response.status_code == 400 and token_response.json()["error"] == "authorization_pending":
402
+ _LOGGER.info("Authorization pending, waiting for user to authorize. Continue polling.")
403
+ return False
404
+
405
+ raise TadoException(f"Login failed. Reason: {token_response.reason}")
406
+
407
+ def device_activation(self) -> None:
408
+ """Activate the device and get the refresh token"""
409
+
410
+ if self._device_activation_status == DeviceActivationStatus.NOT_STARTED:
411
+ raise TadoException("The device flow has not yet started")
412
+
413
+ while True:
414
+ if self._check_device_activation():
415
+ break
416
+
417
+ self._id = self._get_id()
418
+ self._x_api = self._check_x_line_generation()
419
+ self._user_code = None
420
+ self._device_verification_url = None
421
+ self._device_activation_status = DeviceActivationStatus.COMPLETED
422
+
423
+ def _get_id(self) -> int:
424
+ request = TadoRequest()
425
+ request.action = Action.GET
426
+ request.domain = Domain.ME
427
+
428
+ homes_ = self.request(request)["homes"]
429
+
430
+ return homes_[0]["id"]
431
+
432
+ def _check_x_line_generation(self):
433
+ # get home info
434
+ request = TadoRequest()
435
+ request.action = Action.GET
436
+ request.domain = Domain.HOME
437
+ request.command = ""
438
+
439
+ home_ = self.request(request)
440
+
441
+ return "generation" in home_ and home_["generation"] == "LINE_X"
@@ -2,22 +2,14 @@
2
2
  PyTado interface implementation for app.tado.com.
3
3
  """
4
4
 
5
- import enum
6
5
  import datetime
6
+ import enum
7
7
  import logging
8
-
9
8
  from typing import Any
10
9
 
10
+ from ...exceptions import TadoException, TadoNotSupportedException
11
+ from ...http import Action, Domain, Endpoint, Http, Mode, TadoRequest
11
12
  from ...logger import Logger
12
- from ...exceptions import TadoNotSupportedException
13
- from ...http import (
14
- Action,
15
- Domain,
16
- Endpoint,
17
- Http,
18
- Mode,
19
- TadoRequest,
20
- )
21
13
  from ...zone import TadoZone
22
14
 
23
15
 
@@ -155,9 +147,7 @@ class Tado:
155
147
  # showSwitchToAutoGeofencingButton or currently enabled via the
156
148
  # presence of presenceLocked = False
157
149
  if "showSwitchToAutoGeofencingButton" in data:
158
- self._auto_geofencing_supported = data[
159
- "showSwitchToAutoGeofencingButton"
160
- ]
150
+ self._auto_geofencing_supported = data["showSwitchToAutoGeofencingButton"]
161
151
  elif "presenceLocked" in data:
162
152
  if not data["presenceLocked"]:
163
153
  self._auto_geofencing_supported = True
@@ -210,9 +200,7 @@ class Tado:
210
200
  data = self._http.request(request)
211
201
 
212
202
  if "id" not in data:
213
- raise TadoException(
214
- f'Returned data did not contain "id" : {str(data)}'
215
- )
203
+ raise TadoException(f'Returned data did not contain "id" : {str(data)}')
216
204
 
217
205
  return Timetable(data["id"])
218
206
 
@@ -224,14 +212,10 @@ class Tado:
224
212
  try:
225
213
  day = datetime.datetime.strptime(date, "%Y-%m-%d")
226
214
  except ValueError as err:
227
- raise ValueError(
228
- "Incorrect date format, should be YYYY-MM-DD"
229
- ) from err
215
+ raise ValueError("Incorrect date format, should be YYYY-MM-DD") from err
230
216
 
231
217
  request = TadoRequest()
232
- request.command = (
233
- f"zones/{zone:d}/dayReport?date={day.strftime('%Y-%m-%d')}"
234
- )
218
+ request.command = f"zones/{zone:d}/dayReport?date={day.strftime('%Y-%m-%d')}"
235
219
  return self._http.request(request)
236
220
 
237
221
  def set_timetable(self, zone: int, timetable: Timetable) -> None:
@@ -250,22 +234,16 @@ class Tado:
250
234
 
251
235
  self._http.request(request)
252
236
 
253
- def get_schedule(
254
- self, zone: int, timetable: Timetable, day=None
255
- ) -> dict[str, Any]:
237
+ def get_schedule(self, zone: int, timetable: Timetable, day=None) -> dict[str, Any]:
256
238
  """
257
239
  Get the JSON representation of the schedule for a zone.
258
240
  Zone has 3 different schedules, one for each timetable (see setTimetable)
259
241
  """
260
242
  request = TadoRequest()
261
243
  if day:
262
- request.command = (
263
- f"zones/{zone:d}/schedule/timetables/{timetable:d}/blocks/{day}"
264
- )
244
+ request.command = f"zones/{zone:d}/schedule/timetables/{timetable:d}/blocks/{day}"
265
245
  else:
266
- request.command = (
267
- f"zones/{zone:d}/schedule/timetables/{timetable:d}/blocks"
268
- )
246
+ request.command = f"zones/{zone:d}/schedule/timetables/{timetable:d}/blocks"
269
247
  request.mode = Mode.PLAIN
270
248
 
271
249
  return self._http.request(request)
@@ -276,9 +254,7 @@ class Tado:
276
254
  """
277
255
 
278
256
  request = TadoRequest()
279
- request.command = (
280
- f"zones/{zone:d}/schedule/timetables/{timetable:d}/blocks/{day}"
281
- )
257
+ request.command = f"zones/{zone:d}/schedule/timetables/{timetable:d}/blocks/{day}"
282
258
  request.action = Action.CHANGE
283
259
  request.payload = data
284
260
  request.mode = Mode.PLAIN
@@ -451,9 +427,7 @@ class Tado:
451
427
 
452
428
  return self._http.request(request)
453
429
  else:
454
- raise TadoNotSupportedException(
455
- "Auto mode is not known to be supported."
456
- )
430
+ raise TadoNotSupportedException("Auto mode is not known to be supported.")
457
431
 
458
432
  def get_window_state(self, zone):
459
433
  """
@@ -551,9 +525,7 @@ class Tado:
551
525
 
552
526
  return self._http.request(request)
553
527
 
554
- def set_eiq_meter_readings(
555
- self, date=datetime.datetime.now().strftime("%Y-%m-%d"), reading=0
556
- ):
528
+ def set_eiq_meter_readings(self, date=datetime.datetime.now().strftime("%Y-%m-%d"), reading=0):
557
529
  """
558
530
  Send Meter Readings to Tado, date format is YYYY-MM-DD, reading is without decimals
559
531
  """
@@ -636,9 +608,7 @@ class Tado:
636
608
 
637
609
  return self._http.request(request)
638
610
 
639
- def get_running_times(
640
- self, date=datetime.datetime.now().strftime("%Y-%m-%d")
641
- ) -> dict:
611
+ def get_running_times(self, date=datetime.datetime.now().strftime("%Y-%m-%d")) -> dict:
642
612
  """
643
613
  Get the running times from the Minder API
644
614
  """