python-tado 0.18.14__tar.gz → 0.18.16__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.14/python_tado.egg-info → python_tado-0.18.16}/PKG-INFO +2 -2
  2. python_tado-0.18.16/PyTado/__init__.py +6 -0
  3. {python_tado-0.18.14 → python_tado-0.18.16}/PyTado/http.py +25 -1
  4. {python_tado-0.18.14 → python_tado-0.18.16}/PyTado/interface/api/my_tado.py +21 -2
  5. {python_tado-0.18.14 → python_tado-0.18.16}/PyTado/interface/interface.py +8 -0
  6. {python_tado-0.18.14 → python_tado-0.18.16}/pyproject.toml +2 -1
  7. {python_tado-0.18.14 → python_tado-0.18.16/python_tado.egg-info}/PKG-INFO +2 -2
  8. {python_tado-0.18.14 → python_tado-0.18.16}/tests/test_http.py +21 -0
  9. {python_tado-0.18.14 → python_tado-0.18.16}/tests/test_my_tado.py +49 -0
  10. python_tado-0.18.14/PyTado/__init__.py +0 -0
  11. {python_tado-0.18.14 → python_tado-0.18.16}/AUTHORS +0 -0
  12. {python_tado-0.18.14 → python_tado-0.18.16}/LICENSE +0 -0
  13. {python_tado-0.18.14 → python_tado-0.18.16}/PyTado/__main__.py +0 -0
  14. {python_tado-0.18.14 → python_tado-0.18.16}/PyTado/const.py +0 -0
  15. {python_tado-0.18.14 → python_tado-0.18.16}/PyTado/exceptions.py +0 -0
  16. {python_tado-0.18.14 → python_tado-0.18.16}/PyTado/interface/__init__.py +0 -0
  17. {python_tado-0.18.14 → python_tado-0.18.16}/PyTado/interface/api/__init__.py +0 -0
  18. {python_tado-0.18.14 → python_tado-0.18.16}/PyTado/interface/api/hops_tado.py +0 -0
  19. {python_tado-0.18.14 → python_tado-0.18.16}/PyTado/logger.py +0 -0
  20. {python_tado-0.18.14 → python_tado-0.18.16}/PyTado/zone/__init__.py +0 -0
  21. {python_tado-0.18.14 → python_tado-0.18.16}/PyTado/zone/hops_zone.py +0 -0
  22. {python_tado-0.18.14 → python_tado-0.18.16}/PyTado/zone/my_zone.py +0 -0
  23. {python_tado-0.18.14 → python_tado-0.18.16}/README.md +0 -0
  24. {python_tado-0.18.14 → python_tado-0.18.16}/python_tado.egg-info/SOURCES.txt +0 -0
  25. {python_tado-0.18.14 → python_tado-0.18.16}/python_tado.egg-info/dependency_links.txt +0 -0
  26. {python_tado-0.18.14 → python_tado-0.18.16}/python_tado.egg-info/entry_points.txt +0 -0
  27. {python_tado-0.18.14 → python_tado-0.18.16}/python_tado.egg-info/not-zip-safe +0 -0
  28. {python_tado-0.18.14 → python_tado-0.18.16}/python_tado.egg-info/requires.txt +0 -0
  29. {python_tado-0.18.14 → python_tado-0.18.16}/python_tado.egg-info/top_level.txt +0 -0
  30. {python_tado-0.18.14 → python_tado-0.18.16}/setup.cfg +0 -0
  31. {python_tado-0.18.14 → python_tado-0.18.16}/tests/test_hops_zone.py +0 -0
  32. {python_tado-0.18.14 → python_tado-0.18.16}/tests/test_my_zone.py +0 -0
  33. {python_tado-0.18.14 → python_tado-0.18.16}/tests/test_tado_interface.py +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-tado
3
- Version: 0.18.14
3
+ Version: 0.18.16
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
- Author-email: Chris Jewell <chrism0dwk@gmail.com>, "w.malgadey" <w.malgadey@gmail.com>, FilBr <filippo.barba@protonmail.com>
5
+ Author-email: Chris Jewell <chrism0dwk@gmail.com>, "w.malgadey" <w.malgadey@gmail.com>, FilBr <filippo.barba@protonmail.com>, Erwin Douna <e.douna@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
7
7
  Version 3, 29 June 2007
8
8
 
@@ -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
@@ -19,6 +19,7 @@ from urllib.parse import urlencode
19
19
 
20
20
  import requests
21
21
 
22
+ from PyTado import __version__
22
23
  from PyTado.const import CLIENT_ID_DEVICE
23
24
  from PyTado.exceptions import TadoException, TadoWrongCredentialsException
24
25
  from PyTado.logger import Logger
@@ -156,6 +157,7 @@ class Http:
156
157
  saved_refresh_token: str | None = None,
157
158
  http_session: requests.Session | None = None,
158
159
  debug: bool = False,
160
+ user_agent: str | None = None,
159
161
  ) -> None:
160
162
  """
161
163
  Initialize the HTTP client for interacting with the Tado API.
@@ -168,6 +170,8 @@ class Http:
168
170
  http_session (requests.Session | None): An optional pre-configured HTTP session.
169
171
  If None, a new session will be created.
170
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.
171
175
 
172
176
  Returns:
173
177
  None
@@ -181,7 +185,7 @@ class Http:
181
185
  self._refresh_at = datetime.now(timezone.utc) + timedelta(minutes=10)
182
186
  self._session = http_session or self._create_session()
183
187
  self._session.hooks["response"].append(self._log_response)
184
- self._headers = {"Referer": "https://app.tado.com/"}
188
+ self._headers = {"Referer": "https://app.tado.com/", "user-agent": user_agent or f"PyTado/{__version__}"}
185
189
 
186
190
  self._user_code: str | None = None
187
191
  self._device_verification_url: str | None = None
@@ -193,6 +197,8 @@ class Http:
193
197
  self._x_api: bool | None = None
194
198
  self._token_file_path = token_file_path
195
199
 
200
+ self._rate_limit: dict[str, str] = {}
201
+
196
202
  if (saved_refresh_token or self._load_token()) and self._refresh_token(
197
203
  refresh_token=saved_refresh_token, force_refresh=True
198
204
  ):
@@ -252,6 +258,11 @@ class Http:
252
258
  """
253
259
  return self._token_refresh
254
260
 
261
+ @property
262
+ def rate_limit(self) -> dict[str, str]:
263
+ """Retrieve the current rate limit information."""
264
+ return self._rate_limit
265
+
255
266
  def _create_session(self) -> requests.Session:
256
267
  session = requests.Session()
257
268
  session.hooks["response"].append(self._log_response)
@@ -314,6 +325,19 @@ class Http:
314
325
  if response.text is None or response.text == "":
315
326
  return {}
316
327
 
328
+ # Retrieve the headers to update the rate limit info
329
+ # They're stored in the object memory
330
+ if response.headers.get("RateLimit-Policy") is not None:
331
+ policy = response.headers["RateLimit-Policy"]
332
+ rate_limit = response.headers["RateLimit"]
333
+ # Example: RateLimit-Policy': '"perday";q=20000;w=86400'
334
+ # Rate limit is stored as follows: 'RateLimit': '"perday";r=15211'
335
+ self._rate_limit = {
336
+ "per-day": policy.split(";")[1].split("=")[1],
337
+ "window-seconds": policy.split(";")[2].split("=")[1],
338
+ "remaining": rate_limit.split(";")[1].split("=")[1],
339
+ }
340
+
317
341
  return response.json()
318
342
 
319
343
  def _configure_url(self, request: TadoRequest) -> str:
@@ -231,7 +231,7 @@ class Tado:
231
231
  request.command = f"zones/{zone:d}/schedule/activeTimetable"
232
232
  request.action = Action.CHANGE
233
233
  request.payload = {"id": timetable}
234
- request.mode = Mode.PLAIN
234
+ request.mode = Mode.OBJECT
235
235
 
236
236
  self._http.request(request)
237
237
 
@@ -258,7 +258,7 @@ class Tado:
258
258
  request.command = f"zones/{zone:d}/schedule/timetables/{timetable:d}/blocks/{day}"
259
259
  request.action = Action.CHANGE
260
260
  request.payload = data
261
- request.mode = Mode.PLAIN
261
+ request.mode = Mode.OBJECT
262
262
 
263
263
  return self._http.request(request)
264
264
 
@@ -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
@@ -90,6 +94,10 @@ class Tado:
90
94
  """Returns the URL for device verification."""
91
95
  return self._http.device_verification_url
92
96
 
97
+ def rate_limit_info(self) -> dict[str, str]:
98
+ """Retrieve the current rate limit information."""
99
+ return self._http.rate_limit
100
+
93
101
  def device_activation_status(self) -> DeviceActivationStatus:
94
102
  """Returns the status of the device activation."""
95
103
  return self._http.device_activation_status
@@ -4,12 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-tado"
7
- version = "0.18.14"
7
+ version = "0.18.16"
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" },
11
11
  { name = "w.malgadey", email = "w.malgadey@gmail.com" },
12
12
  { name = "FilBr", email = "filippo.barba@protonmail.com" },
13
+ { name = "Erwin Douna", email = "e.douna@gmail.com" },
13
14
  ]
14
15
  dependencies = ["requests"]
15
16
  requires-python = ">=3.11"
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-tado
3
- Version: 0.18.14
3
+ Version: 0.18.16
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
- Author-email: Chris Jewell <chrism0dwk@gmail.com>, "w.malgadey" <w.malgadey@gmail.com>, FilBr <filippo.barba@protonmail.com>
5
+ Author-email: Chris Jewell <chrism0dwk@gmail.com>, "w.malgadey" <w.malgadey@gmail.com>, FilBr <filippo.barba@protonmail.com>, Erwin Douna <e.douna@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
7
7
  Version 3, 29 June 2007
8
8
 
@@ -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