python-tado 0.18.9__tar.gz → 0.19.1__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.19.1/PKG-INFO +240 -0
- {python_tado-0.18.9 → python_tado-0.19.1}/PyTado/__main__.py +62 -11
- {python_tado-0.18.9 → python_tado-0.19.1}/PyTado/http.py +20 -13
- {python_tado-0.18.9 → python_tado-0.19.1}/PyTado/interface/__init__.py +2 -0
- {python_tado-0.18.9 → python_tado-0.19.1}/PyTado/interface/api/__init__.py +2 -0
- {python_tado-0.18.9 → python_tado-0.19.1}/PyTado/interface/api/hops_tado.py +59 -9
- {python_tado-0.18.9 → python_tado-0.19.1}/PyTado/interface/interface.py +6 -5
- python_tado-0.19.1/PyTado/logger.py +24 -0
- {python_tado-0.18.9 → python_tado-0.19.1}/PyTado/zone/__init__.py +2 -0
- {python_tado-0.18.9 → python_tado-0.19.1}/PyTado/zone/hops_zone.py +17 -34
- {python_tado-0.18.9 → python_tado-0.19.1}/PyTado/zone/my_zone.py +21 -7
- {python_tado-0.18.9 → python_tado-0.19.1}/README.md +31 -13
- python_tado-0.19.1/pyproject.toml +74 -0
- python_tado-0.18.9/PKG-INFO +0 -892
- python_tado-0.18.9/PyTado/logger.py +0 -50
- python_tado-0.18.9/pyproject.toml +0 -309
- python_tado-0.18.9/python_tado.egg-info/PKG-INFO +0 -892
- python_tado-0.18.9/python_tado.egg-info/SOURCES.txt +0 -30
- python_tado-0.18.9/python_tado.egg-info/dependency_links.txt +0 -1
- python_tado-0.18.9/python_tado.egg-info/entry_points.txt +0 -2
- python_tado-0.18.9/python_tado.egg-info/not-zip-safe +0 -1
- python_tado-0.18.9/python_tado.egg-info/requires.txt +0 -16
- python_tado-0.18.9/python_tado.egg-info/top_level.txt +0 -1
- python_tado-0.18.9/setup.cfg +0 -4
- python_tado-0.18.9/tests/test_hops_zone.py +0 -152
- python_tado-0.18.9/tests/test_http.py +0 -285
- python_tado-0.18.9/tests/test_my_tado.py +0 -97
- python_tado-0.18.9/tests/test_my_zone.py +0 -680
- python_tado-0.18.9/tests/test_tado_interface.py +0 -86
- {python_tado-0.18.9 → python_tado-0.19.1}/AUTHORS +0 -0
- {python_tado-0.18.9 → python_tado-0.19.1}/LICENSE +0 -0
- {python_tado-0.18.9 → python_tado-0.19.1}/PyTado/__init__.py +0 -0
- {python_tado-0.18.9 → python_tado-0.19.1}/PyTado/const.py +0 -0
- {python_tado-0.18.9 → python_tado-0.19.1}/PyTado/exceptions.py +0 -0
- {python_tado-0.18.9 → python_tado-0.19.1}/PyTado/interface/api/my_tado.py +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: python-tado
|
|
3
|
+
Version: 0.19.1
|
|
4
|
+
Summary: PyTado from chrism0dwk, modified by w.malgadey, diplix, michaelarnauts, LenhartStephan, splifter, syssi, andersonshatch, Yippy, p0thi, Coffee2CodeNL, chiefdragon, FilBr, nikilase, albertomontesg, Moritz-Schmidt, palazzem
|
|
5
|
+
License: GPL-3.0-or-later
|
|
6
|
+
Keywords: tado
|
|
7
|
+
Author: Chris Jewell
|
|
8
|
+
Author-email: chrism0dwk@gmail.com
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
13
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
14
|
+
Classifier: Natural Language :: English
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Home Automation
|
|
20
|
+
Provides-Extra: all
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Provides-Extra: lint
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: black (>=24.10) ; extra == "lint" or extra == "all"
|
|
25
|
+
Requires-Dist: pre-commit ; extra == "dev" or extra == "all"
|
|
26
|
+
Requires-Dist: pylint ; extra == "lint" or extra == "all"
|
|
27
|
+
Requires-Dist: pytest ; extra == "test" or extra == "all"
|
|
28
|
+
Requires-Dist: pytest-cov ; extra == "test" or extra == "all"
|
|
29
|
+
Requires-Dist: pytest-mock ; extra == "test" or extra == "all"
|
|
30
|
+
Requires-Dist: pytest-socket ; extra == "test" or extra == "all"
|
|
31
|
+
Requires-Dist: pytype ; extra == "dev" or extra == "all"
|
|
32
|
+
Requires-Dist: requests
|
|
33
|
+
Requires-Dist: responses ; extra == "test" or extra == "all"
|
|
34
|
+
Requires-Dist: types-requests ; extra == "dev" or extra == "all"
|
|
35
|
+
Project-URL: Documentation, https://wmalgadey.github.io/PyTado/
|
|
36
|
+
Project-URL: Homepage, https://github.com/wmalgadey/PyTado
|
|
37
|
+
Project-URL: Repository, https://github.com/wmalgadey/PyTado
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# PyTado -- Pythonize your central heating
|
|
41
|
+
|
|
42
|
+
[](https://github.com/wmalgadey/PyTado/actions/workflows/lint-and-test-matrix.yml)
|
|
43
|
+
[](https://github.com/wmalgadey/PyTado/actions/workflows/publish-to-pypi.yml)
|
|
44
|
+
[](https://badge.fury.io/py/python-tado)
|
|
45
|
+
[](https://codecov.io/github/wmalgadey/PyTado)
|
|
46
|
+
[![Open in Dev Containers][devcontainer-shield]][devcontainer]
|
|
47
|
+
|
|
48
|
+
PyTado is a Python module implementing an interface to the Tado web API. It allows a user to interact with their
|
|
49
|
+
Tado heating system for the purposes of monitoring or controlling their heating system, beyond what Tado themselves
|
|
50
|
+
currently offer.
|
|
51
|
+
|
|
52
|
+
It is hoped that this module might be used by those who wish to tweak their Tado systems, and further optimise their
|
|
53
|
+
heating setups.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
Original author: Chris Jewell <chrism0dwk@gmail.com>
|
|
58
|
+
|
|
59
|
+
Licence: GPL v3
|
|
60
|
+
|
|
61
|
+
Copyright: Chris Jewell 2016-2018
|
|
62
|
+
|
|
63
|
+
## Disclaimer
|
|
64
|
+
|
|
65
|
+
Besides owning a Tado system, I have no connection with the Tado company themselves. PyTado was created for my own use,
|
|
66
|
+
and for others who may wish to experiment with personal Internet of Things systems. I receive no help (financial or
|
|
67
|
+
otherwise) from Tado, and have no business interest with them. This software is provided without warranty, according to
|
|
68
|
+
the GNU Public Licence version 3, and should therefore not be used where it may endanger life, financial stakes, or
|
|
69
|
+
cause discomfort and inconvenience to others.
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
As of the 15th of March 2025, Tado has updated their OAuth2 authentication flow. It will now use the device flow, instead of a username/password flow. This means that the user will have to authenticate the device using a browser, and then enter the code that is displayed on the browser into the terminal.
|
|
74
|
+
|
|
75
|
+
PyTado handles this as following:
|
|
76
|
+
|
|
77
|
+
1. The `_login_device_flow()` will be invoked at the initialization of a PyTado object. This will start the device flow and will return a URL and a code that the user will have to enter in the browser. The URL can be obtained via the method `device_verification_url()`. Or, when in debug mode, the URL will be printed. Alternatively, you can use the `device_activation_status()` method to check if the device has been activated. It returns three statuses: `NOT_STARTED`, `PENDING`, and `COMPLETED`. Wait to invoke the `device_activation()` method until the status is `PENDING`.
|
|
78
|
+
|
|
79
|
+
2. Once the URL is obtained, the user will have to enter the code that is displayed on the browser into the terminal. By default, the URL has the `user_code` attached, for the ease of going trough the flow. At this point, run the method `device_activation()`. It will poll every five seconds to see if the flow has been completed. If the flow has been completed, the method will return a token that will be used for all further requests. It will timeout after five minutes.
|
|
80
|
+
|
|
81
|
+
3. Once the token has been obtained, the user can use the PyTado object to interact with the Tado API. The token will be stored in the `Tado` object, and will be used for all further requests. The token will be refreshed automatically when it expires.
|
|
82
|
+
The `device_verification_url()` will be reset to `None` and the `device_activation_status()` will return `COMPLETED`.
|
|
83
|
+
|
|
84
|
+
### Screenshots of the device flow
|
|
85
|
+
|
|
86
|
+

|
|
87
|
+

|
|
88
|
+

|
|
89
|
+
|
|
90
|
+
### How to not authenticate the device again
|
|
91
|
+
|
|
92
|
+
It is possible to save the refresh token and reuse to skip the next login.
|
|
93
|
+
|
|
94
|
+
The following code will use the `refresh_token` file to save the refresh-token after login, and load the refresh-token if you create the Tado interface class again.
|
|
95
|
+
|
|
96
|
+
If the file doesn't exists, the webbrowser is started and the device authentication url is automatically opened. You can activate the device in the browser. When you restart the program, the refresh-token is reused and no webbrowser will be opened.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import webbrowser # only needed for direct web browser access
|
|
100
|
+
|
|
101
|
+
from PyTado.interface.interface import Tado
|
|
102
|
+
|
|
103
|
+
tado = Tado(token_file_path="/var/tado/refresh_token")
|
|
104
|
+
|
|
105
|
+
status = tado.device_activation_status()
|
|
106
|
+
|
|
107
|
+
if status == "PENDING":
|
|
108
|
+
url = tado.device_verification_url()
|
|
109
|
+
|
|
110
|
+
webbrowser.open_new_tab(url)
|
|
111
|
+
|
|
112
|
+
tado.device_activation()
|
|
113
|
+
|
|
114
|
+
status = tado.device_activation_status()
|
|
115
|
+
|
|
116
|
+
if status == "COMPLETED":
|
|
117
|
+
print("Login successful")
|
|
118
|
+
else:
|
|
119
|
+
print(f"Login status is {status}")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Example code
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
"""Example client for PyTado"""
|
|
126
|
+
|
|
127
|
+
from PyTado.interface.interface import Tado
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def main() -> None:
|
|
131
|
+
"""Retrieve all zones, once successfully logged in"""
|
|
132
|
+
tado = Tado()
|
|
133
|
+
|
|
134
|
+
print("Device activation status: ", tado.device_activation_status())
|
|
135
|
+
print("Device verification URL: ", tado.device_verification_url())
|
|
136
|
+
|
|
137
|
+
print("Starting device activation")
|
|
138
|
+
tado.device_activation()
|
|
139
|
+
|
|
140
|
+
print("Device activation status: ", tado.device_activation_status())
|
|
141
|
+
|
|
142
|
+
zones = tado.get_zones()
|
|
143
|
+
print(zones)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
main()
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Note: For developers, there is an `example.py` script in `examples/` which is configured to fetch data from your account.
|
|
151
|
+
You can then invoke `python examples/example.py`.
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## Contributing
|
|
155
|
+
|
|
156
|
+
We are very open to the community's contributions - be it a quick fix of a typo, or a completely new feature!
|
|
157
|
+
|
|
158
|
+
You don't need to be a Python expert to provide meaningful improvements. To learn how to get started, check out our
|
|
159
|
+
[Contributor Guidelines](https://github.com/wmalgadey/econnect-python/blob/main/CONTRIBUTING.md) first, and ask for help
|
|
160
|
+
in [GitHub Discussions](https://github.com/wmalgadey/PyTado/discussions) if you have questions.
|
|
161
|
+
|
|
162
|
+
## Development
|
|
163
|
+
|
|
164
|
+
We welcome external contributions, even though the project was initially intended for personal use. If you think some
|
|
165
|
+
parts could be exposed with a more generic interface, please open a [GitHub issue](https://github.com/wmalgadey/PyTado/issues)
|
|
166
|
+
to discuss your suggestion.
|
|
167
|
+
|
|
168
|
+
### Setting up a devcontainer
|
|
169
|
+
|
|
170
|
+
The easiest way to start, is by opening a CodeSpace here on GitHub, or by using
|
|
171
|
+
the [Dev Container][devcontainer] feature of Visual Studio Code.
|
|
172
|
+
|
|
173
|
+
[![Open in Dev Containers][devcontainer-shield]][devcontainer]
|
|
174
|
+
|
|
175
|
+
### Dev Environment
|
|
176
|
+
|
|
177
|
+
To contribute to this repository, you should first clone your fork and then setup your development environment. Clone
|
|
178
|
+
your repository as follows (replace yourusername with your GitHub account name):
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
git clone https://github.com/yourusername/PyTado.git
|
|
182
|
+
cd PyTado
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Then, to create your development environment and install the project with its dependencies, execute the following
|
|
186
|
+
commands in your terminal:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Create and activate a new virtual environment
|
|
190
|
+
python3 -m venv venv
|
|
191
|
+
source venv/bin/activate
|
|
192
|
+
|
|
193
|
+
# install all dependencies
|
|
194
|
+
poetry install
|
|
195
|
+
|
|
196
|
+
# Install pre-commit hooks
|
|
197
|
+
poetry run pre-commit install
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Coding Guidelines
|
|
201
|
+
|
|
202
|
+
To maintain a consistent codebase, we utilize [black][1]. Consistency is crucial as it helps readability, reduces errors,
|
|
203
|
+
and facilitates collaboration among developers.
|
|
204
|
+
|
|
205
|
+
To ensure that every commit adheres to our coding standards, we've integrated [pre-commit hooks][2]. These hooks
|
|
206
|
+
automatically run `black` before each commit, ensuring that all code changes are automatically checked and formatted.
|
|
207
|
+
|
|
208
|
+
For details on how to set up your development environment to make use of these hooks, please refer to the
|
|
209
|
+
[Development][3] section of our documentation.
|
|
210
|
+
|
|
211
|
+
[1]: https://github.com/ambv/black
|
|
212
|
+
[2]: https://pre-commit.com/
|
|
213
|
+
[3]: https://github.com/wmalgadey/PyTado#development
|
|
214
|
+
|
|
215
|
+
### Testing
|
|
216
|
+
|
|
217
|
+
Ensuring the robustness and reliability of our code is paramount. Therefore, all contributions must include at least one
|
|
218
|
+
test to verify the intended behavior.
|
|
219
|
+
|
|
220
|
+
To run tests locally, execute the test suite using `pytest` with the following command:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
pytest tests/ --cov --cov-branch -vv
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
A message from the original author:
|
|
229
|
+
|
|
230
|
+
> This software is at a purely experimental stage. If you're interested and can write Python, clone the Github repo,
|
|
231
|
+
> drop me a line, and get involved!
|
|
232
|
+
>
|
|
233
|
+
> Best wishes and a warm winter to all!
|
|
234
|
+
>
|
|
235
|
+
> Chris Jewell
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
[devcontainer-shield]: https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode
|
|
239
|
+
[devcontainer]: https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/wmalgadey/PyTado
|
|
240
|
+
|
|
@@ -9,43 +9,92 @@ import sys
|
|
|
9
9
|
from PyTado.interface import Tado
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def log_in():
|
|
13
|
-
|
|
12
|
+
def log_in(args):
|
|
13
|
+
"""
|
|
14
|
+
Log in to the Tado API by activating the current device.
|
|
15
|
+
|
|
16
|
+
Add --token_file_path to the command line arguments to store the
|
|
17
|
+
refresh token in a file.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
args (argparse.Namespace): The parsed command-line arguments.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Tado: An instance of the Tado interface.
|
|
24
|
+
"""
|
|
25
|
+
t = Tado(token_file_path=args.token_file_path)
|
|
14
26
|
t.device_activation()
|
|
15
27
|
return t
|
|
16
28
|
|
|
17
29
|
|
|
18
|
-
def get_me():
|
|
19
|
-
|
|
30
|
+
def get_me(args):
|
|
31
|
+
"""
|
|
32
|
+
Retrieve and print home information from the Tado API.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
args (argparse.Namespace): The parsed command-line arguments.
|
|
36
|
+
"""
|
|
37
|
+
t = log_in(args)
|
|
20
38
|
me = t.get_me()
|
|
21
39
|
print(me)
|
|
22
40
|
|
|
23
41
|
|
|
24
42
|
def get_state(args):
|
|
25
|
-
|
|
43
|
+
"""
|
|
44
|
+
Retrieve and print the state of a specific zone from the Tado API.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
args (argparse.Namespace): The parsed command-line arguments.
|
|
48
|
+
"""
|
|
49
|
+
t = log_in(args)
|
|
26
50
|
zone = t.get_state(int(args.zone))
|
|
27
51
|
print(zone)
|
|
28
52
|
|
|
29
53
|
|
|
30
54
|
def get_states(args):
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
55
|
+
"""
|
|
56
|
+
Retrieve and print the states of all zones from the Tado API.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
args (argparse.Namespace): The parsed command-line arguments.
|
|
60
|
+
"""
|
|
61
|
+
t = log_in(args)
|
|
62
|
+
zones = t.get_zone_states()
|
|
63
|
+
print(zones)
|
|
34
64
|
|
|
35
65
|
|
|
36
66
|
def get_capabilities(args):
|
|
37
|
-
|
|
67
|
+
"""
|
|
68
|
+
Retrieve and print the capabilities of a specific zone from the Tado API.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
args (argparse.Namespace): The parsed command-line arguments.
|
|
72
|
+
"""
|
|
73
|
+
t = log_in(args)
|
|
38
74
|
capabilities = t.get_capabilities(int(args.zone))
|
|
39
75
|
print(capabilities)
|
|
40
76
|
|
|
41
77
|
|
|
42
78
|
def main():
|
|
43
|
-
"""
|
|
79
|
+
"""
|
|
80
|
+
Main method for the script.
|
|
81
|
+
|
|
82
|
+
Sets up the argument parser, handles subcommands, and executes the appropriate function.
|
|
83
|
+
"""
|
|
44
84
|
parser = argparse.ArgumentParser(
|
|
45
85
|
description="Pytado - Tado thermostat device control",
|
|
46
86
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
47
87
|
)
|
|
48
88
|
|
|
89
|
+
required_flags = parser.add_argument_group("required arguments")
|
|
90
|
+
|
|
91
|
+
# Required flags go here.
|
|
92
|
+
required_flags.add_argument(
|
|
93
|
+
"--token_file_path",
|
|
94
|
+
required=True,
|
|
95
|
+
help="Path to the file where the refresh token should be stored.",
|
|
96
|
+
)
|
|
97
|
+
|
|
49
98
|
# Flags with default values go here.
|
|
50
99
|
log_levels = {logging.getLevelName(level): level for level in [10, 20, 30, 40, 50]}
|
|
51
100
|
parser.add_argument(
|
|
@@ -67,7 +116,9 @@ def main():
|
|
|
67
116
|
start_activity_parser = subparsers.add_parser("get_states", help="Get states of all zones.")
|
|
68
117
|
start_activity_parser.set_defaults(func=get_states)
|
|
69
118
|
|
|
70
|
-
start_activity_parser = subparsers.add_parser(
|
|
119
|
+
start_activity_parser = subparsers.add_parser(
|
|
120
|
+
"get_capabilities", help="Get capabilities of zone."
|
|
121
|
+
)
|
|
71
122
|
start_activity_parser.add_argument("--zone", help="Zone to get the capabilities of.")
|
|
72
123
|
start_activity_parser.set_defaults(func=get_capabilities)
|
|
73
124
|
|
|
@@ -159,8 +159,8 @@ class Http:
|
|
|
159
159
|
Args:
|
|
160
160
|
token_file_path (str | None): Path to the file where the token is stored.
|
|
161
161
|
If None, the token will not be saved to a file.
|
|
162
|
-
saved_refresh_token (str | None): A previously saved refresh token to use for
|
|
163
|
-
If None, a new token will be requested.
|
|
162
|
+
saved_refresh_token (str | None): A previously saved refresh token to use for
|
|
163
|
+
authentication. If None, a new token will be requested.
|
|
164
164
|
http_session (requests.Session | None): An optional pre-configured HTTP session.
|
|
165
165
|
If None, a new session will be created.
|
|
166
166
|
debug (bool): If True, enables debug logging. Defaults to False.
|
|
@@ -189,10 +189,9 @@ class Http:
|
|
|
189
189
|
self._x_api: bool | None = None
|
|
190
190
|
self._token_file_path = token_file_path
|
|
191
191
|
|
|
192
|
-
if
|
|
193
|
-
refresh_token=saved_refresh_token, force_refresh=True
|
|
194
|
-
|
|
195
|
-
self._device_ready()
|
|
192
|
+
if saved_refresh_token or self._load_token():
|
|
193
|
+
if self._refresh_token(refresh_token=saved_refresh_token, force_refresh=True):
|
|
194
|
+
self._device_ready()
|
|
196
195
|
else:
|
|
197
196
|
self._device_activation_status = self._login_device_flow()
|
|
198
197
|
|
|
@@ -383,9 +382,9 @@ class Http:
|
|
|
383
382
|
|
|
384
383
|
Args:
|
|
385
384
|
refresh_token (str | None, optional): The refresh token to use for obtaining a new
|
|
386
|
-
|
|
385
|
+
access token.
|
|
387
386
|
force_refresh (bool, optional): If True, forces a token refresh regardless of
|
|
388
|
-
|
|
387
|
+
expiration. Defaults to False.
|
|
389
388
|
|
|
390
389
|
Returns:
|
|
391
390
|
bool: True if the token was successfully refreshed, False if the refresh failed due
|
|
@@ -394,7 +393,7 @@ class Http:
|
|
|
394
393
|
Raises:
|
|
395
394
|
TadoException: If a connection error occurs during the token refresh process.
|
|
396
395
|
TadoWrongCredentialsException: If the token refresh fails due to invalid credentials
|
|
397
|
-
|
|
396
|
+
and force_refresh is False.
|
|
398
397
|
"""
|
|
399
398
|
|
|
400
399
|
if self._refresh_at >= datetime.now() and not force_refresh:
|
|
@@ -435,7 +434,8 @@ class Http:
|
|
|
435
434
|
return False
|
|
436
435
|
|
|
437
436
|
raise TadoWrongCredentialsException(
|
|
438
|
-
|
|
437
|
+
"Failed to refresh token, probably wrong credentials. "
|
|
438
|
+
f"Status code: {response.status_code}"
|
|
439
439
|
)
|
|
440
440
|
|
|
441
441
|
self._set_oauth_header(response.json())
|
|
@@ -491,7 +491,9 @@ class Http:
|
|
|
491
491
|
raise TadoException(e) from e
|
|
492
492
|
|
|
493
493
|
if response.status_code != 200:
|
|
494
|
-
raise TadoException(
|
|
494
|
+
raise TadoException(
|
|
495
|
+
f"Login failed. Status code: {response.status_code} and reason: {response.reason}"
|
|
496
|
+
)
|
|
495
497
|
|
|
496
498
|
self._device_flow_data = response.json()
|
|
497
499
|
_LOGGER.debug("Device flow response: %s", self._device_flow_data)
|
|
@@ -514,7 +516,9 @@ class Http:
|
|
|
514
516
|
return DeviceActivationStatus.PENDING
|
|
515
517
|
|
|
516
518
|
def _check_device_activation(self) -> bool:
|
|
517
|
-
if self._expires_at is not None and datetime.timestamp(datetime.now()) > datetime.timestamp(
|
|
519
|
+
if self._expires_at is not None and datetime.timestamp(datetime.now()) > datetime.timestamp(
|
|
520
|
+
self._expires_at
|
|
521
|
+
):
|
|
518
522
|
raise TadoException("User took too long to enter key")
|
|
519
523
|
|
|
520
524
|
# Await the desired interval, before polling the API again
|
|
@@ -538,7 +542,10 @@ class Http:
|
|
|
538
542
|
return True
|
|
539
543
|
|
|
540
544
|
# The user has not yet authorized the device, let's continue
|
|
541
|
-
if
|
|
545
|
+
if (
|
|
546
|
+
token_response.status_code == 400
|
|
547
|
+
and token_response.json()["error"] == "authorization_pending"
|
|
548
|
+
):
|
|
542
549
|
_LOGGER.info("Authorization pending, waiting for user to authorize. Continue polling.")
|
|
543
550
|
return False
|
|
544
551
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
PyTado interface implementation for hops.tado.com (Tado X).
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import functools
|
|
5
6
|
import logging
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -11,6 +12,18 @@ from ...logger import Logger
|
|
|
11
12
|
from ...zone import TadoXZone, TadoZone
|
|
12
13
|
from .my_tado import Tado, Timetable
|
|
13
14
|
|
|
15
|
+
|
|
16
|
+
def not_supported(reason):
|
|
17
|
+
def decorator(func):
|
|
18
|
+
@functools.wraps(func)
|
|
19
|
+
def wrapper(*args, **kwargs):
|
|
20
|
+
raise TadoNotSupportedException(f"{func.__name__} is not supported: {reason}")
|
|
21
|
+
|
|
22
|
+
return wrapper
|
|
23
|
+
|
|
24
|
+
return decorator
|
|
25
|
+
|
|
26
|
+
|
|
14
27
|
_LOGGER = Logger(__name__)
|
|
15
28
|
|
|
16
29
|
|
|
@@ -60,9 +73,13 @@ class TadoX(Tado):
|
|
|
60
73
|
devices = [device for room in rooms for device in room["devices"]]
|
|
61
74
|
|
|
62
75
|
for device in devices:
|
|
76
|
+
serial_number = device.get("serialNo", device.get("serialNumber"))
|
|
77
|
+
if not serial_number:
|
|
78
|
+
continue
|
|
79
|
+
|
|
63
80
|
request = TadoXRequest()
|
|
64
81
|
request.domain = Domain.DEVICES
|
|
65
|
-
request.device =
|
|
82
|
+
request.device = serial_number
|
|
66
83
|
device.update(self._http.request(request))
|
|
67
84
|
|
|
68
85
|
if "otherDevices" in rooms_and_devices:
|
|
@@ -108,12 +125,12 @@ class TadoX(Tado):
|
|
|
108
125
|
|
|
109
126
|
return data
|
|
110
127
|
|
|
128
|
+
@not_supported("This method is not currently supported by the Tado X API")
|
|
111
129
|
def get_capabilities(self, zone):
|
|
112
130
|
"""
|
|
113
131
|
Gets current capabilities of zone.
|
|
114
132
|
"""
|
|
115
|
-
|
|
116
|
-
raise TadoNotSupportedException("This method is not currently supported by the Tado X API")
|
|
133
|
+
pass
|
|
117
134
|
|
|
118
135
|
def get_climate(self, zone):
|
|
119
136
|
"""
|
|
@@ -126,6 +143,7 @@ class TadoX(Tado):
|
|
|
126
143
|
"humidity": data["humidity"]["percentage"],
|
|
127
144
|
}
|
|
128
145
|
|
|
146
|
+
@not_supported("Tado X API only support seven days timetable")
|
|
129
147
|
def set_timetable(self, zone: int, timetable: Timetable) -> None:
|
|
130
148
|
"""
|
|
131
149
|
Set the Timetable type currently active
|
|
@@ -133,8 +151,7 @@ class TadoX(Tado):
|
|
|
133
151
|
id = 1 : THREE_DAY (MONDAY_TO_FRIDAY, SATURDAY, SUNDAY)
|
|
134
152
|
id = 3 : SEVEN_DAY (MONDAY, TUESDAY, WEDNESDAY ...)
|
|
135
153
|
"""
|
|
136
|
-
|
|
137
|
-
raise TadoNotSupportedException("Tado X API only support seven days timetable")
|
|
154
|
+
pass
|
|
138
155
|
|
|
139
156
|
def get_schedule(self, zone: int, timetable: Timetable, day=None) -> dict[str, Any]:
|
|
140
157
|
"""
|
|
@@ -249,12 +266,12 @@ class TadoX(Tado):
|
|
|
249
266
|
|
|
250
267
|
return self._http.request(request)
|
|
251
268
|
|
|
269
|
+
@not_supported("Concept of zones is not available by Tado X API, they use rooms")
|
|
252
270
|
def get_zone_overlay_default(self, zone: int):
|
|
253
271
|
"""
|
|
254
272
|
Get current overlay default settings for zone.
|
|
255
273
|
"""
|
|
256
|
-
|
|
257
|
-
raise TadoNotSupportedException("Concept of zones is not available by Tado X API, they use rooms")
|
|
274
|
+
pass
|
|
258
275
|
|
|
259
276
|
def get_open_window_detected(self, zone):
|
|
260
277
|
"""
|
|
@@ -273,15 +290,21 @@ class TadoX(Tado):
|
|
|
273
290
|
Sets the window in zone to open
|
|
274
291
|
Note: This can only be set if an open window was detected in this zone
|
|
275
292
|
"""
|
|
293
|
+
request = TadoXRequest()
|
|
294
|
+
request.command = f"rooms/{zone}/openWindow"
|
|
295
|
+
request.action = Action.SET
|
|
276
296
|
|
|
277
|
-
|
|
297
|
+
return self._http.request(request)
|
|
278
298
|
|
|
279
299
|
def reset_open_window(self, zone):
|
|
280
300
|
"""
|
|
281
301
|
Sets the window in zone to closed
|
|
282
302
|
"""
|
|
303
|
+
request = TadoXRequest()
|
|
304
|
+
request.command = f"rooms/{zone}/openWindow"
|
|
305
|
+
request.action = Action.RESET
|
|
283
306
|
|
|
284
|
-
|
|
307
|
+
return self._http.request(request)
|
|
285
308
|
|
|
286
309
|
def get_device_info(self, device_id, cmd=""):
|
|
287
310
|
"""
|
|
@@ -324,3 +347,30 @@ class TadoX(Tado):
|
|
|
324
347
|
request.payload = {"childLockEnabled": child_lock}
|
|
325
348
|
|
|
326
349
|
self._http.request(request)
|
|
350
|
+
|
|
351
|
+
def set_flow_temperature_optimization(self, max_flow_temperature: float):
|
|
352
|
+
"""
|
|
353
|
+
Set the flow temperature optimization.
|
|
354
|
+
|
|
355
|
+
max_flow_temperature: float, the maximum flow temperature in Celsius
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
request = TadoXRequest()
|
|
359
|
+
request.action = Action.CHANGE
|
|
360
|
+
request.domain = Domain.HOME
|
|
361
|
+
request.command = "settings/flowTemperatureOptimization"
|
|
362
|
+
request.payload = {"maxFlowTemperature": max_flow_temperature}
|
|
363
|
+
|
|
364
|
+
return self._http.request(request)
|
|
365
|
+
|
|
366
|
+
def get_flow_temperature_optimization(self):
|
|
367
|
+
"""
|
|
368
|
+
Get the current flow temperature optimization
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
request = TadoXRequest()
|
|
372
|
+
request.action = Action.GET
|
|
373
|
+
request.domain = Domain.HOME
|
|
374
|
+
request.command = "settings/flowTemperatureOptimization"
|
|
375
|
+
|
|
376
|
+
return self._http.request(request)
|
|
@@ -61,12 +61,11 @@ class Tado:
|
|
|
61
61
|
|
|
62
62
|
Args:
|
|
63
63
|
token_file_path (str | None, optional): Path to a file which will be used to persist
|
|
64
|
-
|
|
64
|
+
the refresh_token token. Defaults to None.
|
|
65
65
|
saved_refresh_token (str | None, optional): A previously saved refresh token.
|
|
66
|
-
|
|
66
|
+
Defaults to None.
|
|
67
67
|
http_session (requests.Session | None, optional): An optional HTTP session to use for
|
|
68
|
-
|
|
69
|
-
Defaults to None.
|
|
68
|
+
requests (can be used in unit tests). Defaults to None.
|
|
70
69
|
debug (bool, optional): Flag to enable or disable debug mode. Defaults to False.
|
|
71
70
|
"""
|
|
72
71
|
|
|
@@ -117,7 +116,9 @@ class Tado:
|
|
|
117
116
|
else:
|
|
118
117
|
self._api = API.Tado(http=self._http, debug=self._debug)
|
|
119
118
|
else:
|
|
120
|
-
raise TadoException(
|
|
119
|
+
raise TadoException(
|
|
120
|
+
"API is not initialized. Please complete device authentication first."
|
|
121
|
+
)
|
|
121
122
|
|
|
122
123
|
# region Deprecated Methods
|
|
123
124
|
# pylint: disable=invalid-name
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file contains anything related to logging
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Logger(logging.Logger):
|
|
9
|
+
"""
|
|
10
|
+
This class provides a custom logger without masking sensitive information.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
class SimpleFormatter(logging.Formatter):
|
|
14
|
+
"""
|
|
15
|
+
Simple formatter that does not remove any information in logs.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, name: str, level=logging.NOTSET):
|
|
19
|
+
super().__init__(name)
|
|
20
|
+
log_sh = logging.StreamHandler()
|
|
21
|
+
log_fmt = self.SimpleFormatter(fmt="%(name)s :: %(levelname)-8s :: %(message)s")
|
|
22
|
+
log_sh.setFormatter(log_fmt)
|
|
23
|
+
self.addHandler(log_sh)
|
|
24
|
+
self.setLevel(level)
|