dimplex-controller 0.5.0__tar.gz → 0.6.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.
@@ -0,0 +1,290 @@
1
+ Metadata-Version: 2.4
2
+ Name: dimplex-controller
3
+ Version: 0.6.1
4
+ Summary: Python client for Dimplex heating controllers (GDHV IoT)
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: dimplex,heating,control,iot,asyncio
8
+ Author: Kieran Roper
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Home Automation
22
+ Requires-Dist: aiohttp (>=3.9.0,<4.0.0)
23
+ Requires-Dist: beautifulsoup4 (>=4.14.3,<5.0.0)
24
+ Requires-Dist: pydantic (>=2.0.0,<3.0.0)
25
+ Requires-Dist: python-dotenv (>=1.2.1,<2.0.0)
26
+ Project-URL: Homepage, https://github.com/KRoperUK/dimplex-controller-py
27
+ Project-URL: Repository, https://github.com/KRoperUK/dimplex-controller-py
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Dimplex Controller Python Client
31
+
32
+ [![PyPI version](https://img.shields.io/pypi/v/dimplex-controller.svg)](https://pypi.org/project/dimplex-controller/)
33
+ [![Python versions](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
35
+ [![CI Tests](https://github.com/KRoperUK/dimplex-controller-py/actions/workflows/tests.yml/badge.svg)](https://github.com/KRoperUK/dimplex-controller-py/actions)
36
+ [![Downloads](https://img.shields.io/pypi/dm/dimplex-controller.svg)](https://pypi.org/project/dimplex-controller/)
37
+
38
+ <p align="center">
39
+ <strong>Async Python client for controlling Glen Dimplex Heating &amp; Ventilation (GDHV) appliances via the Dimplex cloud API.</strong>
40
+ </p>
41
+
42
+ ---
43
+
44
+ ## What does this do?
45
+
46
+ `dimplex-controller-py` is an asynchronous Python client that talks to the GDHV IoT cloud platform. It handles Azure B2C authentication (including automatic token refresh), discovers your Hubs, Zones and Appliances, and lets you read telemetry and send control commands — all from a script or a larger application.
47
+
48
+ It is the engine behind the [Dimplex Hub Home Assistant integration](https://github.com/KRoperUK/dimplex-controller-hass) and is published to PyPI as [`dimplex-controller`](https://pypi.org/project/dimplex-controller/).
49
+
50
+ > **Note:** This is an unofficial library and is not affiliated with or endorsed by Glen Dimplex Heating & Ventilation (GDHV). Use it at your own risk.
51
+
52
+ ## Contents
53
+
54
+ - [Features](#features)
55
+ - [Installation](#installation)
56
+ - [Quick start](#quick-start)
57
+ - [Usage guide](#usage-guide)
58
+ - [Authentication](#authentication)
59
+ - [Discovery](#discovery)
60
+ - [Reading status](#reading-status)
61
+ - [Sending control commands](#sending-control-commands)
62
+ - [Energy reports](#energy-reports)
63
+ - [Configuration](#configuration)
64
+ - [API reference](#api-reference)
65
+ - [Troubleshooting](#troubleshooting)
66
+ - [Contributing](#contributing)
67
+ - [Changelog](#changelog)
68
+
69
+ ## Features
70
+
71
+ - **Authentication** — Azure B2C login with automatic token refresh and secure token persistence.
72
+ - **Discovery** — List Hubs, Zones and Appliances linked to your account.
73
+ - **Real-time status** — Fetch room temperature, setpoints, comfort status, boost/away modes and EcoStart state.
74
+ - **Control** — Set operation modes, activate Boost and Away, toggle EcoStart and Open Window Detection, and programme timer schedules.
75
+ - **Energy telemetry** — Pull Time Series Insights (TSI) energy reports with a robust telemetry parser that adapts to varying firmware formats.
76
+
77
+ ## Installation
78
+
79
+ ### From PyPI (recommended)
80
+
81
+ ```bash
82
+ pip install dimplex-controller
83
+ ```
84
+
85
+ ### From source
86
+
87
+ ```bash
88
+ git clone https://github.com/KRoperUK/dimplex-controller-py.git
89
+ cd dimplex-controller-py
90
+ pip install .
91
+ ```
92
+
93
+ ### Development install
94
+
95
+ ```bash
96
+ git clone https://github.com/KRoperUK/dimplex-controller-py.git
97
+ cd dimplex-controller-py
98
+ pip install -e ".[dev]"
99
+ ```
100
+
101
+ > **Requires:** Python 3.10 or later.
102
+
103
+ ## Quick start
104
+
105
+ The library uses `asyncio` and `aiohttp`. Here is the smallest example that lists your Hubs and Zones:
106
+
107
+ ```python
108
+ import asyncio
109
+ from aiohttp import ClientSession
110
+ from dimplex_controller import DimplexControl
111
+
112
+ async def main() -> None:
113
+ async with ClientSession() as session:
114
+ client = DimplexControl(session, refresh_token="YOUR_REFRESH_TOKEN")
115
+
116
+ hubs = await client.get_hubs()
117
+ for hub in hubs:
118
+ print(f"Hub: {hub.Name}")
119
+ zones = await client.get_hub_zones(hub.HubId)
120
+ for zone in zones:
121
+ print(f" Zone: {zone.ZoneName}")
122
+
123
+ if __name__ == "__main__":
124
+ asyncio.run(main())
125
+ ```
126
+
127
+ ## Usage guide
128
+
129
+ ### Authentication
130
+
131
+ Dimplex uses Azure AD B2C. You cannot log in purely programmatically with just a username and password — the library must first capture a short-lived authorisation code.
132
+
133
+ The recommended approach is to run the included demo script once:
134
+
135
+ ```bash
136
+ python demo.py
137
+ ```
138
+
139
+ The script walks you through opening a browser, signing in, and pasting the resulting redirect URL back into the terminal. On success, it saves a `dimplex_tokens.json` file in the current working directory.
140
+
141
+ Subsequent runs read the refresh token from that file automatically. You can also pass the refresh token directly to `DimplexControl` if you prefer to manage storage yourself.
142
+
143
+ ### Discovery
144
+
145
+ ```python
146
+ hubs = await client.get_hubs()
147
+ for hub in hubs:
148
+ print(f"Hub: {hub.Name} ({hub.HubId})")
149
+ zones = await client.get_hub_zones(hub.HubId)
150
+ for zone in zones:
151
+ print(f" Zone: {zone.ZoneName} ({zone.ZoneId})")
152
+ appliances = zone.Appliances
153
+ for appliance in appliances:
154
+ print(f" Appliance: {appliance.ApplianceId}")
155
+ ```
156
+
157
+ ### Reading status
158
+
159
+ ```python
160
+ from dimplex_controller.models import ApplianceStatus
161
+
162
+ status_list = await client.get_appliance_overview(hub_id, [appliance_id_1, appliance_id_2])
163
+
164
+ for status in status_list:
165
+ print(f"Room temperature: {status.RoomTemperature}°C")
166
+ print(f"Target temperature: {status.ActiveSetPointTemperature}°C")
167
+ print(f"EcoStart enabled: {status.EcoStartEnabled}")
168
+ print(f"Comfort status: {status.ComfortStatus}")
169
+ ```
170
+
171
+ ### Sending control commands
172
+
173
+ ```python
174
+ from dimplex_controller.models import ApplianceModeSettings
175
+
176
+ # Enable EcoStart
177
+ await client.set_eco_start(hub_id, [appliance_id], True)
178
+
179
+ # Enable Open Window Detection
180
+ await client.set_open_window_detection(hub_id, [appliance_id], True)
181
+
182
+ # Activate Boost (Mode 16, Status 1 = On)
183
+ boost_settings = ApplianceModeSettings(ApplianceModes=16, Status=1, Temperature=25.0)
184
+ await client.set_appliance_mode(hub_id, [appliance_id], boost_settings)
185
+ ```
186
+
187
+ ### Energy reports
188
+
189
+ ```python
190
+ from dimplex_controller import parse_telemetry_points
191
+
192
+ report = await client.get_tsi_energy_report(hub_id)
193
+ for appliance_id, telemetry in report.telemetry.items():
194
+ points = parse_telemetry_points(telemetry)
195
+ for timestamp, value in points:
196
+ print(f"{appliance_id}: {value} kWh at {timestamp}")
197
+ ```
198
+
199
+ The `parse_telemetry_points` helper normalises arbitrary API response shapes (varying firmware formats) into `(timestamp, value)` tuples.
200
+
201
+ ## Configuration
202
+
203
+ | Environment variable | Purpose |
204
+ |---------------------|---------|
205
+ | `DIMPLEX_TOKENS_FILE` | Path to the JSON token store. Defaults to `dimplex_tokens.json`. |
206
+
207
+ ## API reference
208
+
209
+ ### `DimplexControl`
210
+
211
+ Main client class. Construct with an `aiohttp.ClientSession` and a `refresh_token`.
212
+
213
+ | Method | Description |
214
+ |--------|-------------|
215
+ | `get_hubs()` | Returns `list[Hub]`. |
216
+ | `get_hub_zones(hub_id)` | Returns `list[Zone]` for a Hub. |
217
+ | `get_zone(hub_id, zone_id)` | Returns a single `Zone`. |
218
+ | `get_appliance_overview(hub_id, appliance_ids)` | Returns `list[ApplianceStatus]`. |
219
+ | `get_user_context()` | Returns `UserContext`. |
220
+ | `get_appliance_features(hub_id, appliance_ids)` | Returns raw appliance feature data. |
221
+ | `set_mode(hub_id, appliance_ids, mode, temperature)` | Set operation mode. |
222
+ | `set_target_temperature(...)` | Placeholder for future target temperature control. |
223
+ | `set_appliance_mode(hub_id, appliance_ids, settings)` | Send full mode settings. |
224
+ | `set_eco_start(hub_id, appliance_ids, enabled)` | Toggle EcoStart. |
225
+ | `set_open_window_detection(hub_id, appliance_ids, enabled)` | Toggle Open Window Detection. |
226
+ | `get_tsi_energy_report(hub_id)` | Returns `TsiEnergyReport`. |
227
+
228
+ ### Models
229
+
230
+ - **`Hub`** — Hub metadata.
231
+ - **`Zone`** — Zone metadata with linked Appliances.
232
+ - **`Appliance`** — Appliance metadata.
233
+ - **`ApplianceStatus`** — Live telemetry (room temperature, setpoints, comfort, etc.).
234
+ - **`ApplianceModeSettings`** — Payload for mode changes.
235
+ - **`TimerPeriod`** / **`TimerModeSettings`** — Timer schedule structures.
236
+ - **`UserContext`** — Authenticated user profile.
237
+ - **`TsiEnergyReport`** — Energy telemetry keyed by appliance.
238
+
239
+ ### Exceptions
240
+
241
+ - **`DimplexError`** — Base exception.
242
+ - **`DimplexAuthError`** — Authentication or token errors.
243
+ - **`DimplexApiError`** — API returned a non-success status. Contains `status` and `message`.
244
+ - **`DimplexConnectionError`** — Network-level failures.
245
+
246
+ ## Troubleshooting
247
+
248
+ ### Authentication failures
249
+
250
+ - Verify that the refresh token in `dimplex_tokens.json` has not expired. Delete the file and re-run `demo.py` to capture a fresh one.
251
+ - Ensure your network can reach `login.microsoftonline.com` and the Dimplex API endpoints.
252
+ - If you have multi-factor authentication (MFA) enabled on your Dimplex account, the headless flow should still work because it uses a browser session you control manually.
253
+
254
+ ### `DimplexAuthError`
255
+
256
+ This means the API rejected the token. Common causes:
257
+ - Token file is missing or corrupt.
258
+ - The refresh token has expired (Azure B2C refresh tokens typically last 90 days).
259
+ - The token was revoked from the Azure portal.
260
+
261
+ **Fix:** Delete `dimplex_tokens.json` and re-run the `demo.py` flow.
262
+
263
+ ### `DimplexConnectionError`
264
+
265
+ The library could not reach the GDHV API. Check:
266
+ - Internet connectivity.
267
+ - DNS resolution for `api.gdhv.io` (or whatever endpoint is configured in `const.py`).
268
+ - No corporate firewall or proxy is blocking `HTTPS` traffic.
269
+
270
+ ### Telemetry parsing errors
271
+
272
+ If `parse_telemetry_points` returns an empty list, the API likely returned an unexpected schema for your firmware version. Please open an issue with a redacted example of the raw response so the parser can be updated.
273
+
274
+ ### Rate limiting
275
+
276
+ The GDHV cloud API has rate limits. If you hit them, back off for a few minutes before retrying. The library does not currently implement automatic retries with back-off.
277
+
278
+ ## Contributing
279
+
280
+ Contributions are welcome! Please read the [contributing guidelines](CONTRIBUTING.md) before opening a pull request.
281
+
282
+ Key points:
283
+ - Use **Conventional Commits** (`feat:`, `fix:`, `chore:`, etc.) — this drives the automated changelog and PyPI releases.
284
+ - Run `ruff check`, `ruff format --check` and `pytest` locally before pushing.
285
+ - Pre-commit hooks are available — run `pre-commit install` once.
286
+
287
+ ## Changelog
288
+
289
+ See [CHANGELOG.md](CHANGELOG.md) for version history.
290
+
@@ -0,0 +1,260 @@
1
+ # Dimplex Controller Python Client
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/dimplex-controller.svg)](https://pypi.org/project/dimplex-controller/)
4
+ [![Python versions](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+ [![CI Tests](https://github.com/KRoperUK/dimplex-controller-py/actions/workflows/tests.yml/badge.svg)](https://github.com/KRoperUK/dimplex-controller-py/actions)
7
+ [![Downloads](https://img.shields.io/pypi/dm/dimplex-controller.svg)](https://pypi.org/project/dimplex-controller/)
8
+
9
+ <p align="center">
10
+ <strong>Async Python client for controlling Glen Dimplex Heating &amp; Ventilation (GDHV) appliances via the Dimplex cloud API.</strong>
11
+ </p>
12
+
13
+ ---
14
+
15
+ ## What does this do?
16
+
17
+ `dimplex-controller-py` is an asynchronous Python client that talks to the GDHV IoT cloud platform. It handles Azure B2C authentication (including automatic token refresh), discovers your Hubs, Zones and Appliances, and lets you read telemetry and send control commands — all from a script or a larger application.
18
+
19
+ It is the engine behind the [Dimplex Hub Home Assistant integration](https://github.com/KRoperUK/dimplex-controller-hass) and is published to PyPI as [`dimplex-controller`](https://pypi.org/project/dimplex-controller/).
20
+
21
+ > **Note:** This is an unofficial library and is not affiliated with or endorsed by Glen Dimplex Heating & Ventilation (GDHV). Use it at your own risk.
22
+
23
+ ## Contents
24
+
25
+ - [Features](#features)
26
+ - [Installation](#installation)
27
+ - [Quick start](#quick-start)
28
+ - [Usage guide](#usage-guide)
29
+ - [Authentication](#authentication)
30
+ - [Discovery](#discovery)
31
+ - [Reading status](#reading-status)
32
+ - [Sending control commands](#sending-control-commands)
33
+ - [Energy reports](#energy-reports)
34
+ - [Configuration](#configuration)
35
+ - [API reference](#api-reference)
36
+ - [Troubleshooting](#troubleshooting)
37
+ - [Contributing](#contributing)
38
+ - [Changelog](#changelog)
39
+
40
+ ## Features
41
+
42
+ - **Authentication** — Azure B2C login with automatic token refresh and secure token persistence.
43
+ - **Discovery** — List Hubs, Zones and Appliances linked to your account.
44
+ - **Real-time status** — Fetch room temperature, setpoints, comfort status, boost/away modes and EcoStart state.
45
+ - **Control** — Set operation modes, activate Boost and Away, toggle EcoStart and Open Window Detection, and programme timer schedules.
46
+ - **Energy telemetry** — Pull Time Series Insights (TSI) energy reports with a robust telemetry parser that adapts to varying firmware formats.
47
+
48
+ ## Installation
49
+
50
+ ### From PyPI (recommended)
51
+
52
+ ```bash
53
+ pip install dimplex-controller
54
+ ```
55
+
56
+ ### From source
57
+
58
+ ```bash
59
+ git clone https://github.com/KRoperUK/dimplex-controller-py.git
60
+ cd dimplex-controller-py
61
+ pip install .
62
+ ```
63
+
64
+ ### Development install
65
+
66
+ ```bash
67
+ git clone https://github.com/KRoperUK/dimplex-controller-py.git
68
+ cd dimplex-controller-py
69
+ pip install -e ".[dev]"
70
+ ```
71
+
72
+ > **Requires:** Python 3.10 or later.
73
+
74
+ ## Quick start
75
+
76
+ The library uses `asyncio` and `aiohttp`. Here is the smallest example that lists your Hubs and Zones:
77
+
78
+ ```python
79
+ import asyncio
80
+ from aiohttp import ClientSession
81
+ from dimplex_controller import DimplexControl
82
+
83
+ async def main() -> None:
84
+ async with ClientSession() as session:
85
+ client = DimplexControl(session, refresh_token="YOUR_REFRESH_TOKEN")
86
+
87
+ hubs = await client.get_hubs()
88
+ for hub in hubs:
89
+ print(f"Hub: {hub.Name}")
90
+ zones = await client.get_hub_zones(hub.HubId)
91
+ for zone in zones:
92
+ print(f" Zone: {zone.ZoneName}")
93
+
94
+ if __name__ == "__main__":
95
+ asyncio.run(main())
96
+ ```
97
+
98
+ ## Usage guide
99
+
100
+ ### Authentication
101
+
102
+ Dimplex uses Azure AD B2C. You cannot log in purely programmatically with just a username and password — the library must first capture a short-lived authorisation code.
103
+
104
+ The recommended approach is to run the included demo script once:
105
+
106
+ ```bash
107
+ python demo.py
108
+ ```
109
+
110
+ The script walks you through opening a browser, signing in, and pasting the resulting redirect URL back into the terminal. On success, it saves a `dimplex_tokens.json` file in the current working directory.
111
+
112
+ Subsequent runs read the refresh token from that file automatically. You can also pass the refresh token directly to `DimplexControl` if you prefer to manage storage yourself.
113
+
114
+ ### Discovery
115
+
116
+ ```python
117
+ hubs = await client.get_hubs()
118
+ for hub in hubs:
119
+ print(f"Hub: {hub.Name} ({hub.HubId})")
120
+ zones = await client.get_hub_zones(hub.HubId)
121
+ for zone in zones:
122
+ print(f" Zone: {zone.ZoneName} ({zone.ZoneId})")
123
+ appliances = zone.Appliances
124
+ for appliance in appliances:
125
+ print(f" Appliance: {appliance.ApplianceId}")
126
+ ```
127
+
128
+ ### Reading status
129
+
130
+ ```python
131
+ from dimplex_controller.models import ApplianceStatus
132
+
133
+ status_list = await client.get_appliance_overview(hub_id, [appliance_id_1, appliance_id_2])
134
+
135
+ for status in status_list:
136
+ print(f"Room temperature: {status.RoomTemperature}°C")
137
+ print(f"Target temperature: {status.ActiveSetPointTemperature}°C")
138
+ print(f"EcoStart enabled: {status.EcoStartEnabled}")
139
+ print(f"Comfort status: {status.ComfortStatus}")
140
+ ```
141
+
142
+ ### Sending control commands
143
+
144
+ ```python
145
+ from dimplex_controller.models import ApplianceModeSettings
146
+
147
+ # Enable EcoStart
148
+ await client.set_eco_start(hub_id, [appliance_id], True)
149
+
150
+ # Enable Open Window Detection
151
+ await client.set_open_window_detection(hub_id, [appliance_id], True)
152
+
153
+ # Activate Boost (Mode 16, Status 1 = On)
154
+ boost_settings = ApplianceModeSettings(ApplianceModes=16, Status=1, Temperature=25.0)
155
+ await client.set_appliance_mode(hub_id, [appliance_id], boost_settings)
156
+ ```
157
+
158
+ ### Energy reports
159
+
160
+ ```python
161
+ from dimplex_controller import parse_telemetry_points
162
+
163
+ report = await client.get_tsi_energy_report(hub_id)
164
+ for appliance_id, telemetry in report.telemetry.items():
165
+ points = parse_telemetry_points(telemetry)
166
+ for timestamp, value in points:
167
+ print(f"{appliance_id}: {value} kWh at {timestamp}")
168
+ ```
169
+
170
+ The `parse_telemetry_points` helper normalises arbitrary API response shapes (varying firmware formats) into `(timestamp, value)` tuples.
171
+
172
+ ## Configuration
173
+
174
+ | Environment variable | Purpose |
175
+ |---------------------|---------|
176
+ | `DIMPLEX_TOKENS_FILE` | Path to the JSON token store. Defaults to `dimplex_tokens.json`. |
177
+
178
+ ## API reference
179
+
180
+ ### `DimplexControl`
181
+
182
+ Main client class. Construct with an `aiohttp.ClientSession` and a `refresh_token`.
183
+
184
+ | Method | Description |
185
+ |--------|-------------|
186
+ | `get_hubs()` | Returns `list[Hub]`. |
187
+ | `get_hub_zones(hub_id)` | Returns `list[Zone]` for a Hub. |
188
+ | `get_zone(hub_id, zone_id)` | Returns a single `Zone`. |
189
+ | `get_appliance_overview(hub_id, appliance_ids)` | Returns `list[ApplianceStatus]`. |
190
+ | `get_user_context()` | Returns `UserContext`. |
191
+ | `get_appliance_features(hub_id, appliance_ids)` | Returns raw appliance feature data. |
192
+ | `set_mode(hub_id, appliance_ids, mode, temperature)` | Set operation mode. |
193
+ | `set_target_temperature(...)` | Placeholder for future target temperature control. |
194
+ | `set_appliance_mode(hub_id, appliance_ids, settings)` | Send full mode settings. |
195
+ | `set_eco_start(hub_id, appliance_ids, enabled)` | Toggle EcoStart. |
196
+ | `set_open_window_detection(hub_id, appliance_ids, enabled)` | Toggle Open Window Detection. |
197
+ | `get_tsi_energy_report(hub_id)` | Returns `TsiEnergyReport`. |
198
+
199
+ ### Models
200
+
201
+ - **`Hub`** — Hub metadata.
202
+ - **`Zone`** — Zone metadata with linked Appliances.
203
+ - **`Appliance`** — Appliance metadata.
204
+ - **`ApplianceStatus`** — Live telemetry (room temperature, setpoints, comfort, etc.).
205
+ - **`ApplianceModeSettings`** — Payload for mode changes.
206
+ - **`TimerPeriod`** / **`TimerModeSettings`** — Timer schedule structures.
207
+ - **`UserContext`** — Authenticated user profile.
208
+ - **`TsiEnergyReport`** — Energy telemetry keyed by appliance.
209
+
210
+ ### Exceptions
211
+
212
+ - **`DimplexError`** — Base exception.
213
+ - **`DimplexAuthError`** — Authentication or token errors.
214
+ - **`DimplexApiError`** — API returned a non-success status. Contains `status` and `message`.
215
+ - **`DimplexConnectionError`** — Network-level failures.
216
+
217
+ ## Troubleshooting
218
+
219
+ ### Authentication failures
220
+
221
+ - Verify that the refresh token in `dimplex_tokens.json` has not expired. Delete the file and re-run `demo.py` to capture a fresh one.
222
+ - Ensure your network can reach `login.microsoftonline.com` and the Dimplex API endpoints.
223
+ - If you have multi-factor authentication (MFA) enabled on your Dimplex account, the headless flow should still work because it uses a browser session you control manually.
224
+
225
+ ### `DimplexAuthError`
226
+
227
+ This means the API rejected the token. Common causes:
228
+ - Token file is missing or corrupt.
229
+ - The refresh token has expired (Azure B2C refresh tokens typically last 90 days).
230
+ - The token was revoked from the Azure portal.
231
+
232
+ **Fix:** Delete `dimplex_tokens.json` and re-run the `demo.py` flow.
233
+
234
+ ### `DimplexConnectionError`
235
+
236
+ The library could not reach the GDHV API. Check:
237
+ - Internet connectivity.
238
+ - DNS resolution for `api.gdhv.io` (or whatever endpoint is configured in `const.py`).
239
+ - No corporate firewall or proxy is blocking `HTTPS` traffic.
240
+
241
+ ### Telemetry parsing errors
242
+
243
+ If `parse_telemetry_points` returns an empty list, the API likely returned an unexpected schema for your firmware version. Please open an issue with a redacted example of the raw response so the parser can be updated.
244
+
245
+ ### Rate limiting
246
+
247
+ The GDHV cloud API has rate limits. If you hit them, back off for a few minutes before retrying. The library does not currently implement automatic retries with back-off.
248
+
249
+ ## Contributing
250
+
251
+ Contributions are welcome! Please read the [contributing guidelines](CONTRIBUTING.md) before opening a pull request.
252
+
253
+ Key points:
254
+ - Use **Conventional Commits** (`feat:`, `fix:`, `chore:`, etc.) — this drives the automated changelog and PyPI releases.
255
+ - Run `ruff check`, `ruff format --check` and `pytest` locally before pushing.
256
+ - Pre-commit hooks are available — run `pre-commit install` once.
257
+
258
+ ## Changelog
259
+
260
+ See [CHANGELOG.md](CHANGELOG.md) for version history.
@@ -2,8 +2,16 @@
2
2
 
3
3
  from .client import DimplexControl
4
4
  from .exceptions import DimplexApiError, DimplexAuthError, DimplexConnectionError, DimplexError
5
- from .models import Appliance, ApplianceModeSettings, ApplianceStatus, Hub, TsiEnergyReport, Zone
6
- from .telemetry import parse_telemetry_points
5
+ from .models import (
6
+ Appliance,
7
+ ApplianceModeSettings,
8
+ ApplianceStatus,
9
+ AutomaticProvisioning,
10
+ Hub,
11
+ TsiEnergyReport,
12
+ Zone,
13
+ )
14
+ from .telemetry import VALUE_KEY_T2, parse_telemetry_points
7
15
 
8
16
  __all__ = [
9
17
  "DimplexControl",
@@ -12,8 +20,10 @@ __all__ = [
12
20
  "Appliance",
13
21
  "ApplianceStatus",
14
22
  "ApplianceModeSettings",
23
+ "AutomaticProvisioning",
15
24
  "TsiEnergyReport",
16
25
  "parse_telemetry_points",
26
+ "VALUE_KEY_T2",
17
27
  "DimplexError",
18
28
  "DimplexApiError",
19
29
  "DimplexAuthError",
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  from datetime import datetime, timedelta, timezone
3
+ from typing import Any
3
4
 
4
5
  import aiohttp
5
6
 
@@ -194,16 +195,15 @@ class DimplexControl:
194
195
  report_type: int = 1,
195
196
  interval: str = DEFAULT_TSI_INTERVAL,
196
197
  start_date: str | None = None,
198
+ end_date: str | None = None,
197
199
  include_previous_period: bool = False,
198
200
  days_back: int = DEFAULT_TSI_REPORT_DAYS,
199
201
  ) -> TsiEnergyReport:
200
202
  """Fetch the Time Series Insights energy report for a hub.
201
203
 
202
- Returns a :class:`~dimplex_controller.models.TsiEnergyReport`. The
203
- cloud response is the same regardless of ``hub_id`` (the field is
204
- informational), so ``hub_id`` is only required to populate the
205
- returned model. Each per-appliance list is left as the raw payload —
206
- use :func:`dimplex_controller.telemetry.parse_telemetry_points` to
204
+ Returns a :class:`~dimplex_controller.models.TsiEnergyReport`. Each
205
+ per-appliance list is left as the raw payload use
206
+ :func:`dimplex_controller.telemetry.parse_telemetry_points` to
207
207
  normalise the points into ``(timestamp, value)`` tuples.
208
208
 
209
209
  When the hub has no metered appliances (e.g. non-QRAD heaters, or a
@@ -212,13 +212,19 @@ class DimplexControl:
212
212
  """
213
213
  if start_date is None:
214
214
  start_date = _iso_utc_days_ago(days_back)
215
+ if end_date is None:
216
+ end_date = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
215
217
 
216
- payload = {
218
+ payload: dict[str, Any] = {
217
219
  "TsiReportType": report_type,
218
220
  "Interval": interval,
219
221
  "StartDate": start_date,
222
+ "EndDate": end_date,
220
223
  "IncludePreviousPeriod": include_previous_period,
221
224
  }
225
+ if hub_id is not None:
226
+ payload["HubId"] = hub_id
227
+
222
228
  data = await self._request("POST", "/Reports/GetTsiEnergyReportDataForHub", json=payload)
223
229
  return TsiEnergyReport(
224
230
  HubId=(hub_id or data.get("HubId", "")),
@@ -1,6 +1,22 @@
1
+ import json
1
2
  from datetime import datetime, time
2
3
 
3
- from pydantic import BaseModel, Field
4
+ from pydantic import BaseModel, Field, field_validator
5
+
6
+
7
+ class AutomaticProvisioning(BaseModel):
8
+ """Parsed contents of ``ProductModelExtensions.AUTOMATIC_PROVISIONING``.
9
+
10
+ The cloud stores this as a JSON string; :class:`Appliance` decodes it
11
+ automatically so callers can read the heater's electrical characteristics.
12
+ """
13
+
14
+ bottom_element_power_rating: float | None = Field(None, alias="bottomElementPowerRating")
15
+ top_element_power_rating: float | None = Field(None, alias="topElementPowerRating")
16
+ rated_power: float | None = Field(None, alias="ratedPower")
17
+ charge_capacity: float | None = Field(None, alias="chargeCapacity")
18
+ charge_element_resistance: float | None = Field(None, alias="chargeElementResistance")
19
+ power_offset: float | None = Field(None, alias="powerOffset")
4
20
 
5
21
 
6
22
  class Appliance(BaseModel):
@@ -14,6 +30,32 @@ class Appliance(BaseModel):
14
30
  IconColor: str | None = None
15
31
  InstallationDate: datetime | None = None
16
32
  HasConnectivity: bool | None = None
33
+ SecurityCode: str | None = None
34
+ LastTelemDate: datetime | None = None
35
+ SeriesIdentifier: str | None = None
36
+ FirmwareVersion: str | None = None
37
+ ProductModelExtensions: dict[str, str] | None = None
38
+
39
+ @field_validator("ProductModelExtensions", mode="before")
40
+ @classmethod
41
+ def _coerce_provisioning_extensions(cls, value):
42
+ """Keep the raw extension dict but normalise string values to strings."""
43
+ if value is None:
44
+ return None
45
+ if isinstance(value, dict):
46
+ return {str(k): v if isinstance(v, str) else json.dumps(v) for k, v in value.items()}
47
+ return value
48
+
49
+ @property
50
+ def automatic_provisioning(self) -> AutomaticProvisioning | None:
51
+ """Return the decoded AUTOMATIC_PROVISIONING payload, if present."""
52
+ raw = (self.ProductModelExtensions or {}).get("AUTOMATIC_PROVISIONING")
53
+ if not raw:
54
+ return None
55
+ try:
56
+ return AutomaticProvisioning.model_validate_json(raw)
57
+ except (json.JSONDecodeError, ValueError):
58
+ return None
17
59
 
18
60
 
19
61
  class Zone(BaseModel):
@@ -21,6 +63,10 @@ class Zone(BaseModel):
21
63
  ZoneName: str
22
64
  HubId: str
23
65
  ZoneType: str
66
+ RoomType: str | None = None
67
+ AppName: str | None = None
68
+ Icon: str | None = None
69
+ IconColor: str | None = None
24
70
  Appliances: list[Appliance] = Field(default_factory=list)
25
71
 
26
72
 
@@ -28,6 +74,30 @@ class Hub(BaseModel):
28
74
  HubId: str
29
75
  Name: str | None = Field(None, alias="HubName")
30
76
  FriendlyName: str | None = None
77
+ SecurityCode: str | None = None
78
+ AddressLine1: str | None = None
79
+ AddressLine2: str | None = None
80
+ TownCity: str | None = None
81
+ Postcode: str | None = None
82
+ County: str | None = None
83
+ Country: str | None = None
84
+ Latitude: float | None = None
85
+ Longitude: float | None = None
86
+ HubRegistrationDate: datetime | None = None
87
+ InstallationDate: datetime | None = None
88
+ LastTelemDate: datetime | None = None
89
+ TimeZoneId: int | None = None
90
+ TimeZoneName: str | None = None
91
+ NumberOfZones: int | None = None
92
+ NumberOfAppliances: int | None = None
93
+ IsServiceModeEnabled: bool | None = None
94
+ FirmwareVersion: str | None = None
95
+ ConnectionState: int | None = None
96
+ HubType: str | None = None
97
+ BluetoothName: str | None = None
98
+ PrimaryUserEmail: str | None = None
99
+ IsDefault: bool | None = None
100
+ RoleName: str | None = None
31
101
 
32
102
 
33
103
  class TimerPeriod(BaseModel):
@@ -1,10 +1,13 @@
1
1
  """Telemetry parsing for the Dimplex Reports API.
2
2
 
3
- The response from `POST /Reports/GetTsiEnergyReportDataForHub` contains
3
+ The response from ``POST /Reports/GetTsiEnergyReportDataForHub`` contains
4
4
  ``ApplianceTelemetryData``: a dict keyed by appliance id, with one list of
5
5
  ``telemetry points`` per appliance. The shape of each point is not documented
6
6
  and varies between firmware versions, so we normalise whatever the cloud
7
7
  sends into ``(timestamp, value)`` tuples.
8
+
9
+ Real-world payloads have been observed using ``TS`` (Unix-epoch timestamp)
10
+ with either ``T1`` or ``ST`` as the energy value key.
8
11
  """
9
12
 
10
13
  from __future__ import annotations
@@ -16,7 +19,7 @@ from typing import Any
16
19
  # Keys the cloud has been observed using, in priority order. The first match
17
20
  # wins. Case-insensitive lookup is done by lowercasing the dict before
18
21
  # scanning, so callers do not have to worry about casing.
19
- _TIMESTAMP_KEYS = (
22
+ _DEFAULT_TIMESTAMP_KEYS = (
20
23
  "timestamp",
21
24
  "time",
22
25
  "ts",
@@ -26,8 +29,9 @@ _TIMESTAMP_KEYS = (
26
29
  "from",
27
30
  "start",
28
31
  )
29
- _VALUE_KEYS = (
32
+ _DEFAULT_VALUE_KEYS = (
30
33
  "t1",
34
+ "st", # Observed on QRAD050F / QRAD075F energy reports (see dimplex-controller-hass #27).
31
35
  "value",
32
36
  "kwh",
33
37
  "energy",
@@ -35,6 +39,17 @@ _VALUE_KEYS = (
35
39
  "energykwh",
36
40
  "amount",
37
41
  "v",
42
+ # Fallback for appliances that only report the secondary register.
43
+ "t2",
44
+ )
45
+
46
+ # Secondary energy register observed for some Quantum appliances. Points may
47
+ # contain both ``T1`` and ``T2`` in the same payload.
48
+ VALUE_KEY_T2 = (
49
+ "t2",
50
+ "value2",
51
+ "kwh2",
52
+ "energy2",
38
53
  )
39
54
 
40
55
 
@@ -92,10 +107,14 @@ def _iter_items(point: Any) -> Iterable[tuple[str, Any]] | None:
92
107
  if not isinstance(point, dict):
93
108
  return None
94
109
  lower = {str(k).lower(): v for k, v in point.items()}
95
- return ((k, lower.get(k)) for k in (*_TIMESTAMP_KEYS, *_VALUE_KEYS))
110
+ return ((k, lower.get(k)) for k in (*_DEFAULT_TIMESTAMP_KEYS, *_DEFAULT_VALUE_KEYS, *VALUE_KEY_T2))
96
111
 
97
112
 
98
- def parse_telemetry_points(points: Any) -> list[tuple[datetime | None, float]]:
113
+ def parse_telemetry_points(
114
+ points: Any,
115
+ *,
116
+ value_keys: tuple[str, ...] | None = None,
117
+ ) -> list[tuple[datetime | None, float]]:
99
118
  """Normalise a list of telemetry points into ``(timestamp, value)`` pairs.
100
119
 
101
120
  Each entry in ``points`` may be:
@@ -104,12 +123,19 @@ def parse_telemetry_points(points: Any) -> list[tuple[datetime | None, float]]:
104
123
  * a 2-element ``[timestamp, value]`` list or tuple
105
124
  * a bare scalar (treated as a cumulative value at an unknown timestamp)
106
125
 
126
+ ``value_keys`` overrides the value-key priority list. Use
127
+ :data:`VALUE_KEY_T2` to extract the secondary energy register when the
128
+ cloud returns both ``T1`` and ``T2`` in the same payload.
129
+
107
130
  Unparseable entries are silently skipped. The order of the input list is
108
131
  preserved.
109
132
  """
110
133
  if not isinstance(points, list):
111
134
  return []
112
135
 
136
+ value_key_order = value_keys if value_keys is not None else _DEFAULT_VALUE_KEYS
137
+ timestamp_keys = _DEFAULT_TIMESTAMP_KEYS
138
+
113
139
  out: list[tuple[datetime | None, float]] = []
114
140
  for point in points:
115
141
  ts: datetime | None = None
@@ -118,9 +144,9 @@ def parse_telemetry_points(points: Any) -> list[tuple[datetime | None, float]]:
118
144
  items = _iter_items(point)
119
145
  if items is not None:
120
146
  for key, raw in items:
121
- if key in _TIMESTAMP_KEYS and ts is None:
147
+ if key in timestamp_keys and ts is None:
122
148
  ts = _coerce_timestamp(raw)
123
- elif key in _VALUE_KEYS and value is None:
149
+ elif key in value_key_order and value is None:
124
150
  value = _coerce_value(raw)
125
151
  elif isinstance(point, list | tuple) and len(point) == 2:
126
152
  ts = _coerce_timestamp(point[0])
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dimplex-controller"
3
- version = "0.5.0"
3
+ version = "0.6.1"
4
4
  description = "Python client for Dimplex heating controllers (GDHV IoT)"
5
5
  authors = ["Kieran Roper"]
6
6
  license = "MIT"
@@ -1,136 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: dimplex-controller
3
- Version: 0.5.0
4
- Summary: Python client for Dimplex heating controllers (GDHV IoT)
5
- License: MIT
6
- License-File: LICENSE
7
- Keywords: dimplex,heating,control,iot,asyncio
8
- Author: Kieran Roper
9
- Requires-Python: >=3.10,<4.0
10
- Classifier: Development Status :: 5 - Production/Stable
11
- Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Natural Language :: English
14
- Classifier: Operating System :: OS Independent
15
- Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.10
17
- Classifier: Programming Language :: Python :: 3.11
18
- Classifier: Programming Language :: Python :: 3.12
19
- Classifier: Programming Language :: Python :: 3.13
20
- Classifier: Programming Language :: Python :: 3.14
21
- Classifier: Topic :: Home Automation
22
- Requires-Dist: aiohttp (>=3.9.0,<4.0.0)
23
- Requires-Dist: beautifulsoup4 (>=4.14.3,<5.0.0)
24
- Requires-Dist: pydantic (>=2.0.0,<3.0.0)
25
- Requires-Dist: python-dotenv (>=1.2.1,<2.0.0)
26
- Project-URL: Homepage, https://github.com/KRoperUK/dimplex-controller-py
27
- Project-URL: Repository, https://github.com/KRoperUK/dimplex-controller-py
28
- Description-Content-Type: text/markdown
29
-
30
- # Dimplex Controller Python Client
31
-
32
- [![PyPI](https://img.shields.io/pypi/v/dimplex-controller.svg)](https://pypi.org/project/dimplex-controller/)
33
- [![Tests](https://github.com/KRoperUK/dimplex-controller-py/actions/workflows/tests.yml/badge.svg)](https://github.com/KRoperUK/dimplex-controller-py/actions)
34
- [![Downloads](https://img.shields.io/pypi/dm/dimplex-controller.svg)](https://pypi.org/project/dimplex-controller/)
35
- [![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-blue)](https://www.python.org/downloads/)
36
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
37
- [![GitHub](https://img.shields.io/badge/GitHub-Repository-black?logo=github)](https://github.com/KRoperUK/dimplex-controller-py)
38
-
39
- A Python asyncio client for controlling Dimplex heating systems (GDHV IoT).
40
-
41
- ## Features
42
-
43
- - **Authentication**: Easy login flow and automatic token refresh (Azure B2C).
44
- - **Discovery**: List Hubs, Zones, and Appliances associated with your account.
45
- - **Detailed Status**: Fetch real-time data including room temperature, setpoints, comfort status, and active boost settings.
46
- - **Control**:
47
- - Set operation modes (Manual, Timer, Frost Protection).
48
- - Activate **Boost** and **Away** modes.
49
- - Toggle **EcoStart** and **Open Window Detection**.
50
- - Program heating schedules (Timer Periods).
51
-
52
- ## Installation
53
-
54
- This project is managed with Poetry.
55
-
56
- ```bash
57
- git clone <repo-url>
58
- cd dimplex-controller-py
59
- poetry install
60
- ```
61
-
62
- ## Getting Started
63
-
64
- ### 1. Initial Authentication
65
- Due to the nature of the Azure B2C flow, you must perform the initial login manually to capture an authorization code.
66
-
67
- Run the demo script to guide you through the process:
68
-
69
- ```bash
70
- poetry run python demo.py
71
- ```
72
-
73
- Follow the on-screen instructions. Once successful, a `dimplex_tokens.json` file will be created, allowing the library to authenticate automatically in the future.
74
-
75
- ### 2. Basic Usage
76
-
77
- ```python
78
- import asyncio
79
- import aiohttp
80
- from dimplex_controller import DimplexControl
81
-
82
- async def main():
83
- async with aiohttp.ClientSession() as session:
84
- # Pass tokens from dimplex_tokens.json or just the refresh_token
85
- client = DimplexControl(session, refresh_token="YOUR_REFRESH_TOKEN")
86
-
87
- # Get Hubs
88
- hubs = await client.get_hubs()
89
- for hub in hubs:
90
- print(f"Hub: {hub.Name}")
91
-
92
- # Get Zones and Appliances
93
- zones = await client.get_hub_zones(hub.HubId)
94
- for zone in zones:
95
- print(f" Zone: {zone.ZoneName}")
96
-
97
- if __name__ == "__main__":
98
- asyncio.run(main())
99
- ```
100
-
101
- ### 3. Advanced Operations
102
-
103
- #### Get Real-time Status
104
- ```python
105
- # Fetch status for a list of appliance IDs
106
- status_list = await client.get_appliance_overview(hub_id, ["appliance_id_1", "appliance_id_2"])
107
-
108
- for status in status_list:
109
- print(f"Temp: {status.RoomTemperature}°C, Target: {status.ActiveSetPointTemperature}°C")
110
- print(f"EcoStart: {status.EcoStartEnabled}")
111
- ```
112
-
113
- #### Control Features
114
- ```python
115
- from dimplex_controller.models import ApplianceModeSettings
116
-
117
- # Enable EcoStart
118
- await client.set_eco_start(hub_id, [appliance_id], True)
119
-
120
- # Enable Open Window Detection
121
- await client.set_open_window_detection(hub_id, [appliance_id], True)
122
-
123
- # Activate Boost (Mode 16, Status 1 = On)
124
- boost_settings = ApplianceModeSettings(ApplianceModes=16, Status=1, Temperature=25.0)
125
- await client.set_appliance_mode(hub_id, [appliance_id], boost_settings)
126
- ```
127
-
128
- ## Development & API Reference
129
-
130
- - **`openapi.yaml`**: This file contains the most complete technical specification of the API discovered so far. It includes all known endpoints, request bodies, and response schemas.
131
- - **Traffic Logs**: If you identify new features in the mobile app, capture the traffic and add the endpoints to `openapi.yaml` and the `DimplexControl` client.
132
-
133
- ## Disclaimer
134
-
135
- This is an unofficial library and is not affiliated with or endorsed by Glen Dimplex Heating & Ventilation (GDHV). Use it at your own risk.
136
-
@@ -1,106 +0,0 @@
1
- # Dimplex Controller Python Client
2
-
3
- [![PyPI](https://img.shields.io/pypi/v/dimplex-controller.svg)](https://pypi.org/project/dimplex-controller/)
4
- [![Tests](https://github.com/KRoperUK/dimplex-controller-py/actions/workflows/tests.yml/badge.svg)](https://github.com/KRoperUK/dimplex-controller-py/actions)
5
- [![Downloads](https://img.shields.io/pypi/dm/dimplex-controller.svg)](https://pypi.org/project/dimplex-controller/)
6
- [![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-blue)](https://www.python.org/downloads/)
7
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
- [![GitHub](https://img.shields.io/badge/GitHub-Repository-black?logo=github)](https://github.com/KRoperUK/dimplex-controller-py)
9
-
10
- A Python asyncio client for controlling Dimplex heating systems (GDHV IoT).
11
-
12
- ## Features
13
-
14
- - **Authentication**: Easy login flow and automatic token refresh (Azure B2C).
15
- - **Discovery**: List Hubs, Zones, and Appliances associated with your account.
16
- - **Detailed Status**: Fetch real-time data including room temperature, setpoints, comfort status, and active boost settings.
17
- - **Control**:
18
- - Set operation modes (Manual, Timer, Frost Protection).
19
- - Activate **Boost** and **Away** modes.
20
- - Toggle **EcoStart** and **Open Window Detection**.
21
- - Program heating schedules (Timer Periods).
22
-
23
- ## Installation
24
-
25
- This project is managed with Poetry.
26
-
27
- ```bash
28
- git clone <repo-url>
29
- cd dimplex-controller-py
30
- poetry install
31
- ```
32
-
33
- ## Getting Started
34
-
35
- ### 1. Initial Authentication
36
- Due to the nature of the Azure B2C flow, you must perform the initial login manually to capture an authorization code.
37
-
38
- Run the demo script to guide you through the process:
39
-
40
- ```bash
41
- poetry run python demo.py
42
- ```
43
-
44
- Follow the on-screen instructions. Once successful, a `dimplex_tokens.json` file will be created, allowing the library to authenticate automatically in the future.
45
-
46
- ### 2. Basic Usage
47
-
48
- ```python
49
- import asyncio
50
- import aiohttp
51
- from dimplex_controller import DimplexControl
52
-
53
- async def main():
54
- async with aiohttp.ClientSession() as session:
55
- # Pass tokens from dimplex_tokens.json or just the refresh_token
56
- client = DimplexControl(session, refresh_token="YOUR_REFRESH_TOKEN")
57
-
58
- # Get Hubs
59
- hubs = await client.get_hubs()
60
- for hub in hubs:
61
- print(f"Hub: {hub.Name}")
62
-
63
- # Get Zones and Appliances
64
- zones = await client.get_hub_zones(hub.HubId)
65
- for zone in zones:
66
- print(f" Zone: {zone.ZoneName}")
67
-
68
- if __name__ == "__main__":
69
- asyncio.run(main())
70
- ```
71
-
72
- ### 3. Advanced Operations
73
-
74
- #### Get Real-time Status
75
- ```python
76
- # Fetch status for a list of appliance IDs
77
- status_list = await client.get_appliance_overview(hub_id, ["appliance_id_1", "appliance_id_2"])
78
-
79
- for status in status_list:
80
- print(f"Temp: {status.RoomTemperature}°C, Target: {status.ActiveSetPointTemperature}°C")
81
- print(f"EcoStart: {status.EcoStartEnabled}")
82
- ```
83
-
84
- #### Control Features
85
- ```python
86
- from dimplex_controller.models import ApplianceModeSettings
87
-
88
- # Enable EcoStart
89
- await client.set_eco_start(hub_id, [appliance_id], True)
90
-
91
- # Enable Open Window Detection
92
- await client.set_open_window_detection(hub_id, [appliance_id], True)
93
-
94
- # Activate Boost (Mode 16, Status 1 = On)
95
- boost_settings = ApplianceModeSettings(ApplianceModes=16, Status=1, Temperature=25.0)
96
- await client.set_appliance_mode(hub_id, [appliance_id], boost_settings)
97
- ```
98
-
99
- ## Development & API Reference
100
-
101
- - **`openapi.yaml`**: This file contains the most complete technical specification of the API discovered so far. It includes all known endpoints, request bodies, and response schemas.
102
- - **Traffic Logs**: If you identify new features in the mobile app, capture the traffic and add the endpoints to `openapi.yaml` and the `DimplexControl` client.
103
-
104
- ## Disclaimer
105
-
106
- This is an unofficial library and is not affiliated with or endorsed by Glen Dimplex Heating & Ventilation (GDHV). Use it at your own risk.