python-tado 0.18.0__tar.gz → 0.18.2__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 (31) hide show
  1. {python_tado-0.18.0/python_tado.egg-info → python_tado-0.18.2}/PKG-INFO +4 -1
  2. {python_tado-0.18.0 → python_tado-0.18.2}/PyTado/const.py +6 -0
  3. {python_tado-0.18.0 → python_tado-0.18.2}/PyTado/exceptions.py +0 -4
  4. {python_tado-0.18.0 → python_tado-0.18.2}/PyTado/http.py +20 -10
  5. {python_tado-0.18.0 → python_tado-0.18.2}/PyTado/interface/api/hops_tado.py +1 -1
  6. {python_tado-0.18.0 → python_tado-0.18.2}/PyTado/interface/api/my_tado.py +1 -1
  7. {python_tado-0.18.0 → python_tado-0.18.2}/PyTado/interface/interface.py +1 -1
  8. {python_tado-0.18.0 → python_tado-0.18.2}/pyproject.toml +2 -2
  9. {python_tado-0.18.0 → python_tado-0.18.2/python_tado.egg-info}/PKG-INFO +4 -1
  10. {python_tado-0.18.0 → python_tado-0.18.2}/python_tado.egg-info/SOURCES.txt +4 -2
  11. {python_tado-0.18.0 → python_tado-0.18.2}/python_tado.egg-info/requires.txt +3 -0
  12. python_tado-0.18.2/tests/test_http.py +180 -0
  13. {python_tado-0.18.0 → python_tado-0.18.2}/tests/test_my_tado.py +1 -1
  14. {python_tado-0.18.0 → python_tado-0.18.2}/tests/test_my_zone.py +2 -2
  15. python_tado-0.18.2/tests/test_tado_interface.py +84 -0
  16. {python_tado-0.18.0 → python_tado-0.18.2}/AUTHORS +0 -0
  17. {python_tado-0.18.0 → python_tado-0.18.2}/LICENSE +0 -0
  18. {python_tado-0.18.0 → python_tado-0.18.2}/PyTado/__init__.py +0 -0
  19. {python_tado-0.18.0 → python_tado-0.18.2}/PyTado/__main__.py +0 -0
  20. {python_tado-0.18.0 → python_tado-0.18.2}/PyTado/interface/__init__.py +0 -0
  21. {python_tado-0.18.0 → python_tado-0.18.2}/PyTado/interface/api/__init__.py +0 -0
  22. /python_tado-0.18.0/PyTado/logging.py → /python_tado-0.18.2/PyTado/logger.py +0 -0
  23. {python_tado-0.18.0 → python_tado-0.18.2}/PyTado/zone/__init__.py +0 -0
  24. {python_tado-0.18.0 → python_tado-0.18.2}/PyTado/zone/hops_zone.py +0 -0
  25. {python_tado-0.18.0 → python_tado-0.18.2}/PyTado/zone/my_zone.py +0 -0
  26. {python_tado-0.18.0 → python_tado-0.18.2}/README.md +0 -0
  27. {python_tado-0.18.0 → python_tado-0.18.2}/python_tado.egg-info/dependency_links.txt +0 -0
  28. {python_tado-0.18.0 → python_tado-0.18.2}/python_tado.egg-info/entry_points.txt +0 -0
  29. {python_tado-0.18.0 → python_tado-0.18.2}/python_tado.egg-info/not-zip-safe +0 -0
  30. {python_tado-0.18.0 → python_tado-0.18.2}/python_tado.egg-info/top_level.txt +0 -0
  31. {python_tado-0.18.0 → python_tado-0.18.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-tado
3
- Version: 0.18.0
3
+ Version: 0.18.2
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>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -699,7 +699,10 @@ Requires-Dist: black>=24.3; extra == "dev"
699
699
  Requires-Dist: pytype; extra == "dev"
700
700
  Requires-Dist: pylint; extra == "dev"
701
701
  Requires-Dist: types-requests; extra == "dev"
702
+ Requires-Dist: responses; extra == "dev"
703
+ Requires-Dist: coverage; extra == "dev"
702
704
  Requires-Dist: pytest; extra == "dev"
705
+ Requires-Dist: pytest-cov; extra == "dev"
703
706
 
704
707
  PyTado -- Pythonize your central heating
705
708
  ========================================
@@ -1,5 +1,11 @@
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
+ )
8
+
3
9
  # Types
4
10
  TYPE_AIR_CONDITIONING = "AIR_CONDITIONING"
5
11
  TYPE_HEATING = "HEATING"
@@ -9,10 +9,6 @@ class TadoNotSupportedException(TadoException):
9
9
  """Exception to indicate a requested action is not supported by Tado."""
10
10
 
11
11
 
12
- class TadoXNotSupportedException(TadoException):
13
- """Exception to indicate a requested action is not supported by the Tado X API."""
14
-
15
-
16
12
  class TadoCredentialsException(TadoException):
17
13
  """Exception to indicate something with credentials"""
18
14
 
@@ -11,8 +11,9 @@ from typing import Any
11
11
 
12
12
  import requests
13
13
 
14
- from PyTado.exceptions import TadoWrongCredentialsException
15
- from PyTado.logging import Logger
14
+ from PyTado.const import CLIENT_ID, CLIENT_SECRET
15
+ from PyTado.exceptions import TadoException, TadoWrongCredentialsException
16
+ from PyTado.logger import Logger
16
17
 
17
18
  _LOGGER = Logger(__name__)
18
19
 
@@ -141,7 +142,8 @@ class Http:
141
142
  self._headers = {"Referer": "https://app.tado.com/"}
142
143
  self._username = username
143
144
  self._password = password
144
- self._id, self._x_api, self._token_refresh = self._login()
145
+ self._id, self._token_refresh = self._login()
146
+ self._x_api = self._check_x_line_generation()
145
147
 
146
148
  def _log_response(self, response: requests.Response, *args, **kwargs):
147
149
  og_request_method = response.request.method
@@ -199,6 +201,7 @@ class Http:
199
201
 
200
202
  return response.json()
201
203
 
204
+ @property
202
205
  def is_x_line(self):
203
206
  return self._x_api
204
207
 
@@ -249,10 +252,8 @@ class Http:
249
252
 
250
253
  url = "https://auth.tado.com/oauth/token"
251
254
  data = {
252
- "client_id": "tado-web-app",
253
- "client_secret": (
254
- "wZaRN7rpjn3FoNyF5IFuxg9uMzYJcvOoQ8QWiIqS3hfk6gLhVlG57j5YNoZL2Rtc"
255
- ),
255
+ "client_id": CLIENT_ID,
256
+ "client_secret": CLIENT_SECRET,
256
257
  "grant_type": "refresh_token",
257
258
  "scope": "home.user",
258
259
  "refresh_token": self._token_refresh,
@@ -273,7 +274,13 @@ class Http:
273
274
  },
274
275
  )
275
276
 
276
- self._set_oauth_header(response.json())
277
+ if response.status_code == 200:
278
+ self._set_oauth_header(response.json())
279
+ return
280
+
281
+ raise TadoException(
282
+ f"Unknown error while refreshing token with status code {response.status_code}"
283
+ )
277
284
 
278
285
  def _login(self) -> tuple[int, bool, str] | None:
279
286
 
@@ -310,9 +317,12 @@ class Http:
310
317
  if response.status_code == 200:
311
318
  refresh_token = self._set_oauth_header(response.json())
312
319
  id_ = self._get_id()
313
- x_api_ = self._check_x_line_generation()
314
320
 
315
- return id_, x_api_, refresh_token
321
+ return id_, refresh_token
322
+
323
+ raise TadoException(
324
+ f"Login failed for unknown reason with status code {response.status_code}"
325
+ )
316
326
 
317
327
  def _get_id(self) -> int:
318
328
  request = TadoRequest()
@@ -8,7 +8,7 @@ from typing import Any
8
8
 
9
9
  from .my_tado import Tado, Timetable
10
10
 
11
- from ...logging import Logger
11
+ from ...logger import Logger
12
12
  from ...exceptions import TadoNotSupportedException
13
13
  from ...http import (
14
14
  Action,
@@ -8,7 +8,7 @@ import logging
8
8
 
9
9
  from typing import Any
10
10
 
11
- from ...logging import Logger
11
+ from ...logger import Logger
12
12
  from ...exceptions import TadoNotSupportedException
13
13
  from ...http import (
14
14
  Action,
@@ -36,4 +36,4 @@ class Tado:
36
36
 
37
37
  def __getattr__(self, name):
38
38
  """Delegiert den Aufruf von Methoden an die richtige API-Client-Implementierung."""
39
- return getattr(self.client, name)
39
+ return getattr(self._api, name)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-tado"
7
- version = "0.18.0"
7
+ version = "0.18.2"
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" },
@@ -30,7 +30,7 @@ classifiers = [
30
30
  GitHub = "https://github.com/wmalgadey/PyTado"
31
31
 
32
32
  [project.optional-dependencies]
33
- dev = ["black>=24.3", "pytype", "pylint", "types-requests", "pytest"]
33
+ dev = ["black>=24.3", "pytype", "pylint", "types-requests", "responses", "coverage", "pytest", "pytest-cov"]
34
34
 
35
35
  [project.scripts]
36
36
  pytado = "pytado.__main__:main"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-tado
3
- Version: 0.18.0
3
+ Version: 0.18.2
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>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -699,7 +699,10 @@ Requires-Dist: black>=24.3; extra == "dev"
699
699
  Requires-Dist: pytype; extra == "dev"
700
700
  Requires-Dist: pylint; extra == "dev"
701
701
  Requires-Dist: types-requests; extra == "dev"
702
+ Requires-Dist: responses; extra == "dev"
703
+ Requires-Dist: coverage; extra == "dev"
702
704
  Requires-Dist: pytest; extra == "dev"
705
+ Requires-Dist: pytest-cov; extra == "dev"
703
706
 
704
707
  PyTado -- Pythonize your central heating
705
708
  ========================================
@@ -7,7 +7,7 @@ PyTado/__main__.py
7
7
  PyTado/const.py
8
8
  PyTado/exceptions.py
9
9
  PyTado/http.py
10
- PyTado/logging.py
10
+ PyTado/logger.py
11
11
  PyTado/interface/__init__.py
12
12
  PyTado/interface/interface.py
13
13
  PyTado/interface/api/__init__.py
@@ -23,5 +23,7 @@ python_tado.egg-info/entry_points.txt
23
23
  python_tado.egg-info/not-zip-safe
24
24
  python_tado.egg-info/requires.txt
25
25
  python_tado.egg-info/top_level.txt
26
+ tests/test_http.py
26
27
  tests/test_my_tado.py
27
- tests/test_my_zone.py
28
+ tests/test_my_zone.py
29
+ tests/test_tado_interface.py
@@ -5,4 +5,7 @@ black>=24.3
5
5
  pytype
6
6
  pylint
7
7
  types-requests
8
+ responses
9
+ coverage
8
10
  pytest
11
+ pytest-cov
@@ -0,0 +1,180 @@
1
+ """Test the Http class."""
2
+
3
+ from datetime import datetime, timedelta
4
+ import json
5
+ import responses
6
+ import unittest
7
+
8
+ from PyTado.const import CLIENT_ID, CLIENT_SECRET
9
+ from PyTado.exceptions import TadoException, TadoWrongCredentialsException
10
+
11
+ from . import common
12
+
13
+ from PyTado.http import Http
14
+
15
+
16
+ class TestHttp(unittest.TestCase):
17
+ """Testcases for Http class."""
18
+
19
+ def setUp(self):
20
+ super().setUp()
21
+
22
+ # Mock the login response
23
+ responses.add(
24
+ responses.POST,
25
+ "https://auth.tado.com/oauth/token",
26
+ json={
27
+ "access_token": "value",
28
+ "expires_in": 1234,
29
+ "refresh_token": "another_value",
30
+ },
31
+ status=200,
32
+ )
33
+
34
+ responses.add(
35
+ responses.GET,
36
+ "https://my.tado.com/api/v2/homes/1234/",
37
+ json={"homes": [{"id": 1234}]},
38
+ status=200,
39
+ )
40
+
41
+ # Mock the get me response
42
+ responses.add(
43
+ responses.GET,
44
+ "https://my.tado.com/api/v2/me",
45
+ json={"homes": [{"id": 1234}]},
46
+ status=200,
47
+ )
48
+
49
+ @responses.activate
50
+ def test_login_successful(self):
51
+
52
+ instance = Http(
53
+ username="test_user",
54
+ password="test_pass",
55
+ debug=True,
56
+ )
57
+
58
+ # Verify that the login was successful
59
+ self.assertEqual(instance._id, 1234)
60
+ self.assertEqual(instance.is_x_line, False)
61
+
62
+ @responses.activate
63
+ def test_login_failed(self):
64
+
65
+ responses.replace(
66
+ responses.POST,
67
+ "https://auth.tado.com/oauth/token",
68
+ json={"error": "invalid_grant"},
69
+ status=400,
70
+ )
71
+
72
+ with self.assertRaises(
73
+ expected_exception=TadoWrongCredentialsException,
74
+ msg="Your username or password is invalid",
75
+ ):
76
+ Http(
77
+ username="test_user",
78
+ password="test_pass",
79
+ debug=True,
80
+ )
81
+
82
+ responses.replace(
83
+ responses.POST,
84
+ "https://auth.tado.com/oauth/token",
85
+ json={"error": "server failed"},
86
+ status=503,
87
+ )
88
+
89
+ with self.assertRaises(
90
+ expected_exception=TadoException,
91
+ msg="Login failed for unknown reason with status code 503",
92
+ ):
93
+ Http(
94
+ username="test_user",
95
+ password="test_pass",
96
+ debug=True,
97
+ )
98
+
99
+ @responses.activate
100
+ def test_line_x(self):
101
+
102
+ responses.replace(
103
+ responses.GET,
104
+ "https://my.tado.com/api/v2/homes/1234/",
105
+ json=json.loads(common.load_fixture("tadox/homes_response.json")),
106
+ status=200,
107
+ )
108
+
109
+ instance = Http(
110
+ username="test_user",
111
+ password="test_pass",
112
+ debug=True,
113
+ )
114
+
115
+ # Verify that the login was successful
116
+ self.assertEqual(instance._id, 1234)
117
+ self.assertEqual(instance.is_x_line, True)
118
+
119
+ @responses.activate
120
+ def test_refresh_token_success(self):
121
+ instance = Http(
122
+ username="test_user",
123
+ password="test_pass",
124
+ debug=True,
125
+ )
126
+
127
+ expected_params = {
128
+ "client_id": CLIENT_ID,
129
+ "client_secret": CLIENT_SECRET,
130
+ "grant_type": "refresh_token",
131
+ "scope": "home.user",
132
+ "refresh_token": "another_value",
133
+ }
134
+ # Mock the refresh token response
135
+ refresh_token = responses.replace(
136
+ responses.POST,
137
+ "https://auth.tado.com/oauth/token",
138
+ match=[
139
+ responses.matchers.query_param_matcher(expected_params),
140
+ ],
141
+ json={
142
+ "access_token": "new_value",
143
+ "expires_in": 1234,
144
+ "refresh_token": "new_refresh_value",
145
+ },
146
+ status=200,
147
+ )
148
+
149
+ # Force token refresh
150
+ instance._refresh_at = datetime.now() - timedelta(seconds=1)
151
+ instance._refresh_token()
152
+
153
+ assert refresh_token.call_count == 1
154
+
155
+ # Verify that the token was refreshed
156
+ self.assertEqual(instance._headers["Authorization"], "Bearer new_value")
157
+
158
+ @responses.activate
159
+ def test_refresh_token_failure(self):
160
+ instance = Http(
161
+ username="test_user",
162
+ password="test_pass",
163
+ debug=True,
164
+ )
165
+
166
+ # Mock the refresh token response with failure
167
+ refresh_token = responses.replace(
168
+ responses.POST,
169
+ "https://auth.tado.com/oauth/token",
170
+ json={"error": "invalid_grant"},
171
+ status=400,
172
+ )
173
+
174
+ # Force token refresh
175
+ instance._refresh_at = datetime.now() - timedelta(seconds=1)
176
+
177
+ with self.assertRaises(TadoException):
178
+ instance._refresh_token()
179
+
180
+ assert refresh_token.call_count == 1
@@ -16,7 +16,7 @@ class TadoTestCase(unittest.TestCase):
16
16
  def setUp(self) -> None:
17
17
  super().setUp()
18
18
  login_patch = mock.patch(
19
- "PyTado.http.Http._login", return_value=(1, False, "foo")
19
+ "PyTado.http.Http._login", return_value=(1, "foo")
20
20
  )
21
21
  get_me_patch = mock.patch("PyTado.interface.api.Tado.get_me")
22
22
  login_patch.start()
@@ -10,13 +10,13 @@ from PyTado.http import Http
10
10
  from PyTado.interface.api import Tado
11
11
 
12
12
 
13
- class TadoTestCase(unittest.TestCase):
13
+ class TadoZoneTestCase(unittest.TestCase):
14
14
  """Test cases for zone class"""
15
15
 
16
16
  def setUp(self) -> None:
17
17
  super().setUp()
18
18
  login_patch = mock.patch(
19
- "PyTado.http.Http._login", return_value=(1, False, "foo")
19
+ "PyTado.http.Http._login", return_value=(1, "foo")
20
20
  )
21
21
  get_me_patch = mock.patch("PyTado.interface.api.Tado.get_me")
22
22
  login_patch.start()
@@ -0,0 +1,84 @@
1
+ import json
2
+ import unittest
3
+ from unittest import mock
4
+
5
+ from . import common
6
+
7
+ from PyTado.http import Http
8
+ from PyTado.interface import Tado
9
+ import PyTado.interface.api as API
10
+
11
+
12
+ class TestTadoInterface(unittest.TestCase):
13
+ """Test cases for main tado interface class"""
14
+
15
+ def test_interface_with_tado_api(self):
16
+ login_patch = mock.patch(
17
+ "PyTado.http.Http._login", return_value=(1, "foo")
18
+ )
19
+ login_mock = login_patch.start()
20
+ check_x_patch = mock.patch(
21
+ "PyTado.http.Http._check_x_line_generation", return_value=False
22
+ )
23
+ check_x_patch.start()
24
+ self.addCleanup(check_x_patch.stop)
25
+
26
+ with mock.patch("PyTado.interface.api.my_tado.Tado.get_me") as mock_it:
27
+ tado_interface = Tado("my@username.com", "mypassword")
28
+ tado_interface.get_me()
29
+
30
+ assert not tado_interface._http.is_x_line
31
+ mock_it.assert_called_once()
32
+
33
+ with mock.patch(
34
+ "PyTado.interface.api.hops_tado.TadoX.get_me"
35
+ ) as mock_it:
36
+ tado_interface = Tado("my@username.com", "mypassword")
37
+ tado_interface.get_me()
38
+
39
+ mock_it.assert_not_called()
40
+
41
+ assert login_mock.call_count == 2
42
+
43
+ def test_interface_with_tadox_api(self):
44
+ login_patch = mock.patch(
45
+ "PyTado.http.Http._login", return_value=(1, "foo")
46
+ )
47
+ login_mock = login_patch.start()
48
+ check_x_patch = mock.patch(
49
+ "PyTado.http.Http._check_x_line_generation", return_value=True
50
+ )
51
+ check_x_patch.start()
52
+ self.addCleanup(check_x_patch.stop)
53
+
54
+ with mock.patch(
55
+ "PyTado.interface.api.hops_tado.TadoX.get_me"
56
+ ) as mock_it:
57
+ tado_interface = Tado("my@username.com", "mypassword")
58
+ tado_interface.get_me()
59
+
60
+ mock_it.assert_called_once()
61
+ assert tado_interface._http.is_x_line
62
+
63
+ login_mock.assert_called_once()
64
+
65
+ def test_error_handling_on_api_calls(self):
66
+ login_patch = mock.patch(
67
+ "PyTado.http.Http._login", return_value=(1, "foo")
68
+ )
69
+ login_mock = login_patch.start()
70
+ check_x_patch = mock.patch(
71
+ "PyTado.http.Http._check_x_line_generation", return_value=False
72
+ )
73
+ check_x_patch.start()
74
+ self.addCleanup(check_x_patch.stop)
75
+
76
+ with mock.patch("PyTado.interface.api.my_tado.Tado.get_me") as mock_it:
77
+ mock_it.side_effect = Exception("API Error")
78
+
79
+ tado_interface = Tado("my@username.com", "mypassword")
80
+
81
+ with self.assertRaises(Exception) as context:
82
+ tado_interface.get_me()
83
+
84
+ self.assertIn("API Error", str(context.exception))
File without changes
File without changes
File without changes
File without changes