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.
- dimplex_controller-0.6.1/PKG-INFO +290 -0
- dimplex_controller-0.6.1/README.md +260 -0
- {dimplex_controller-0.5.0 → dimplex_controller-0.6.1}/dimplex_controller/__init__.py +12 -2
- {dimplex_controller-0.5.0 → dimplex_controller-0.6.1}/dimplex_controller/client.py +12 -6
- {dimplex_controller-0.5.0 → dimplex_controller-0.6.1}/dimplex_controller/models.py +71 -1
- {dimplex_controller-0.5.0 → dimplex_controller-0.6.1}/dimplex_controller/telemetry.py +33 -7
- {dimplex_controller-0.5.0 → dimplex_controller-0.6.1}/pyproject.toml +1 -1
- dimplex_controller-0.5.0/PKG-INFO +0 -136
- dimplex_controller-0.5.0/README.md +0 -106
- {dimplex_controller-0.5.0 → dimplex_controller-0.6.1}/LICENSE +0 -0
- {dimplex_controller-0.5.0 → dimplex_controller-0.6.1}/dimplex_controller/auth.py +0 -0
- {dimplex_controller-0.5.0 → dimplex_controller-0.6.1}/dimplex_controller/const.py +0 -0
- {dimplex_controller-0.5.0 → dimplex_controller-0.6.1}/dimplex_controller/exceptions.py +0 -0
|
@@ -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
|
+
[](https://pypi.org/project/dimplex-controller/)
|
|
33
|
+
[](https://www.python.org/downloads/)
|
|
34
|
+
[](LICENSE)
|
|
35
|
+
[](https://github.com/KRoperUK/dimplex-controller-py/actions)
|
|
36
|
+
[](https://pypi.org/project/dimplex-controller/)
|
|
37
|
+
|
|
38
|
+
<p align="center">
|
|
39
|
+
<strong>Async Python client for controlling Glen Dimplex Heating & 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
|
+
[](https://pypi.org/project/dimplex-controller/)
|
|
4
|
+
[](https://www.python.org/downloads/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/KRoperUK/dimplex-controller-py/actions)
|
|
7
|
+
[](https://pypi.org/project/dimplex-controller/)
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<strong>Async Python client for controlling Glen Dimplex Heating & 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
|
|
6
|
-
|
|
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`.
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (*
|
|
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(
|
|
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
|
|
147
|
+
if key in timestamp_keys and ts is None:
|
|
122
148
|
ts = _coerce_timestamp(raw)
|
|
123
|
-
elif key in
|
|
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,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
|
-
[](https://pypi.org/project/dimplex-controller/)
|
|
33
|
-
[](https://github.com/KRoperUK/dimplex-controller-py/actions)
|
|
34
|
-
[](https://pypi.org/project/dimplex-controller/)
|
|
35
|
-
[](https://www.python.org/downloads/)
|
|
36
|
-
[](LICENSE)
|
|
37
|
-
[](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
|
-
[](https://pypi.org/project/dimplex-controller/)
|
|
4
|
-
[](https://github.com/KRoperUK/dimplex-controller-py/actions)
|
|
5
|
-
[](https://pypi.org/project/dimplex-controller/)
|
|
6
|
-
[](https://www.python.org/downloads/)
|
|
7
|
-
[](LICENSE)
|
|
8
|
-
[](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.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|