vaillant-netatmo-api-ng 0.1.0__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.
- vaillant_netatmo_api_ng-0.1.0/LICENSE +22 -0
- vaillant_netatmo_api_ng-0.1.0/PKG-INFO +198 -0
- vaillant_netatmo_api_ng-0.1.0/README.md +178 -0
- vaillant_netatmo_api_ng-0.1.0/pyproject.toml +39 -0
- vaillant_netatmo_api_ng-0.1.0/setup.cfg +4 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/__init__.py +79 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/auth.py +79 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/auth_test.py +65 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/base.py +83 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/errors.py +129 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/errors_test.py +97 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/home.py +75 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/homestatus.py +86 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/thermostat.py +897 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/thermostat_auth.py +107 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/thermostat_home_test.py +117 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/thermostat_program_test.py +180 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/thermostat_test.py +423 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/time.py +7 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/token.py +105 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/token_test.py +69 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api_ng.egg-info/PKG-INFO +198 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api_ng.egg-info/SOURCES.txt +26 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api_ng.egg-info/dependency_links.txt +1 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api_ng.egg-info/requires.txt +2 -0
- vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api_ng.egg-info/top_level.txt +1 -0
- vaillant_netatmo_api_ng-0.1.0/tests/test_home_room_module_ids.py +12 -0
- vaillant_netatmo_api_ng-0.1.0/tests/test_homestatus.py +94 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Mislav Mandarić
|
|
4
|
+
Copyright (c) 2026 LEOSCHUMY (NG fork modifications)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vaillant-netatmo-api-ng
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Community fork for managing Vaillant thermostats using Netatmo-backed APIs.
|
|
5
|
+
Author: Mislav Mandaric
|
|
6
|
+
Maintainer: LEOSCHUMY
|
|
7
|
+
Project-URL: Homepage, https://github.com/LEOSCHUMY/vaillant-netatmo-api-NG
|
|
8
|
+
Project-URL: Source, https://github.com/LEOSCHUMY/vaillant-netatmo-api-NG
|
|
9
|
+
Project-URL: Issues, https://github.com/LEOSCHUMY/vaillant-netatmo-api-NG/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/LEOSCHUMY/vaillant-netatmo-api-NG/blob/master/CHANGELOG.md
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: httpx>=0.18.2
|
|
18
|
+
Requires-Dist: tenacity>=8.0.1
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# vaillant-netatmo-api-ng
|
|
22
|
+
|
|
23
|
+
[](https://github.com/LEOSCHUMY/vaillant-netatmo-api-NG/actions/workflows/tests.yml)
|
|
24
|
+
|
|
25
|
+
## About this fork
|
|
26
|
+
|
|
27
|
+
**vaillant-netatmo-api-ng** is a community-maintained fork of [Mislav Mandarić's vaillant-netatmo-api](https://github.com/MislavMandaric/vaillant-netatmo-api), a Python library for accessing Vaillant vSMART/eRELAX thermostats through Netatmo-backed APIs.
|
|
28
|
+
|
|
29
|
+
This fork starts its **own version series at 0.1.0**. Version numbers do **not** track the original project's releases. The initial NG work adds read-only room temperatures from Vaillant's `homestatus` endpoint, without replacing existing thermostat data or control APIs.
|
|
30
|
+
|
|
31
|
+
> [!IMPORTANT]
|
|
32
|
+
> **Independent, unofficial project — NOT affiliated with, authorized, sponsored, or endorsed by Vaillant or Netatmo.**
|
|
33
|
+
> This community fork is not an official Vaillant or Netatmo product, and neither company provides support for it. Their APIs may change or become unavailable without notice.
|
|
34
|
+
|
|
35
|
+
This fork is also independent of the original project's maintainer. See [LICENSE](LICENSE) for the MIT license and original copyright notice.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- Existing authenticated thermostat data access and heating controls from the upstream library.
|
|
40
|
+
- `ThermostatClient.async_get_homes_data()` for homes, rooms and `Room.module_ids`.
|
|
41
|
+
- **NEW:** `ThermostatClient.async_get_home_status(home_id)` for room status (including measured temperature, target temperature and reachability) keyed by room ID.
|
|
42
|
+
- Defensive validation for malformed responses and duplicate or conflicting room records.
|
|
43
|
+
- No fabricated measurement timestamp: the `homestatus` response does not provide a verified temperature acquisition time.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
Once published on PyPI, the NG package can be installed with:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install vaillant-netatmo-api-ng==0.1.0
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Publication status:** Version 0.1.0 is prepared in this repository but is **not yet confirmed as published on PyPI**. Until release, use the [source repository](https://github.com/LEOSCHUMY/vaillant-netatmo-api-NG) and a branch/commit-specific installation for development.
|
|
54
|
+
|
|
55
|
+
The **distribution name** is `vaillant-netatmo-api-ng`, but the Python **import name** intentionally remains `vaillant_netatmo_api` for compatibility:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from vaillant_netatmo_api.thermostat import ThermostatClient
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Do not install `vaillant-netatmo-api` and `vaillant-netatmo-api-ng` in the same environment; they provide the same import package and may overwrite each other's files.
|
|
62
|
+
|
|
63
|
+
Requires Python 3.8+ and the `httpx` and `tenacity` dependencies.
|
|
64
|
+
|
|
65
|
+
## API usage
|
|
66
|
+
|
|
67
|
+
### Reading room temperatures
|
|
68
|
+
|
|
69
|
+
With an existing authenticated `ThermostatClient` instance:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
homes = await client.async_get_homes_data()
|
|
73
|
+
for home in homes:
|
|
74
|
+
statuses = await client.async_get_home_status(home.id)
|
|
75
|
+
for room in home.rooms:
|
|
76
|
+
room_status = statuses.get(room.id)
|
|
77
|
+
if room_status is not None:
|
|
78
|
+
print(room_status.measured_temperature)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Use `homesdata` `rooms[].module_ids` to map room readings to the right thermostat module; do not assume the first room represents every thermostat. If `homestatus` cannot provide a valid room reading, applications should retain their previous measured-temperature fallback. This library does not change polling intervals for consumers.
|
|
82
|
+
|
|
83
|
+
### Existing authentication and thermostat operations
|
|
84
|
+
|
|
85
|
+
### Getting the token from the OAuth API
|
|
86
|
+
|
|
87
|
+
All Netatmo APIs are protected and require a bearer token to authenticate. To get this token, Netatmo offers an OAuth API.
|
|
88
|
+
|
|
89
|
+
Since Vaillant uses Resource Owner Password Credentials Grant, there is only one method in the `AuthClient` API:
|
|
90
|
+
|
|
91
|
+
* `async_token`: getting a bearer token and storing it in the token store
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from vaillant_netatmo_api import auth_client
|
|
95
|
+
|
|
96
|
+
CLIENT_ID = ""
|
|
97
|
+
CLIENT_SECRET = ""
|
|
98
|
+
|
|
99
|
+
def handle_token_update(token):
|
|
100
|
+
token_string = token.serialize()
|
|
101
|
+
write_to_storage(token_string)
|
|
102
|
+
|
|
103
|
+
async with auth_client(CLIENT_ID, CLIENT_SECRET, handle_token_update) as client:
|
|
104
|
+
await client.async_token(
|
|
105
|
+
username,
|
|
106
|
+
password,
|
|
107
|
+
user_prefix,
|
|
108
|
+
app_version,
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Accessing the Thermostat API
|
|
113
|
+
|
|
114
|
+
The existing `ThermostatClient` methods include:
|
|
115
|
+
|
|
116
|
+
* `async_get_thermostats_data`: getting all the devices associated with the user account
|
|
117
|
+
* `async_set_system_mode`: changing system mode for a device and module (ie. summer, winter or frostguard)
|
|
118
|
+
* `async_set_minor_mode`: changing minor mode for a device and module (ie. manual mode, away mode or hot water boost mode)
|
|
119
|
+
* `async_sync_schedule`: updating schedule data for a device and module
|
|
120
|
+
* `async_switch_schedule`: changing active schedule for a device and module
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from vaillant_netatmo_api import thermostat_client, SystemMode, Token
|
|
124
|
+
|
|
125
|
+
CLIENT_ID = ""
|
|
126
|
+
CLIENT_SECRET = ""
|
|
127
|
+
|
|
128
|
+
token_string = read_from_storage()
|
|
129
|
+
token = Token.deserialize(token_string)
|
|
130
|
+
|
|
131
|
+
def handle_token_update(token):
|
|
132
|
+
token_string = token.serialize()
|
|
133
|
+
write_to_storage(token_string)
|
|
134
|
+
|
|
135
|
+
async with thermostat_client(CLIENT_ID, CLIENT_SECRET, token, handle_token_update) as client:
|
|
136
|
+
devices = await client.async_get_thermostats_data()
|
|
137
|
+
|
|
138
|
+
d_id = devices[0].id
|
|
139
|
+
m_id = devices[0].modules[0].id
|
|
140
|
+
|
|
141
|
+
await client.async_set_system_mode(d_id, m_id, SystemMode.WINTER)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Using clients as singletons
|
|
145
|
+
|
|
146
|
+
Even though library offers context manager for using `AuthClient` and `ThermostatClient`, this should only be done during development or in very infrequent usage scenarios.
|
|
147
|
+
|
|
148
|
+
Both of the clients use `httpx.AsyncClient` as the underlying HTTP communication library, which implements connection pooling and connection reuse. This means doing multiple concurent requests should be done by using the same instance of the `AuthClient` or `ThermostatClient`, which is not possible by using the context manager API since this API will return new instance of the client every time `auth_client` or `thermostat_client` method is called.
|
|
149
|
+
|
|
150
|
+
To achieve optimal usage, which will utilize connection pooling and connection reuse, both `AuthClient` and `ThermostatClient` should be used by instantiating the clients and providing `httpx.AsyncClient` instance in a constructor. This provided client should be used as singleton, or with some other context management mechanism, with the context wider than one block of code or one inbound request.
|
|
151
|
+
|
|
152
|
+
Here is an example for usage in Home Assistant.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# When setting up integration with all the devices of one account, instantiate and store the client in a configuration memory store
|
|
156
|
+
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
157
|
+
client = get_async_client(hass)
|
|
158
|
+
token_store = TokenStore(
|
|
159
|
+
entry.data[CONF_CLIENT_ID],
|
|
160
|
+
entry.data[CONF_CLIENT_SECRET],
|
|
161
|
+
token,
|
|
162
|
+
handle_token_update,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
hass.data[DOMAIN][entry.entry_id] = ThermostatClient(client, token_store)
|
|
166
|
+
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
|
167
|
+
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
# When unloading the integration of this same account, read the client and close it manually
|
|
171
|
+
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
172
|
+
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
173
|
+
if unload_ok:
|
|
174
|
+
hass.data[DOMAIN].pop(entry.entry_id)
|
|
175
|
+
|
|
176
|
+
return unload_ok
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Similar hooks which represent some kind of application context should be used when integrating this library into a different application (Flask, Django or similar).
|
|
180
|
+
|
|
181
|
+
## Acknowledgements
|
|
182
|
+
|
|
183
|
+
The original library was created by **Mislav Mandarić**, who retains the original copyright and attribution. This NG fork builds on that work and preserves its MIT license.
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
This library would not exist if it weren't for previous implementations by the following projects:
|
|
187
|
+
|
|
188
|
+
* https://github.com/philippelt/netatmo-api-python
|
|
189
|
+
* https://github.com/jabesq/netatmo-api-python
|
|
190
|
+
* https://github.com/samueldumont/netatmo-api-python
|
|
191
|
+
* https://github.com/jabesq/pyatmo
|
|
192
|
+
* https://github.com/pjmaenh/pyvaillant
|
|
193
|
+
|
|
194
|
+
They laid out the foundation by exploring and documenting the APIs.
|
|
195
|
+
|
|
196
|
+
## Disclaimers
|
|
197
|
+
|
|
198
|
+
**This is an unofficial, independent community project. It is not affiliated with, authorized, sponsored, or endorsed by Vaillant or Netatmo.** The project is provided without warranties or official support. Changes to either company's APIs, including restrictions on third-party access, may cause some or all features to stop working.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# vaillant-netatmo-api-ng
|
|
2
|
+
|
|
3
|
+
[](https://github.com/LEOSCHUMY/vaillant-netatmo-api-NG/actions/workflows/tests.yml)
|
|
4
|
+
|
|
5
|
+
## About this fork
|
|
6
|
+
|
|
7
|
+
**vaillant-netatmo-api-ng** is a community-maintained fork of [Mislav Mandarić's vaillant-netatmo-api](https://github.com/MislavMandaric/vaillant-netatmo-api), a Python library for accessing Vaillant vSMART/eRELAX thermostats through Netatmo-backed APIs.
|
|
8
|
+
|
|
9
|
+
This fork starts its **own version series at 0.1.0**. Version numbers do **not** track the original project's releases. The initial NG work adds read-only room temperatures from Vaillant's `homestatus` endpoint, without replacing existing thermostat data or control APIs.
|
|
10
|
+
|
|
11
|
+
> [!IMPORTANT]
|
|
12
|
+
> **Independent, unofficial project — NOT affiliated with, authorized, sponsored, or endorsed by Vaillant or Netatmo.**
|
|
13
|
+
> This community fork is not an official Vaillant or Netatmo product, and neither company provides support for it. Their APIs may change or become unavailable without notice.
|
|
14
|
+
|
|
15
|
+
This fork is also independent of the original project's maintainer. See [LICENSE](LICENSE) for the MIT license and original copyright notice.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- Existing authenticated thermostat data access and heating controls from the upstream library.
|
|
20
|
+
- `ThermostatClient.async_get_homes_data()` for homes, rooms and `Room.module_ids`.
|
|
21
|
+
- **NEW:** `ThermostatClient.async_get_home_status(home_id)` for room status (including measured temperature, target temperature and reachability) keyed by room ID.
|
|
22
|
+
- Defensive validation for malformed responses and duplicate or conflicting room records.
|
|
23
|
+
- No fabricated measurement timestamp: the `homestatus` response does not provide a verified temperature acquisition time.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Once published on PyPI, the NG package can be installed with:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install vaillant-netatmo-api-ng==0.1.0
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Publication status:** Version 0.1.0 is prepared in this repository but is **not yet confirmed as published on PyPI**. Until release, use the [source repository](https://github.com/LEOSCHUMY/vaillant-netatmo-api-NG) and a branch/commit-specific installation for development.
|
|
34
|
+
|
|
35
|
+
The **distribution name** is `vaillant-netatmo-api-ng`, but the Python **import name** intentionally remains `vaillant_netatmo_api` for compatibility:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from vaillant_netatmo_api.thermostat import ThermostatClient
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Do not install `vaillant-netatmo-api` and `vaillant-netatmo-api-ng` in the same environment; they provide the same import package and may overwrite each other's files.
|
|
42
|
+
|
|
43
|
+
Requires Python 3.8+ and the `httpx` and `tenacity` dependencies.
|
|
44
|
+
|
|
45
|
+
## API usage
|
|
46
|
+
|
|
47
|
+
### Reading room temperatures
|
|
48
|
+
|
|
49
|
+
With an existing authenticated `ThermostatClient` instance:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
homes = await client.async_get_homes_data()
|
|
53
|
+
for home in homes:
|
|
54
|
+
statuses = await client.async_get_home_status(home.id)
|
|
55
|
+
for room in home.rooms:
|
|
56
|
+
room_status = statuses.get(room.id)
|
|
57
|
+
if room_status is not None:
|
|
58
|
+
print(room_status.measured_temperature)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use `homesdata` `rooms[].module_ids` to map room readings to the right thermostat module; do not assume the first room represents every thermostat. If `homestatus` cannot provide a valid room reading, applications should retain their previous measured-temperature fallback. This library does not change polling intervals for consumers.
|
|
62
|
+
|
|
63
|
+
### Existing authentication and thermostat operations
|
|
64
|
+
|
|
65
|
+
### Getting the token from the OAuth API
|
|
66
|
+
|
|
67
|
+
All Netatmo APIs are protected and require a bearer token to authenticate. To get this token, Netatmo offers an OAuth API.
|
|
68
|
+
|
|
69
|
+
Since Vaillant uses Resource Owner Password Credentials Grant, there is only one method in the `AuthClient` API:
|
|
70
|
+
|
|
71
|
+
* `async_token`: getting a bearer token and storing it in the token store
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from vaillant_netatmo_api import auth_client
|
|
75
|
+
|
|
76
|
+
CLIENT_ID = ""
|
|
77
|
+
CLIENT_SECRET = ""
|
|
78
|
+
|
|
79
|
+
def handle_token_update(token):
|
|
80
|
+
token_string = token.serialize()
|
|
81
|
+
write_to_storage(token_string)
|
|
82
|
+
|
|
83
|
+
async with auth_client(CLIENT_ID, CLIENT_SECRET, handle_token_update) as client:
|
|
84
|
+
await client.async_token(
|
|
85
|
+
username,
|
|
86
|
+
password,
|
|
87
|
+
user_prefix,
|
|
88
|
+
app_version,
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Accessing the Thermostat API
|
|
93
|
+
|
|
94
|
+
The existing `ThermostatClient` methods include:
|
|
95
|
+
|
|
96
|
+
* `async_get_thermostats_data`: getting all the devices associated with the user account
|
|
97
|
+
* `async_set_system_mode`: changing system mode for a device and module (ie. summer, winter or frostguard)
|
|
98
|
+
* `async_set_minor_mode`: changing minor mode for a device and module (ie. manual mode, away mode or hot water boost mode)
|
|
99
|
+
* `async_sync_schedule`: updating schedule data for a device and module
|
|
100
|
+
* `async_switch_schedule`: changing active schedule for a device and module
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from vaillant_netatmo_api import thermostat_client, SystemMode, Token
|
|
104
|
+
|
|
105
|
+
CLIENT_ID = ""
|
|
106
|
+
CLIENT_SECRET = ""
|
|
107
|
+
|
|
108
|
+
token_string = read_from_storage()
|
|
109
|
+
token = Token.deserialize(token_string)
|
|
110
|
+
|
|
111
|
+
def handle_token_update(token):
|
|
112
|
+
token_string = token.serialize()
|
|
113
|
+
write_to_storage(token_string)
|
|
114
|
+
|
|
115
|
+
async with thermostat_client(CLIENT_ID, CLIENT_SECRET, token, handle_token_update) as client:
|
|
116
|
+
devices = await client.async_get_thermostats_data()
|
|
117
|
+
|
|
118
|
+
d_id = devices[0].id
|
|
119
|
+
m_id = devices[0].modules[0].id
|
|
120
|
+
|
|
121
|
+
await client.async_set_system_mode(d_id, m_id, SystemMode.WINTER)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Using clients as singletons
|
|
125
|
+
|
|
126
|
+
Even though library offers context manager for using `AuthClient` and `ThermostatClient`, this should only be done during development or in very infrequent usage scenarios.
|
|
127
|
+
|
|
128
|
+
Both of the clients use `httpx.AsyncClient` as the underlying HTTP communication library, which implements connection pooling and connection reuse. This means doing multiple concurent requests should be done by using the same instance of the `AuthClient` or `ThermostatClient`, which is not possible by using the context manager API since this API will return new instance of the client every time `auth_client` or `thermostat_client` method is called.
|
|
129
|
+
|
|
130
|
+
To achieve optimal usage, which will utilize connection pooling and connection reuse, both `AuthClient` and `ThermostatClient` should be used by instantiating the clients and providing `httpx.AsyncClient` instance in a constructor. This provided client should be used as singleton, or with some other context management mechanism, with the context wider than one block of code or one inbound request.
|
|
131
|
+
|
|
132
|
+
Here is an example for usage in Home Assistant.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
# When setting up integration with all the devices of one account, instantiate and store the client in a configuration memory store
|
|
136
|
+
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
137
|
+
client = get_async_client(hass)
|
|
138
|
+
token_store = TokenStore(
|
|
139
|
+
entry.data[CONF_CLIENT_ID],
|
|
140
|
+
entry.data[CONF_CLIENT_SECRET],
|
|
141
|
+
token,
|
|
142
|
+
handle_token_update,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
hass.data[DOMAIN][entry.entry_id] = ThermostatClient(client, token_store)
|
|
146
|
+
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
|
147
|
+
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
# When unloading the integration of this same account, read the client and close it manually
|
|
151
|
+
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
152
|
+
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
153
|
+
if unload_ok:
|
|
154
|
+
hass.data[DOMAIN].pop(entry.entry_id)
|
|
155
|
+
|
|
156
|
+
return unload_ok
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Similar hooks which represent some kind of application context should be used when integrating this library into a different application (Flask, Django or similar).
|
|
160
|
+
|
|
161
|
+
## Acknowledgements
|
|
162
|
+
|
|
163
|
+
The original library was created by **Mislav Mandarić**, who retains the original copyright and attribution. This NG fork builds on that work and preserves its MIT license.
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
This library would not exist if it weren't for previous implementations by the following projects:
|
|
167
|
+
|
|
168
|
+
* https://github.com/philippelt/netatmo-api-python
|
|
169
|
+
* https://github.com/jabesq/netatmo-api-python
|
|
170
|
+
* https://github.com/samueldumont/netatmo-api-python
|
|
171
|
+
* https://github.com/jabesq/pyatmo
|
|
172
|
+
* https://github.com/pjmaenh/pyvaillant
|
|
173
|
+
|
|
174
|
+
They laid out the foundation by exploring and documenting the APIs.
|
|
175
|
+
|
|
176
|
+
## Disclaimers
|
|
177
|
+
|
|
178
|
+
**This is an unofficial, independent community project. It is not affiliated with, authorized, sponsored, or endorsed by Vaillant or Netatmo.** The project is provided without warranties or official support. Changes to either company's APIs, including restrictions on third-party access, may cause some or all features to stop working.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"setuptools",
|
|
4
|
+
]
|
|
5
|
+
build-backend = "setuptools.build_meta"
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "vaillant-netatmo-api-ng"
|
|
9
|
+
version = "0.1.0"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name="Mislav Mandaric" },
|
|
12
|
+
]
|
|
13
|
+
description = "Community fork for managing Vaillant thermostats using Netatmo-backed APIs."
|
|
14
|
+
readme = "README.md"
|
|
15
|
+
maintainers = [
|
|
16
|
+
{ name="LEOSCHUMY" },
|
|
17
|
+
]
|
|
18
|
+
requires-python = ">=3.8"
|
|
19
|
+
dependencies = [
|
|
20
|
+
"httpx>=0.18.2",
|
|
21
|
+
"tenacity>=8.0.1",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/LEOSCHUMY/vaillant-netatmo-api-NG"
|
|
31
|
+
Source = "https://github.com/LEOSCHUMY/vaillant-netatmo-api-NG"
|
|
32
|
+
Issues = "https://github.com/LEOSCHUMY/vaillant-netatmo-api-NG/issues"
|
|
33
|
+
Changelog = "https://github.com/LEOSCHUMY/vaillant-netatmo-api-NG/blob/master/CHANGELOG.md"
|
|
34
|
+
|
|
35
|
+
[tool.semantic_release]
|
|
36
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
37
|
+
build_command = "pip install build && python -m build"
|
|
38
|
+
commit_message = "chore(release): {version} [skip ci]"
|
|
39
|
+
major_on_zero = false
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Module HTTP communication with the Netatmo API."""
|
|
2
|
+
|
|
3
|
+
from .auth import AuthClient, auth_client
|
|
4
|
+
from .errors import (
|
|
5
|
+
ApiException,
|
|
6
|
+
NonOkResponseException,
|
|
7
|
+
NetworkException,
|
|
8
|
+
NetworkTimeoutException,
|
|
9
|
+
NonRetryableException,
|
|
10
|
+
RequestBackoffException,
|
|
11
|
+
RequestClientException,
|
|
12
|
+
RequestException,
|
|
13
|
+
RequestServerException,
|
|
14
|
+
RequestUnauthorizedException,
|
|
15
|
+
RetryableException,
|
|
16
|
+
UnsuportedArgumentsException,
|
|
17
|
+
)
|
|
18
|
+
from .home import (
|
|
19
|
+
Home,
|
|
20
|
+
Room,
|
|
21
|
+
TemperatureControlMode,
|
|
22
|
+
ThermMode,
|
|
23
|
+
ThermSetpointMode,
|
|
24
|
+
)
|
|
25
|
+
from .thermostat import (
|
|
26
|
+
Device,
|
|
27
|
+
Measured,
|
|
28
|
+
MeasurementItem,
|
|
29
|
+
MeasurementScale,
|
|
30
|
+
MeasurementType,
|
|
31
|
+
Module,
|
|
32
|
+
Program,
|
|
33
|
+
Setpoint,
|
|
34
|
+
SetpointMode,
|
|
35
|
+
SystemMode,
|
|
36
|
+
ThermostatClient,
|
|
37
|
+
TimeSlot,
|
|
38
|
+
Zone,
|
|
39
|
+
thermostat_client,
|
|
40
|
+
)
|
|
41
|
+
from .token import Token, TokenStore
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"AuthClient",
|
|
45
|
+
"ThermostatClient",
|
|
46
|
+
"ApiException",
|
|
47
|
+
"NonOkResponseException",
|
|
48
|
+
"NetworkException",
|
|
49
|
+
"NetworkTimeoutException",
|
|
50
|
+
"NonRetryableException",
|
|
51
|
+
"RequestBackoffException",
|
|
52
|
+
"RequestClientException",
|
|
53
|
+
"RequestException",
|
|
54
|
+
"RequestServerException",
|
|
55
|
+
"RequestUnauthorizedException",
|
|
56
|
+
"RetryableException",
|
|
57
|
+
"UnsuportedArgumentsException",
|
|
58
|
+
"Home",
|
|
59
|
+
"Room",
|
|
60
|
+
"TemperatureControlMode",
|
|
61
|
+
"ThermMode",
|
|
62
|
+
"ThermSetpointMode",
|
|
63
|
+
"Device",
|
|
64
|
+
"Module",
|
|
65
|
+
"Program",
|
|
66
|
+
"Zone",
|
|
67
|
+
"TimeSlot",
|
|
68
|
+
"Setpoint",
|
|
69
|
+
"Measured",
|
|
70
|
+
"MeasurementItem",
|
|
71
|
+
"MeasurementType",
|
|
72
|
+
"MeasurementScale",
|
|
73
|
+
"SystemMode",
|
|
74
|
+
"SetpointMode",
|
|
75
|
+
"Token",
|
|
76
|
+
"TokenStore",
|
|
77
|
+
"auth_client",
|
|
78
|
+
"thermostat_client",
|
|
79
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Module containing an AuthClient for the Netatmo API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from typing import AsyncGenerator, Callable
|
|
7
|
+
|
|
8
|
+
from httpx import AsyncClient
|
|
9
|
+
|
|
10
|
+
from .base import BaseClient
|
|
11
|
+
from .token import Token, TokenStore
|
|
12
|
+
|
|
13
|
+
_TOKEN_PATH = "oauth2/token"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@asynccontextmanager
|
|
17
|
+
async def auth_client(
|
|
18
|
+
client_id: str,
|
|
19
|
+
client_secret: str,
|
|
20
|
+
on_token_update: Callable[[Token], None],
|
|
21
|
+
) -> AsyncGenerator[AuthClient, None]:
|
|
22
|
+
client = AsyncClient()
|
|
23
|
+
token_store = TokenStore(client_id, client_secret, None, on_token_update)
|
|
24
|
+
|
|
25
|
+
c = AuthClient(client, token_store)
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
yield c
|
|
29
|
+
finally:
|
|
30
|
+
await client.aclose()
|
|
31
|
+
|
|
32
|
+
class AuthClient(BaseClient):
|
|
33
|
+
"""
|
|
34
|
+
Client for making HTTP requests to the Netatmo API. Used for the subset of the API related to the authentication: getting the access and refresh tokens.
|
|
35
|
+
|
|
36
|
+
Uses BaseClient as a basis for making HTTP requests.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
client: AsyncClient,
|
|
42
|
+
token_store: TokenStore,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Create new auth client instance.
|
|
46
|
+
|
|
47
|
+
Uses the provided parameters to instantiate the BaseClient class.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
self._token_store = token_store
|
|
51
|
+
super().__init__(client, None)
|
|
52
|
+
|
|
53
|
+
async def async_token(
|
|
54
|
+
self,
|
|
55
|
+
username: str,
|
|
56
|
+
password: str,
|
|
57
|
+
user_prefix: str,
|
|
58
|
+
app_version: str,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Get the access and refresh tokens from the Netatmo API which can be used for making requests towards all other protected APIs. Uses the resource owner password credentials grant, with custom Vaillant parameters - user prefix and app version.
|
|
62
|
+
|
|
63
|
+
On success, returns nothing. On error, throws an exception.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
data = {
|
|
67
|
+
"username": username,
|
|
68
|
+
"password": password,
|
|
69
|
+
"user_prefix": user_prefix,
|
|
70
|
+
"app_version": app_version,
|
|
71
|
+
}
|
|
72
|
+
data.update(self._token_store.access_token_request)
|
|
73
|
+
|
|
74
|
+
body = await self._post(
|
|
75
|
+
_TOKEN_PATH,
|
|
76
|
+
data=data,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
self._token_store.token = Token(body)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from pytest_mock import MockerFixture
|
|
5
|
+
from respx import MockRouter
|
|
6
|
+
|
|
7
|
+
from vaillant_netatmo_api.auth import auth_client
|
|
8
|
+
from vaillant_netatmo_api.errors import RequestClientException
|
|
9
|
+
from vaillant_netatmo_api.token import Token
|
|
10
|
+
|
|
11
|
+
get_token_request = {
|
|
12
|
+
"username": "user",
|
|
13
|
+
"password": "pass",
|
|
14
|
+
"user_prefix": "prefix",
|
|
15
|
+
"app_version": "version",
|
|
16
|
+
"grant_type": "password",
|
|
17
|
+
"client_id": "client",
|
|
18
|
+
"client_secret": "secret",
|
|
19
|
+
"scope": "all_scopes",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get_token_response = {
|
|
23
|
+
"access_token": "12345",
|
|
24
|
+
"refresh_token": "abcde",
|
|
25
|
+
"expires_at": "",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.mark.asyncio
|
|
30
|
+
class TestAuth:
|
|
31
|
+
async def test_async_get_token__invalid_request_params__raises_error(self, respx_mock: MockRouter):
|
|
32
|
+
respx_mock.post("https://app.netatmo.net/oauth2/token",
|
|
33
|
+
data=get_token_request).respond(400)
|
|
34
|
+
|
|
35
|
+
async with auth_client(get_token_request["client_id"], get_token_request["client_secret"], None) as client:
|
|
36
|
+
with pytest.raises(RequestClientException):
|
|
37
|
+
await client.async_token(get_token_request["username"], get_token_request["password"], get_token_request["user_prefix"], get_token_request["app_version"])
|
|
38
|
+
|
|
39
|
+
async def test_async_get_token__server_errors__retry_until_success(self, respx_mock: MockRouter, mocker: MockerFixture):
|
|
40
|
+
respx_mock.post("https://app.netatmo.net/oauth2/token", data=get_token_request).mock(side_effect=[
|
|
41
|
+
httpx.Response(500),
|
|
42
|
+
httpx.Response(200, json=get_token_response),
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
on_token_update_stub = mocker.stub()
|
|
46
|
+
|
|
47
|
+
async with auth_client(get_token_request["client_id"], get_token_request["client_secret"], on_token_update_stub) as client:
|
|
48
|
+
await client.async_token(get_token_request["username"], get_token_request["password"], get_token_request["user_prefix"], get_token_request["app_version"])
|
|
49
|
+
|
|
50
|
+
assert respx_mock.calls.call_count == 2
|
|
51
|
+
on_token_update_stub.assert_called_once_with(
|
|
52
|
+
Token(get_token_response))
|
|
53
|
+
|
|
54
|
+
async def test_async_get_token__valid_request_params__returns_valid_oauth_token(self, respx_mock: MockRouter, mocker: MockerFixture):
|
|
55
|
+
respx_mock.post("https://app.netatmo.net/oauth2/token",
|
|
56
|
+
data=get_token_request).respond(200, json=get_token_response)
|
|
57
|
+
|
|
58
|
+
on_token_update_stub = mocker.stub()
|
|
59
|
+
|
|
60
|
+
async with auth_client(get_token_request["client_id"], get_token_request["client_secret"], on_token_update_stub) as client:
|
|
61
|
+
await client.async_token(get_token_request["username"], get_token_request["password"], get_token_request["user_prefix"], get_token_request["app_version"])
|
|
62
|
+
|
|
63
|
+
assert respx_mock.calls.call_count == 1
|
|
64
|
+
on_token_update_stub.assert_called_once_with(
|
|
65
|
+
Token(get_token_response))
|