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.
Files changed (28) hide show
  1. vaillant_netatmo_api_ng-0.1.0/LICENSE +22 -0
  2. vaillant_netatmo_api_ng-0.1.0/PKG-INFO +198 -0
  3. vaillant_netatmo_api_ng-0.1.0/README.md +178 -0
  4. vaillant_netatmo_api_ng-0.1.0/pyproject.toml +39 -0
  5. vaillant_netatmo_api_ng-0.1.0/setup.cfg +4 -0
  6. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/__init__.py +79 -0
  7. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/auth.py +79 -0
  8. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/auth_test.py +65 -0
  9. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/base.py +83 -0
  10. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/errors.py +129 -0
  11. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/errors_test.py +97 -0
  12. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/home.py +75 -0
  13. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/homestatus.py +86 -0
  14. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/thermostat.py +897 -0
  15. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/thermostat_auth.py +107 -0
  16. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/thermostat_home_test.py +117 -0
  17. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/thermostat_program_test.py +180 -0
  18. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/thermostat_test.py +423 -0
  19. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/time.py +7 -0
  20. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/token.py +105 -0
  21. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api/token_test.py +69 -0
  22. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api_ng.egg-info/PKG-INFO +198 -0
  23. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api_ng.egg-info/SOURCES.txt +26 -0
  24. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api_ng.egg-info/dependency_links.txt +1 -0
  25. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api_ng.egg-info/requires.txt +2 -0
  26. vaillant_netatmo_api_ng-0.1.0/src/vaillant_netatmo_api_ng.egg-info/top_level.txt +1 -0
  27. vaillant_netatmo_api_ng-0.1.0/tests/test_home_room_module_ids.py +12 -0
  28. 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
+ [![Tests](https://github.com/LEOSCHUMY/vaillant-netatmo-api-NG/actions/workflows/tests.yml/badge.svg)](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
+ [![Tests](https://github.com/LEOSCHUMY/vaillant-netatmo-api-NG/actions/workflows/tests.yml/badge.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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))