python-tado 0.18.13__tar.gz → 0.18.15__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 (33) hide show
  1. {python_tado-0.18.13/python_tado.egg-info → python_tado-0.18.15}/PKG-INFO +1 -1
  2. python_tado-0.18.15/PyTado/__init__.py +6 -0
  3. {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/http.py +47 -39
  4. {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/interface/api/my_tado.py +19 -0
  5. {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/interface/interface.py +4 -0
  6. {python_tado-0.18.13 → python_tado-0.18.15}/pyproject.toml +1 -1
  7. {python_tado-0.18.13 → python_tado-0.18.15/python_tado.egg-info}/PKG-INFO +1 -1
  8. {python_tado-0.18.13 → python_tado-0.18.15}/tests/test_http.py +21 -0
  9. {python_tado-0.18.13 → python_tado-0.18.15}/tests/test_my_tado.py +49 -0
  10. python_tado-0.18.13/PyTado/__init__.py +0 -0
  11. {python_tado-0.18.13 → python_tado-0.18.15}/AUTHORS +0 -0
  12. {python_tado-0.18.13 → python_tado-0.18.15}/LICENSE +0 -0
  13. {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/__main__.py +0 -0
  14. {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/const.py +0 -0
  15. {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/exceptions.py +0 -0
  16. {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/interface/__init__.py +0 -0
  17. {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/interface/api/__init__.py +0 -0
  18. {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/interface/api/hops_tado.py +0 -0
  19. {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/logger.py +0 -0
  20. {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/zone/__init__.py +0 -0
  21. {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/zone/hops_zone.py +0 -0
  22. {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/zone/my_zone.py +0 -0
  23. {python_tado-0.18.13 → python_tado-0.18.15}/README.md +0 -0
  24. {python_tado-0.18.13 → python_tado-0.18.15}/python_tado.egg-info/SOURCES.txt +0 -0
  25. {python_tado-0.18.13 → python_tado-0.18.15}/python_tado.egg-info/dependency_links.txt +0 -0
  26. {python_tado-0.18.13 → python_tado-0.18.15}/python_tado.egg-info/entry_points.txt +0 -0
  27. {python_tado-0.18.13 → python_tado-0.18.15}/python_tado.egg-info/not-zip-safe +0 -0
  28. {python_tado-0.18.13 → python_tado-0.18.15}/python_tado.egg-info/requires.txt +0 -0
  29. {python_tado-0.18.13 → python_tado-0.18.15}/python_tado.egg-info/top_level.txt +0 -0
  30. {python_tado-0.18.13 → python_tado-0.18.15}/setup.cfg +0 -0
  31. {python_tado-0.18.13 → python_tado-0.18.15}/tests/test_hops_zone.py +0 -0
  32. {python_tado-0.18.13 → python_tado-0.18.15}/tests/test_my_zone.py +0 -0
  33. {python_tado-0.18.13 → python_tado-0.18.15}/tests/test_tado_interface.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-tado
3
- Version: 0.18.13
3
+ Version: 0.18.15
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
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("python-tado")
5
+ except PackageNotFoundError:
6
+ __version__ = "test" # happens when running as pre-commit hook
@@ -7,16 +7,19 @@ import json
7
7
  import logging
8
8
  import os
9
9
  import pprint
10
+ import threading
10
11
  import time
11
12
  from datetime import datetime, timedelta, timezone
12
13
  from json import dump as json_dump
13
14
  from json import load as json_load
14
15
  from pathlib import Path
16
+ from threading import Lock
15
17
  from typing import Any
16
18
  from urllib.parse import urlencode
17
19
 
18
20
  import requests
19
21
 
22
+ from PyTado import __version__
20
23
  from PyTado.const import CLIENT_ID_DEVICE
21
24
  from PyTado.exceptions import TadoException, TadoWrongCredentialsException
22
25
  from PyTado.logger import Logger
@@ -146,12 +149,15 @@ _DEFAULT_RETRIES = 5
146
149
  class Http:
147
150
  """API Request Class"""
148
151
 
152
+ _lock: threading.Lock = Lock()
153
+
149
154
  def __init__(
150
155
  self,
151
156
  token_file_path: str | None = None,
152
157
  saved_refresh_token: str | None = None,
153
158
  http_session: requests.Session | None = None,
154
159
  debug: bool = False,
160
+ user_agent: str | None = None,
155
161
  ) -> None:
156
162
  """
157
163
  Initialize the HTTP client for interacting with the Tado API.
@@ -164,6 +170,8 @@ class Http:
164
170
  http_session (requests.Session | None): An optional pre-configured HTTP session.
165
171
  If None, a new session will be created.
166
172
  debug (bool): If True, enables debug logging. Defaults to False.
173
+ user_agent (str | None): Optional user-agent header to use for the HTTP requests.
174
+ If None, a default user-agent PyTado/<PyTado-version> will be used.
167
175
 
168
176
  Returns:
169
177
  None
@@ -177,7 +185,7 @@ class Http:
177
185
  self._refresh_at = datetime.now(timezone.utc) + timedelta(minutes=10)
178
186
  self._session = http_session or self._create_session()
179
187
  self._session.hooks["response"].append(self._log_response)
180
- self._headers = {"Referer": "https://app.tado.com/"}
188
+ self._headers = {"Referer": "https://app.tado.com/", "user-agent": user_agent or f"PyTado/{__version__}"}
181
189
 
182
190
  self._user_code: str | None = None
183
191
  self._device_verification_url: str | None = None
@@ -396,51 +404,51 @@ class Http:
396
404
  TadoWrongCredentialsException: If the token refresh fails due to invalid credentials
397
405
  and force_refresh is False.
398
406
  """
407
+ with self._lock:
408
+ if self._refresh_at >= datetime.now(timezone.utc) and not force_refresh:
409
+ return True
410
+
411
+ url = "https://login.tado.com/oauth2/token"
412
+ data = {
413
+ "client_id": CLIENT_ID_DEVICE,
414
+ "grant_type": "refresh_token",
415
+ "refresh_token": refresh_token or self._token_refresh,
416
+ }
417
+ self._session.close()
418
+ self._session = self._create_session()
399
419
 
400
- if self._refresh_at >= datetime.now(timezone.utc) and not force_refresh:
401
- return True
402
-
403
- url = "https://login.tado.com/oauth2/token"
404
- data = {
405
- "client_id": CLIENT_ID_DEVICE,
406
- "grant_type": "refresh_token",
407
- "refresh_token": refresh_token or self._token_refresh,
408
- }
409
- self._session.close()
410
- self._session = self._create_session()
420
+ try:
421
+ response = self._session.request(
422
+ "post",
423
+ url,
424
+ params=data,
425
+ timeout=_DEFAULT_TIMEOUT,
426
+ data=json.dumps({}).encode("utf8"),
427
+ headers={
428
+ "Content-Type": "application/json",
429
+ "Referer": "https://app.tado.com/",
430
+ },
431
+ )
411
432
 
412
- try:
413
- response = self._session.request(
414
- "post",
415
- url,
416
- params=data,
417
- timeout=_DEFAULT_TIMEOUT,
418
- data=json.dumps({}).encode("utf8"),
419
- headers={
420
- "Content-Type": "application/json",
421
- "Referer": "https://app.tado.com/",
422
- },
423
- )
433
+ except requests.exceptions.ConnectionError as e:
434
+ _LOGGER.error("Connection error: %s", e)
435
+ raise TadoException(e) from e
424
436
 
425
- except requests.exceptions.ConnectionError as e:
426
- _LOGGER.error("Connection error: %s", e)
427
- raise TadoException(e) from e
437
+ if response.status_code != 200:
438
+ if force_refresh:
439
+ _LOGGER.error(
440
+ "Failed to refresh token, probably wrong credentials. Status code: %s",
441
+ response.status_code,
442
+ )
443
+ return False
428
444
 
429
- if response.status_code != 200:
430
- if force_refresh:
431
- _LOGGER.error(
432
- "Failed to refresh token, probably wrong credentials. Status code: %s",
433
- response.status_code,
445
+ raise TadoWrongCredentialsException(
446
+ f"Failed to refresh token, probably wrong credentials. Status code: {response.status_code}"
434
447
  )
435
- return False
436
448
 
437
- raise TadoWrongCredentialsException(
438
- f"Failed to refresh token, probably wrong credentials. Status code: {response.status_code}"
439
- )
440
-
441
- self._set_oauth_header(response.json())
449
+ self._set_oauth_header(response.json())
442
450
 
443
- return True
451
+ return True
444
452
 
445
453
  def _save_token(self):
446
454
  """Save the refresh token to a file."""
@@ -526,6 +526,25 @@ class Tado:
526
526
 
527
527
  return self._http.request(request)
528
528
 
529
+ def get_eiq_consumption_overview(self, date=datetime.datetime.now().strftime("%Y-%m")):
530
+ """
531
+ Get consumption overview data for a specific month
532
+
533
+ Args:
534
+ date (str): The year-month to get the consumption overview for.
535
+
536
+ Returns:
537
+ dict: Consumption overview data for the specified month
538
+ """
539
+
540
+ request = TadoRequest()
541
+ request.command = "consumptionOverview"
542
+ request.action = Action.GET
543
+ request.endpoint = Endpoint.EIQ
544
+ request.params = {"month": f"{date}"}
545
+
546
+ return self._http.request(request)
547
+
529
548
  def set_eiq_meter_readings(self, date=datetime.datetime.now().strftime("%Y-%m-%d"), reading=0):
530
549
  """
531
550
  Send Meter Readings to Tado, date format is YYYY-MM-DD, reading is without decimals
@@ -55,6 +55,7 @@ class Tado:
55
55
  saved_refresh_token: str | None = None,
56
56
  http_session: requests.Session | None = None,
57
57
  debug: bool = False,
58
+ user_agent: str | None = None,
58
59
  ):
59
60
  """
60
61
  Initializes the interface class.
@@ -68,6 +69,8 @@ class Tado:
68
69
  requests (can be used in unit tests).
69
70
  Defaults to None.
70
71
  debug (bool, optional): Flag to enable or disable debug mode. Defaults to False.
72
+ user_agent (str | None): Optional user-agent header to use for the HTTP requests.
73
+ If None, a default user-agent PyTado/<PyTado-version> will be used.
71
74
  """
72
75
 
73
76
  self._http = Http(
@@ -75,6 +78,7 @@ class Tado:
75
78
  saved_refresh_token=saved_refresh_token,
76
79
  http_session=http_session,
77
80
  debug=debug,
81
+ user_agent=user_agent,
78
82
  )
79
83
  self._api: API.Tado | API.TadoX | None = None
80
84
  self._debug = debug
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-tado"
7
- version = "0.18.13"
7
+ version = "0.18.15"
8
8
  description = "PyTado from chrism0dwk, modfied by w.malgadey, diplix, michaelarnauts, LenhartStephan, splifter, syssi, andersonshatch, Yippy, p0thi, Coffee2CodeNL, chiefdragon, FilBr, nikilase, albertomontesg, Moritz-Schmidt, palazzem"
9
9
  authors = [
10
10
  { name = "Chris Jewell", email = "chrism0dwk@gmail.com" },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-tado
3
- Version: 0.18.13
3
+ Version: 0.18.15
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
@@ -124,6 +124,27 @@ class TestHttp(unittest.TestCase):
124
124
  self.assertEqual(instance._id, 1234)
125
125
  self.assertEqual(instance.is_x_line, True)
126
126
 
127
+ @responses.activate
128
+ def test_user_agent(self):
129
+ """Test that the we set the correct user-agent."""
130
+ responses.replace(
131
+ responses.GET,
132
+ "https://my.tado.com/api/v2/homes/1234/",
133
+ json=json.loads(
134
+ common.load_fixture(
135
+ "home_1234/tadov2.my_api_v2_home_state.json"
136
+ )
137
+ ),
138
+ match=[matchers.header_matcher({"user-agent": "MyCustomAgent/1.0"})],
139
+ status=200,
140
+ )
141
+
142
+ instance = Http(debug=True, user_agent="MyCustomAgent/1.0")
143
+ instance.device_activation()
144
+
145
+ # Verify that the login was successful
146
+ self.assertEqual(instance._id, 1234)
147
+
127
148
  @responses.activate
128
149
  def test_refresh_token_success(self):
129
150
  """Test that the refresh token is successfully updated."""
@@ -30,6 +30,7 @@ class TadoTestCase(unittest.TestCase):
30
30
  self.http = Http()
31
31
  self.tado_client = Tado(self.http)
32
32
 
33
+
33
34
  def test_home_set_to_manual_mode(
34
35
  self,
35
36
  ):
@@ -95,3 +96,51 @@ class TadoTestCase(unittest.TestCase):
95
96
  assert self.tado_client._http.request.called
96
97
  assert running_times["lastUpdated"] == "2023-08-05T19:50:21Z"
97
98
  assert running_times["runningTimes"][0]["zones"][0]["id"] == 1
99
+
100
+ def get_eiq_consumption_overview(self):
101
+ """Test the get_eiq_consumption_overview method."""
102
+
103
+ with mock.patch(
104
+ "PyTado.http.Http.request",
105
+ return_value=json.loads(common.load_fixture("consumption_overview.json")),
106
+ ):
107
+ #consumption = self.tado_client.get_eiq_consumption_overview("2024", "03", "HUN")
108
+ consumption = self.tado_client.get_eiq_consumption_overview("2024-03")
109
+
110
+ # Verify API call was made
111
+ assert self.tado_client._http.request.called
112
+
113
+ # Verify summary data
114
+ assert consumption["summary"]["consumption"] == 10.575
115
+ assert consumption["summary"]["unit"] == "m3"
116
+ assert consumption["summary"]["tariff"]["unitPriceInCents"] == 9.18
117
+
118
+ # Verify monthly data
119
+ monthly = consumption["graphConsumption"]["monthlyAggregation"][
120
+ "requestedMonth"
121
+ ]
122
+ assert monthly["startDate"] == "2025-04-01"
123
+ assert monthly["endDate"] == "2025-04-10"
124
+ assert monthly["totalConsumption"] == 10.575
125
+ assert monthly["totalCostInCents"] == 1024.18
126
+
127
+ # Verify consumption comparison
128
+ comparison = consumption["consumptionComparison"]["consumption"]
129
+ assert comparison["comparedToMonthBefore"]["trend"] == "DECREASE"
130
+ assert comparison["comparedToMonthBefore"]["percentage"] == 65
131
+ assert comparison["comparedToYearBefore"]["trend"] == "INCREASE"
132
+ assert comparison["comparedToYearBefore"]["percentage"] == 71
133
+
134
+ # Verify room breakdown
135
+ rooms = consumption["roomBreakdown"]["requestedMonth"]["perRoom"]
136
+ assert len(rooms) == 7 # Verify total number of rooms
137
+ assert rooms[0]["name"] == "Schlafzimmer"
138
+ assert rooms[0]["consumption"] == 4.102
139
+ assert rooms[0]["costInCents"] == 397.27
140
+
141
+ # Verify heating insights
142
+ insights = consumption["heatingInsights"]
143
+ assert insights["heatingHours"]["trend"] == "DECREASE"
144
+ assert insights["heatingHours"]["diff"] == 181
145
+ assert insights["outsideTemperature"]["diff"] == 1
146
+ assert insights["awayHours"]["diff"] == 37
File without changes
File without changes
File without changes
File without changes
File without changes