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.
- {python_tado-0.18.13/python_tado.egg-info → python_tado-0.18.15}/PKG-INFO +1 -1
- python_tado-0.18.15/PyTado/__init__.py +6 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/http.py +47 -39
- {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/interface/api/my_tado.py +19 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/interface/interface.py +4 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/pyproject.toml +1 -1
- {python_tado-0.18.13 → python_tado-0.18.15/python_tado.egg-info}/PKG-INFO +1 -1
- {python_tado-0.18.13 → python_tado-0.18.15}/tests/test_http.py +21 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/tests/test_my_tado.py +49 -0
- python_tado-0.18.13/PyTado/__init__.py +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/AUTHORS +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/LICENSE +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/__main__.py +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/const.py +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/exceptions.py +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/interface/__init__.py +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/interface/api/__init__.py +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/interface/api/hops_tado.py +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/logger.py +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/zone/__init__.py +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/zone/hops_zone.py +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/PyTado/zone/my_zone.py +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/README.md +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/python_tado.egg-info/SOURCES.txt +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/python_tado.egg-info/dependency_links.txt +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/python_tado.egg-info/entry_points.txt +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/python_tado.egg-info/not-zip-safe +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/python_tado.egg-info/requires.txt +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/python_tado.egg-info/top_level.txt +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/setup.cfg +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/tests/test_hops_zone.py +0 -0
- {python_tado-0.18.13 → python_tado-0.18.15}/tests/test_my_zone.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|