python-tado 0.18.1__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.1/python_tado.egg-info → python_tado-0.18.2}/PKG-INFO +2 -1
  2. {python_tado-0.18.1 → python_tado-0.18.2}/PyTado/const.py +6 -0
  3. {python_tado-0.18.1 → python_tado-0.18.2}/PyTado/exceptions.py +0 -4
  4. {python_tado-0.18.1 → python_tado-0.18.2}/PyTado/http.py +19 -10
  5. {python_tado-0.18.1 → python_tado-0.18.2}/PyTado/interface/api/hops_tado.py +1 -1
  6. {python_tado-0.18.1 → python_tado-0.18.2}/PyTado/interface/api/my_tado.py +1 -1
  7. {python_tado-0.18.1 → python_tado-0.18.2}/pyproject.toml +2 -2
  8. {python_tado-0.18.1 → python_tado-0.18.2/python_tado.egg-info}/PKG-INFO +2 -1
  9. {python_tado-0.18.1 → python_tado-0.18.2}/python_tado.egg-info/SOURCES.txt +2 -1
  10. {python_tado-0.18.1 → python_tado-0.18.2}/python_tado.egg-info/requires.txt +1 -0
  11. python_tado-0.18.2/tests/test_http.py +180 -0
  12. {python_tado-0.18.1 → python_tado-0.18.2}/tests/test_my_tado.py +1 -1
  13. {python_tado-0.18.1 → python_tado-0.18.2}/tests/test_my_zone.py +1 -1
  14. {python_tado-0.18.1 → python_tado-0.18.2}/tests/test_tado_interface.py +19 -7
  15. {python_tado-0.18.1 → python_tado-0.18.2}/AUTHORS +0 -0
  16. {python_tado-0.18.1 → python_tado-0.18.2}/LICENSE +0 -0
  17. {python_tado-0.18.1 → python_tado-0.18.2}/PyTado/__init__.py +0 -0
  18. {python_tado-0.18.1 → python_tado-0.18.2}/PyTado/__main__.py +0 -0
  19. {python_tado-0.18.1 → python_tado-0.18.2}/PyTado/interface/__init__.py +0 -0
  20. {python_tado-0.18.1 → python_tado-0.18.2}/PyTado/interface/api/__init__.py +0 -0
  21. {python_tado-0.18.1 → python_tado-0.18.2}/PyTado/interface/interface.py +0 -0
  22. /python_tado-0.18.1/PyTado/logging.py → /python_tado-0.18.2/PyTado/logger.py +0 -0
  23. {python_tado-0.18.1 → python_tado-0.18.2}/PyTado/zone/__init__.py +0 -0
  24. {python_tado-0.18.1 → python_tado-0.18.2}/PyTado/zone/hops_zone.py +0 -0
  25. {python_tado-0.18.1 → python_tado-0.18.2}/PyTado/zone/my_zone.py +0 -0
  26. {python_tado-0.18.1 → python_tado-0.18.2}/README.md +0 -0
  27. {python_tado-0.18.1 → python_tado-0.18.2}/python_tado.egg-info/dependency_links.txt +0 -0
  28. {python_tado-0.18.1 → python_tado-0.18.2}/python_tado.egg-info/entry_points.txt +0 -0
  29. {python_tado-0.18.1 → python_tado-0.18.2}/python_tado.egg-info/not-zip-safe +0 -0
  30. {python_tado-0.18.1 → python_tado-0.18.2}/python_tado.egg-info/top_level.txt +0 -0
  31. {python_tado-0.18.1 → 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.1
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,6 +699,7 @@ 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"
702
703
  Requires-Dist: coverage; extra == "dev"
703
704
  Requires-Dist: pytest; extra == "dev"
704
705
  Requires-Dist: pytest-cov; extra == "dev"
@@ -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
@@ -250,10 +252,8 @@ class Http:
250
252
 
251
253
  url = "https://auth.tado.com/oauth/token"
252
254
  data = {
253
- "client_id": "tado-web-app",
254
- "client_secret": (
255
- "wZaRN7rpjn3FoNyF5IFuxg9uMzYJcvOoQ8QWiIqS3hfk6gLhVlG57j5YNoZL2Rtc"
256
- ),
255
+ "client_id": CLIENT_ID,
256
+ "client_secret": CLIENT_SECRET,
257
257
  "grant_type": "refresh_token",
258
258
  "scope": "home.user",
259
259
  "refresh_token": self._token_refresh,
@@ -274,7 +274,13 @@ class Http:
274
274
  },
275
275
  )
276
276
 
277
- 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
+ )
278
284
 
279
285
  def _login(self) -> tuple[int, bool, str] | None:
280
286
 
@@ -311,9 +317,12 @@ class Http:
311
317
  if response.status_code == 200:
312
318
  refresh_token = self._set_oauth_header(response.json())
313
319
  id_ = self._get_id()
314
- x_api_ = self._check_x_line_generation()
315
320
 
316
- 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
+ )
317
326
 
318
327
  def _get_id(self) -> int:
319
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,
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-tado"
7
- version = "0.18.1"
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", "coverage", "pytest", "pytest-cov"]
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.1
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,6 +699,7 @@ 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"
702
703
  Requires-Dist: coverage; extra == "dev"
703
704
  Requires-Dist: pytest; extra == "dev"
704
705
  Requires-Dist: pytest-cov; extra == "dev"
@@ -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,6 +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
28
  tests/test_my_zone.py
28
29
  tests/test_tado_interface.py
@@ -5,6 +5,7 @@ black>=24.3
5
5
  pytype
6
6
  pylint
7
7
  types-requests
8
+ responses
8
9
  coverage
9
10
  pytest
10
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()
@@ -16,7 +16,7 @@ class TadoZoneTestCase(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()
@@ -14,10 +14,14 @@ class TestTadoInterface(unittest.TestCase):
14
14
 
15
15
  def test_interface_with_tado_api(self):
16
16
  login_patch = mock.patch(
17
- "PyTado.http.Http._login", return_value=(1, False, "foo")
17
+ "PyTado.http.Http._login", return_value=(1, "foo")
18
18
  )
19
19
  login_mock = login_patch.start()
20
- self.addCleanup(login_patch.stop)
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)
21
25
 
22
26
  with mock.patch("PyTado.interface.api.my_tado.Tado.get_me") as mock_it:
23
27
  tado_interface = Tado("my@username.com", "mypassword")
@@ -38,10 +42,14 @@ class TestTadoInterface(unittest.TestCase):
38
42
 
39
43
  def test_interface_with_tadox_api(self):
40
44
  login_patch = mock.patch(
41
- "PyTado.http.Http._login", return_value=(1, True, "foo")
45
+ "PyTado.http.Http._login", return_value=(1, "foo")
42
46
  )
43
47
  login_mock = login_patch.start()
44
- self.addCleanup(login_patch.stop)
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)
45
53
 
46
54
  with mock.patch(
47
55
  "PyTado.interface.api.hops_tado.TadoX.get_me"
@@ -56,10 +64,14 @@ class TestTadoInterface(unittest.TestCase):
56
64
 
57
65
  def test_error_handling_on_api_calls(self):
58
66
  login_patch = mock.patch(
59
- "PyTado.http.Http._login", return_value=(1, False, "foo")
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
60
72
  )
61
- login_patch.start()
62
- self.addCleanup(login_patch.stop)
73
+ check_x_patch.start()
74
+ self.addCleanup(check_x_patch.stop)
63
75
 
64
76
  with mock.patch("PyTado.interface.api.my_tado.Tado.get_me") as mock_it:
65
77
  mock_it.side_effect = Exception("API Error")
File without changes
File without changes
File without changes
File without changes