dimplex-controller 0.13.0__tar.gz → 0.14.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dimplex_controller-0.13.0 → dimplex_controller-0.14.0}/PKG-INFO +27 -15
- {dimplex_controller-0.13.0 → dimplex_controller-0.14.0}/README.md +20 -6
- {dimplex_controller-0.13.0 → dimplex_controller-0.14.0}/dimplex_controller/__init__.py +9 -2
- {dimplex_controller-0.13.0 → dimplex_controller-0.14.0}/dimplex_controller/capabilities.py +8 -1
- {dimplex_controller-0.13.0 → dimplex_controller-0.14.0}/dimplex_controller/client.py +46 -9
- {dimplex_controller-0.13.0 → dimplex_controller-0.14.0}/dimplex_controller/const.py +11 -2
- {dimplex_controller-0.13.0 → dimplex_controller-0.14.0}/dimplex_controller/exceptions.py +22 -2
- {dimplex_controller-0.13.0 → dimplex_controller-0.14.0}/dimplex_controller/models.py +11 -3
- {dimplex_controller-0.13.0 → dimplex_controller-0.14.0}/dimplex_controller/telemetry.py +25 -3
- dimplex_controller-0.14.0/pyproject.toml +96 -0
- dimplex_controller-0.14.0/pyproject.toml.orig +106 -0
- dimplex_controller-0.13.0/pyproject.toml +0 -80
- {dimplex_controller-0.13.0 → dimplex_controller-0.14.0}/LICENSE +0 -0
- {dimplex_controller-0.13.0 → dimplex_controller-0.14.0}/dimplex_controller/auth.py +0 -0
- {dimplex_controller-0.13.0 → dimplex_controller-0.14.0}/dimplex_controller/cli.py +0 -0
- {dimplex_controller-0.13.0 → dimplex_controller-0.14.0}/dimplex_controller/py.typed +0 -0
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dimplex-controller
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Python client for Dimplex
|
|
5
|
-
License: MIT
|
|
6
|
-
License-File: LICENSE
|
|
3
|
+
Version: 0.14.0
|
|
4
|
+
Summary: Unofficial async Python client for Dimplex (GDHV IoT) heating appliances — not affiliated with Glen Dimplex
|
|
7
5
|
Keywords: dimplex,heating,control,iot,asyncio
|
|
8
6
|
Author: Kieran Roper
|
|
9
|
-
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
10
9
|
Classifier: Development Status :: 5 - Production/Stable
|
|
11
10
|
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
13
11
|
Classifier: Natural Language :: English
|
|
14
12
|
Classifier: Operating System :: OS Independent
|
|
15
13
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -19,8 +17,9 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
19
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
18
|
Classifier: Programming Language :: Python :: 3.14
|
|
21
19
|
Classifier: Topic :: Home Automation
|
|
22
|
-
Requires-Dist: aiohttp
|
|
23
|
-
Requires-Dist: pydantic
|
|
20
|
+
Requires-Dist: aiohttp>=3.14.3,<4
|
|
21
|
+
Requires-Dist: pydantic>=2.13.5,<3
|
|
22
|
+
Requires-Python: >=3.10, <4.0
|
|
24
23
|
Project-URL: Homepage, https://github.com/KRoperUK/dimplex-controller-py
|
|
25
24
|
Project-URL: Repository, https://github.com/KRoperUK/dimplex-controller-py
|
|
26
25
|
Description-Content-Type: text/markdown
|
|
@@ -40,14 +39,24 @@ Description-Content-Type: text/markdown
|
|
|
40
39
|
|
|
41
40
|
---
|
|
42
41
|
|
|
42
|
+
> [!IMPORTANT]
|
|
43
|
+
> **Unofficial library.** Not affiliated with, endorsed by, or supported by Dimplex, Glen
|
|
44
|
+
> Dimplex Heating & Ventilation (GDHV), or the Glen Dimplex Group. This is an independent
|
|
45
|
+
> project built by **reverse-engineering** the official Dimplex Control Android app and the
|
|
46
|
+
> private cloud API it talks to.
|
|
47
|
+
>
|
|
48
|
+
> There is no public or documented API. Dimplex can change or withdraw that API at any time
|
|
49
|
+
> and without notice, which may break this library. Use it at your own risk.
|
|
50
|
+
>
|
|
51
|
+
> No Dimplex code is included or redistributed. "Dimplex", "Quantum", "QRAD" and related
|
|
52
|
+
> marks belong to their respective owners and are used here only to describe compatibility.
|
|
53
|
+
|
|
43
54
|
## What does this do?
|
|
44
55
|
|
|
45
56
|
`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.
|
|
46
57
|
|
|
47
58
|
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/).
|
|
48
59
|
|
|
49
|
-
> **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.
|
|
50
|
-
|
|
51
60
|
## Contents
|
|
52
61
|
|
|
53
62
|
- [Features](#features)
|
|
@@ -94,9 +103,12 @@ pip install .
|
|
|
94
103
|
```bash
|
|
95
104
|
git clone https://github.com/KRoperUK/dimplex-controller-py.git
|
|
96
105
|
cd dimplex-controller-py
|
|
97
|
-
|
|
106
|
+
uv sync
|
|
98
107
|
```
|
|
99
108
|
|
|
109
|
+
`uv sync` creates `.venv`, installs the library in editable mode, and installs the
|
|
110
|
+
dev dependency group from `uv.lock`.
|
|
111
|
+
|
|
100
112
|
> **Requires:** Python 3.10 or later.
|
|
101
113
|
|
|
102
114
|
## Quick start
|
|
@@ -186,8 +198,9 @@ await client.set_open_window_detection(hub_id, [appliance_id], True)
|
|
|
186
198
|
# Timed Boost (ApplianceModes=2, Time = minutes)
|
|
187
199
|
await client.set_boost(hub_id, [appliance_id], temperature=25.0, duration_minutes=60)
|
|
188
200
|
|
|
189
|
-
# Away until a given moment (ApplianceModes=4). Away is a settable 7–
|
|
190
|
-
# setback and defaults to the 7 °C anti-freeze floor.
|
|
201
|
+
# Away until a given moment (ApplianceModes=4). Away is a settable 7–18 °C
|
|
202
|
+
# setback and defaults to the 7 °C anti-freeze floor. A higher temperature is
|
|
203
|
+
# clamped to 18 °C with a warning, matching what the cloud does to it anyway.
|
|
191
204
|
from datetime import datetime, timedelta, timezone
|
|
192
205
|
|
|
193
206
|
await client.set_away(
|
|
@@ -292,7 +305,7 @@ Main client class. Construct with an `aiohttp.ClientSession` and a `refresh_toke
|
|
|
292
305
|
|
|
293
306
|
- **`DimplexError`** — Base exception.
|
|
294
307
|
- **`DimplexAuthError`** — Authentication or token errors.
|
|
295
|
-
- **`DimplexApiError`** — API returned a non-success status. Contains `status` and `
|
|
308
|
+
- **`DimplexApiError`** — API returned a non-success status. Contains `status`, `message`, `transient` (`True` for a 5xx/429 raised after retries were exhausted, so a caller can back off rather than surface it) and an optional `code`.
|
|
296
309
|
- **`DimplexConnectionError`** — Network-level failures.
|
|
297
310
|
|
|
298
311
|
## Troubleshooting
|
|
@@ -389,4 +402,3 @@ Key points:
|
|
|
389
402
|
## Changelog
|
|
390
403
|
|
|
391
404
|
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
392
|
-
|
|
@@ -13,14 +13,24 @@
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
+
> [!IMPORTANT]
|
|
17
|
+
> **Unofficial library.** Not affiliated with, endorsed by, or supported by Dimplex, Glen
|
|
18
|
+
> Dimplex Heating & Ventilation (GDHV), or the Glen Dimplex Group. This is an independent
|
|
19
|
+
> project built by **reverse-engineering** the official Dimplex Control Android app and the
|
|
20
|
+
> private cloud API it talks to.
|
|
21
|
+
>
|
|
22
|
+
> There is no public or documented API. Dimplex can change or withdraw that API at any time
|
|
23
|
+
> and without notice, which may break this library. Use it at your own risk.
|
|
24
|
+
>
|
|
25
|
+
> No Dimplex code is included or redistributed. "Dimplex", "Quantum", "QRAD" and related
|
|
26
|
+
> marks belong to their respective owners and are used here only to describe compatibility.
|
|
27
|
+
|
|
16
28
|
## What does this do?
|
|
17
29
|
|
|
18
30
|
`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.
|
|
19
31
|
|
|
20
32
|
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/).
|
|
21
33
|
|
|
22
|
-
> **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.
|
|
23
|
-
|
|
24
34
|
## Contents
|
|
25
35
|
|
|
26
36
|
- [Features](#features)
|
|
@@ -67,9 +77,12 @@ pip install .
|
|
|
67
77
|
```bash
|
|
68
78
|
git clone https://github.com/KRoperUK/dimplex-controller-py.git
|
|
69
79
|
cd dimplex-controller-py
|
|
70
|
-
|
|
80
|
+
uv sync
|
|
71
81
|
```
|
|
72
82
|
|
|
83
|
+
`uv sync` creates `.venv`, installs the library in editable mode, and installs the
|
|
84
|
+
dev dependency group from `uv.lock`.
|
|
85
|
+
|
|
73
86
|
> **Requires:** Python 3.10 or later.
|
|
74
87
|
|
|
75
88
|
## Quick start
|
|
@@ -159,8 +172,9 @@ await client.set_open_window_detection(hub_id, [appliance_id], True)
|
|
|
159
172
|
# Timed Boost (ApplianceModes=2, Time = minutes)
|
|
160
173
|
await client.set_boost(hub_id, [appliance_id], temperature=25.0, duration_minutes=60)
|
|
161
174
|
|
|
162
|
-
# Away until a given moment (ApplianceModes=4). Away is a settable 7–
|
|
163
|
-
# setback and defaults to the 7 °C anti-freeze floor.
|
|
175
|
+
# Away until a given moment (ApplianceModes=4). Away is a settable 7–18 °C
|
|
176
|
+
# setback and defaults to the 7 °C anti-freeze floor. A higher temperature is
|
|
177
|
+
# clamped to 18 °C with a warning, matching what the cloud does to it anyway.
|
|
164
178
|
from datetime import datetime, timedelta, timezone
|
|
165
179
|
|
|
166
180
|
await client.set_away(
|
|
@@ -265,7 +279,7 @@ Main client class. Construct with an `aiohttp.ClientSession` and a `refresh_toke
|
|
|
265
279
|
|
|
266
280
|
- **`DimplexError`** — Base exception.
|
|
267
281
|
- **`DimplexAuthError`** — Authentication or token errors.
|
|
268
|
-
- **`DimplexApiError`** — API returned a non-success status. Contains `status` and `
|
|
282
|
+
- **`DimplexApiError`** — API returned a non-success status. Contains `status`, `message`, `transient` (`True` for a 5xx/429 raised after retries were exhausted, so a caller can back off rather than surface it) and an optional `code`.
|
|
269
283
|
- **`DimplexConnectionError`** — Network-level failures.
|
|
270
284
|
|
|
271
285
|
## Troubleshooting
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"""Dimplex Controller Client.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
API.
|
|
3
|
+
Unofficial async Python client for the Glen Dimplex Heating & Ventilation
|
|
4
|
+
(GDHV) cloud API. Not affiliated with, endorsed by, or supported by Dimplex
|
|
5
|
+
or the Glen Dimplex Group: the protocol was recovered by reverse-engineering
|
|
6
|
+
the official Dimplex Control Android app, so Dimplex may change or withdraw
|
|
7
|
+
it without notice. See :class:`~dimplex_controller.client.DimplexControl` for the entry
|
|
5
8
|
point and :meth:`DimplexControl.get_appliance_overview` for the read path
|
|
6
9
|
used by the Home Assistant integration.
|
|
7
10
|
|
|
@@ -15,6 +18,8 @@ from .auth import TokenBundle, TokenListener
|
|
|
15
18
|
from .capabilities import ApplianceCapabilities, capabilities_for
|
|
16
19
|
from .client import DimplexControl
|
|
17
20
|
from .const import (
|
|
21
|
+
AWAY_TEMP_MAX,
|
|
22
|
+
AWAY_TEMP_MIN,
|
|
18
23
|
DEFAULT_AWAY_TEMPERATURE,
|
|
19
24
|
DEFAULT_BOOST_TEMPERATURE,
|
|
20
25
|
FROST_TEMPERATURE,
|
|
@@ -84,6 +89,8 @@ __all__ = [
|
|
|
84
89
|
"TsiEnergyReport",
|
|
85
90
|
"DEFAULT_AWAY_TEMPERATURE",
|
|
86
91
|
"DEFAULT_BOOST_TEMPERATURE",
|
|
92
|
+
"AWAY_TEMP_MIN",
|
|
93
|
+
"AWAY_TEMP_MAX",
|
|
87
94
|
"FROST_TEMPERATURE",
|
|
88
95
|
"MODE_TEMP_MIN",
|
|
89
96
|
"MODE_TEMP_MAX",
|
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
-
from .const import FROST_TEMPERATURE, MODE_TEMP_MAX, MODE_TEMP_MIN
|
|
12
|
+
from .const import AWAY_TEMP_MAX, AWAY_TEMP_MIN, FROST_TEMPERATURE, MODE_TEMP_MAX, MODE_TEMP_MIN
|
|
13
13
|
from .models import Appliance, ApplianceStatus, AutomaticProvisioning, ProductModel
|
|
14
14
|
|
|
15
15
|
# Default boost lengths (minutes) offered by the mobile app for most heaters.
|
|
@@ -47,6 +47,11 @@ class ApplianceCapabilities:
|
|
|
47
47
|
climate: bool = True
|
|
48
48
|
min_temp: float = MODE_TEMP_MIN
|
|
49
49
|
max_temp: float = MODE_TEMP_MAX
|
|
50
|
+
# Away is the one carousel with its own bounds: it shares the 7 °C floor but
|
|
51
|
+
# stops at 18 °C rather than 30 °C (dimplex-controller-py#98). Consumers
|
|
52
|
+
# should gate the Away picker on these rather than on min_temp/max_temp.
|
|
53
|
+
away_min_temp: float = AWAY_TEMP_MIN
|
|
54
|
+
away_max_temp: float = AWAY_TEMP_MAX
|
|
50
55
|
frost_temp: float = FROST_TEMPERATURE
|
|
51
56
|
default_boost_minutes: int = DEFAULT_BOOST_MINUTES
|
|
52
57
|
boost_durations: tuple[int, ...] = DEFAULT_BOOST_DURATIONS
|
|
@@ -83,6 +88,8 @@ class ApplianceCapabilities:
|
|
|
83
88
|
"climate": self.climate,
|
|
84
89
|
"min_temp": self.min_temp,
|
|
85
90
|
"max_temp": self.max_temp,
|
|
91
|
+
"away_min_temp": self.away_min_temp,
|
|
92
|
+
"away_max_temp": self.away_max_temp,
|
|
86
93
|
"frost_temp": self.frost_temp,
|
|
87
94
|
"default_boost_minutes": self.default_boost_minutes,
|
|
88
95
|
"boost_durations": list(self.boost_durations),
|
|
@@ -12,6 +12,8 @@ import aiohttp
|
|
|
12
12
|
from .auth import AuthManager, TokenBundle, TokenListener
|
|
13
13
|
from .capabilities import ApplianceCapabilities, capabilities_for
|
|
14
14
|
from .const import (
|
|
15
|
+
AWAY_TEMP_MAX,
|
|
16
|
+
AWAY_TEMP_MIN,
|
|
15
17
|
BASE_URL,
|
|
16
18
|
DEFAULT_AWAY_TEMPERATURE,
|
|
17
19
|
DEFAULT_BOOST_TEMPERATURE,
|
|
@@ -70,6 +72,26 @@ _RETRYABLE_STATUS = frozenset({429, 500, 502, 503, 504})
|
|
|
70
72
|
DEFAULT_TIMEOUT = 30.0
|
|
71
73
|
|
|
72
74
|
|
|
75
|
+
def _clamp_away_temperature(temperature: float) -> float:
|
|
76
|
+
"""Clamp an Away target into the mode's own 7–18 °C range.
|
|
77
|
+
|
|
78
|
+
The cloud reduces a higher value silently, so clamping here keeps the value
|
|
79
|
+
the caller asked for and the value the appliance ends up at in agreement,
|
|
80
|
+
and the warning makes the adjustment visible.
|
|
81
|
+
"""
|
|
82
|
+
value = float(temperature)
|
|
83
|
+
clamped = min(max(value, AWAY_TEMP_MIN), AWAY_TEMP_MAX)
|
|
84
|
+
if clamped != value:
|
|
85
|
+
_LOGGER.warning(
|
|
86
|
+
"Away temperature %.1f °C is outside the %s–%s °C the app offers for Away; using %.1f °C",
|
|
87
|
+
value,
|
|
88
|
+
AWAY_TEMP_MIN,
|
|
89
|
+
AWAY_TEMP_MAX,
|
|
90
|
+
clamped,
|
|
91
|
+
)
|
|
92
|
+
return clamped
|
|
93
|
+
|
|
94
|
+
|
|
73
95
|
def _coerce_timeout(timeout: float | aiohttp.ClientTimeout | None) -> aiohttp.ClientTimeout | None:
|
|
74
96
|
"""Normalise a timeout value into an :class:`aiohttp.ClientTimeout`.
|
|
75
97
|
|
|
@@ -252,12 +274,14 @@ class DimplexControl:
|
|
|
252
274
|
attempts - 1,
|
|
253
275
|
delay,
|
|
254
276
|
)
|
|
255
|
-
last_error = DimplexApiError(resp.status, text)
|
|
277
|
+
last_error = DimplexApiError(resp.status, text, transient=resp.status in _RETRYABLE_STATUS)
|
|
256
278
|
await asyncio.sleep(delay)
|
|
257
279
|
continue
|
|
258
280
|
|
|
259
281
|
_LOGGER.error("API request failed: %s - %s", resp.status, text)
|
|
260
|
-
|
|
282
|
+
# A 5xx/429 that reaches here was retried and still failed, so it
|
|
283
|
+
# is transient (back off); a 4xx is a hard failure (surface it).
|
|
284
|
+
raise DimplexApiError(resp.status, text, transient=resp.status in _RETRYABLE_STATUS)
|
|
261
285
|
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
|
262
286
|
if allow_retry and attempt + 1 < attempts:
|
|
263
287
|
delay = self._backoff_seconds(attempt)
|
|
@@ -325,7 +349,7 @@ class DimplexControl:
|
|
|
325
349
|
"""Get details for a specific zone."""
|
|
326
350
|
payload = {"HubId": hub_id, "ZoneId": zone_id}
|
|
327
351
|
data = await self._request("POST", "/Zones/GetZone", json=payload)
|
|
328
|
-
return Zone.model_validate(data)
|
|
352
|
+
return Zone.model_validate(data)
|
|
329
353
|
|
|
330
354
|
async def get_appliance_overview(self, hub_id: str, appliance_ids: list[str]) -> list[ApplianceStatus]:
|
|
331
355
|
"""Get status overview for specific appliances.
|
|
@@ -351,7 +375,7 @@ class DimplexControl:
|
|
|
351
375
|
async def get_user_context(self) -> UserContext:
|
|
352
376
|
"""Get user profile/context."""
|
|
353
377
|
data = await self._request("GET", "/Identity/GetUserContext")
|
|
354
|
-
return UserContext.model_validate(data)
|
|
378
|
+
return UserContext.model_validate(data)
|
|
355
379
|
|
|
356
380
|
async def get_product_models(self) -> list[ProductModel]:
|
|
357
381
|
"""Return the cloud product catalogue (models + provisioning metadata).
|
|
@@ -371,7 +395,7 @@ class DimplexControl:
|
|
|
371
395
|
"TimerMode": 0, # Required field in request; value ignored on read
|
|
372
396
|
}
|
|
373
397
|
data = await self._request("POST", "/RemoteControl/GetTimerModeDetailsForAppliance", json=payload)
|
|
374
|
-
return TimerModeSettings.model_validate(data)
|
|
398
|
+
return TimerModeSettings.model_validate(data)
|
|
375
399
|
|
|
376
400
|
async def get_schedule(self, hub_id: str, appliance_id: str) -> TimerModeSettings:
|
|
377
401
|
"""Return the current timer mode + periods (alias of :meth:`get_appliance_features`)."""
|
|
@@ -591,15 +615,26 @@ class DimplexControl:
|
|
|
591
615
|
"""Enable or disable Away mode.
|
|
592
616
|
|
|
593
617
|
Sends ``ApplianceModes=4`` (:attr:`~dimplex_controller.ApplianceModeFlag.AWAY`).
|
|
594
|
-
Away is a *settable* setback: the app offers 7–
|
|
595
|
-
|
|
596
|
-
|
|
618
|
+
Away is a *settable* setback: the app offers 7–18 °C
|
|
619
|
+
(:data:`~dimplex_controller.AWAY_TEMP_MIN` / :data:`~dimplex_controller.AWAY_TEMP_MAX`)
|
|
620
|
+
and defaults to the 7 °C anti-freeze floor, so a low ``temperature`` is
|
|
621
|
+
the normal case rather than a bug.
|
|
597
622
|
|
|
598
623
|
``until`` is the "away until" datetime the app sends in ``Date``. Pass a
|
|
599
624
|
:class:`~datetime.datetime` or an ISO-8601 string. ``number_of_days`` is
|
|
600
625
|
retained for backwards compatibility and, when ``until`` is omitted, is
|
|
601
626
|
converted into an equivalent ``Date``.
|
|
602
627
|
|
|
628
|
+
When engaging Away, ``temperature`` is clamped into the Away range and a
|
|
629
|
+
warning logged if it had to be: the cloud silently reduces a higher
|
|
630
|
+
value, so clamping locally makes the outcome the caller actually gets
|
|
631
|
+
visible rather than surprising (dimplex-controller-py#98). The clamp is
|
|
632
|
+
skipped when clearing, where the temperature is inert.
|
|
633
|
+
|
|
634
|
+
.. versionchanged:: 0.13.1
|
|
635
|
+
``temperature`` is clamped to ``AWAY_TEMP_MIN``..``AWAY_TEMP_MAX``
|
|
636
|
+
when ``enable`` is true.
|
|
637
|
+
|
|
603
638
|
.. versionchanged:: 0.13.0
|
|
604
639
|
Previously sent ``ApplianceModes=32``, which is **FrostProtect** —
|
|
605
640
|
the appliance was pinned to the fixed 7 °C frost setpoint and the
|
|
@@ -607,6 +642,8 @@ class DimplexControl:
|
|
|
607
642
|
Away duration now travels in ``Date`` as the app does, not only in
|
|
608
643
|
``NumberOfDays``.
|
|
609
644
|
"""
|
|
645
|
+
if enable:
|
|
646
|
+
temperature = _clamp_away_temperature(temperature)
|
|
610
647
|
days = int(number_of_days)
|
|
611
648
|
if until is None and days > 0:
|
|
612
649
|
until = datetime.now(timezone.utc) + timedelta(days=days)
|
|
@@ -876,7 +913,7 @@ class DimplexControl:
|
|
|
876
913
|
"/RemoteControl/ApiGetTimerModeDetailsForHeatPumpHwcAppliance",
|
|
877
914
|
json=payload,
|
|
878
915
|
)
|
|
879
|
-
return TimerModeSettings.model_validate(data)
|
|
916
|
+
return TimerModeSettings.model_validate(data)
|
|
880
917
|
|
|
881
918
|
async def set_heat_pump_hot_water_schedule(self, settings: TimerModeSettings) -> TimerModeSettings:
|
|
882
919
|
"""Write an ASHW cylinder's schedule periods.
|
|
@@ -26,15 +26,24 @@ B2C_POLICY = "B2C_1A_DimplexControlSignupSignin"
|
|
|
26
26
|
# recovered from Dimplex Control APK 2.26.0.
|
|
27
27
|
# See ``docs/decompiled-api-reference.md``.
|
|
28
28
|
|
|
29
|
-
# The mode carousels (
|
|
29
|
+
# The mode carousels (Boost / Frost / Manual / Eco) offer 7–30 °C. Away shares
|
|
30
|
+
# the floor but *not* the ceiling — see AWAY_TEMP_MAX below.
|
|
30
31
|
MODE_TEMP_MIN = 7.0
|
|
31
32
|
MODE_TEMP_MAX = 30.0
|
|
32
33
|
|
|
34
|
+
# Away has its own, narrower ceiling. The app's Away picker will not offer above
|
|
35
|
+
# 18 °C, and a live test against a Quantum requesting 25 °C produced 18 °C in the
|
|
36
|
+
# cloud overview, the app and the appliance's own panel — so the earlier reading
|
|
37
|
+
# of ``UpdateTempRange`` that folded Away into the shared 7–30 carousel was
|
|
38
|
+
# over-generalised (dimplex-controller-py#98).
|
|
39
|
+
AWAY_TEMP_MIN = 7.0
|
|
40
|
+
AWAY_TEMP_MAX = 18.0
|
|
41
|
+
|
|
33
42
|
# Frost protection is always driven at the anti-freeze floor.
|
|
34
43
|
FROST_TEMPERATURE = 7.0
|
|
35
44
|
|
|
36
45
|
# Away defaults to the frost floor (it is an anti-freeze setback by default)
|
|
37
|
-
# but is user-settable across
|
|
46
|
+
# but is user-settable across AWAY_TEMP_MIN..AWAY_TEMP_MAX.
|
|
38
47
|
DEFAULT_AWAY_TEMPERATURE = FROST_TEMPERATURE
|
|
39
48
|
|
|
40
49
|
# Boost's default in the app's picker.
|
|
@@ -79,11 +79,31 @@ class DimplexAuthTransientError(DimplexAuthError):
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
class DimplexApiError(DimplexError):
|
|
82
|
-
"""Exception for API errors.
|
|
82
|
+
"""Exception for API errors.
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
Attributes:
|
|
85
|
+
status: HTTP status returned by the API.
|
|
86
|
+
message: Short server message (never a full token).
|
|
87
|
+
transient: ``True`` when the failure may succeed on retry — a 5xx or 429
|
|
88
|
+
raised only after ``client.py`` exhausted its internal retries. ``False``
|
|
89
|
+
for a hard 4xx (404/400/403). Callers (e.g. the Home Assistant
|
|
90
|
+
coordinator) back off and keep entities available on ``transient``, and
|
|
91
|
+
surface the error otherwise.
|
|
92
|
+
code: Optional stable, machine-readable identifier.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
status: int,
|
|
98
|
+
message: str,
|
|
99
|
+
*,
|
|
100
|
+
transient: bool = False,
|
|
101
|
+
code: str | None = None,
|
|
102
|
+
):
|
|
85
103
|
self.status = status
|
|
86
104
|
self.message = message
|
|
105
|
+
self.transient = transient
|
|
106
|
+
self.code = code
|
|
87
107
|
super().__init__(f"API Error {status}: {message}")
|
|
88
108
|
|
|
89
109
|
|
|
@@ -139,7 +139,7 @@ class ProductModel(BaseModel):
|
|
|
139
139
|
if not raw:
|
|
140
140
|
return None
|
|
141
141
|
try:
|
|
142
|
-
return AutomaticProvisioning.model_validate_json(raw)
|
|
142
|
+
return AutomaticProvisioning.model_validate_json(raw)
|
|
143
143
|
except (json.JSONDecodeError, ValueError):
|
|
144
144
|
return None
|
|
145
145
|
|
|
@@ -178,7 +178,7 @@ class Appliance(BaseModel):
|
|
|
178
178
|
if not raw:
|
|
179
179
|
return None
|
|
180
180
|
try:
|
|
181
|
-
return AutomaticProvisioning.model_validate_json(raw)
|
|
181
|
+
return AutomaticProvisioning.model_validate_json(raw)
|
|
182
182
|
except (json.JSONDecodeError, ValueError):
|
|
183
183
|
return None
|
|
184
184
|
|
|
@@ -287,6 +287,14 @@ class ApplianceStatus(BaseModel):
|
|
|
287
287
|
ErrorCode: str | None = None
|
|
288
288
|
WarningCode: str | None = None
|
|
289
289
|
|
|
290
|
+
@field_validator("ErrorCode", "WarningCode", mode="before")
|
|
291
|
+
@classmethod
|
|
292
|
+
def _coerce_code(cls, value: Any) -> Any:
|
|
293
|
+
# GetApplianceOverview sends these as integers (0 when healthy).
|
|
294
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
295
|
+
return str(value)
|
|
296
|
+
return value
|
|
297
|
+
|
|
290
298
|
@property
|
|
291
299
|
def mode_flags(self) -> ApplianceModeFlag:
|
|
292
300
|
"""Return ``ApplianceModes`` as a typed flag set."""
|
|
@@ -425,7 +433,7 @@ class TsiEnergyReport(BaseModel):
|
|
|
425
433
|
"""
|
|
426
434
|
|
|
427
435
|
HubId: str
|
|
428
|
-
ApplianceTelemetryData: dict[str, list] = Field(default_factory=dict)
|
|
436
|
+
ApplianceTelemetryData: dict[str, list[Any]] = Field(default_factory=dict)
|
|
429
437
|
|
|
430
438
|
|
|
431
439
|
# `TsiReportType` integer values understood by the API. We do not know the
|
|
@@ -117,6 +117,15 @@ def _coerce_timestamp(raw: Any) -> datetime | None:
|
|
|
117
117
|
return raw
|
|
118
118
|
if isinstance(raw, int | float):
|
|
119
119
|
ts = float(raw)
|
|
120
|
+
# Seconds-vs-milliseconds disambiguation is magnitude-only, which is safe for
|
|
121
|
+
# the data this parses: a Unix *seconds* epoch does not reach 1e12 until the
|
|
122
|
+
# year ~33658, and the cloud emits second-resolution `TS` values, so anything
|
|
123
|
+
# above the threshold is a millisecond epoch. The converse edge is not
|
|
124
|
+
# defended — a millisecond epoch before 2001-09-09 (< 1e12) would be misread
|
|
125
|
+
# as seconds — because the cloud has never produced one. A value just below
|
|
126
|
+
# 1e12 read as seconds lands past datetime's representable range and is
|
|
127
|
+
# dropped by the guard below rather than mis-parsed. See
|
|
128
|
+
# dimplex-controller-py#121 for why this is intentional rather than a gap.
|
|
120
129
|
if ts > 1e12:
|
|
121
130
|
ts = ts / 1000.0
|
|
122
131
|
try:
|
|
@@ -276,6 +285,11 @@ def summarise_energy(
|
|
|
276
285
|
``points`` may be raw cloud payloads or already-parsed
|
|
277
286
|
:data:`TelemetryPoint` tuples. When raw, they are passed through
|
|
278
287
|
:func:`parse_telemetry_points` first.
|
|
288
|
+
|
|
289
|
+
Points with no parseable timestamp (``ts is None``) are excluded from the
|
|
290
|
+
total in **every** mode. They cannot be placed on a calendar day, so
|
|
291
|
+
counting them only in ``lifetime`` would make the cumulative total disagree
|
|
292
|
+
with the sum of the per-day totals (dimplex-controller-py#120).
|
|
279
293
|
"""
|
|
280
294
|
parsed: list[TelemetryPoint]
|
|
281
295
|
if (
|
|
@@ -285,7 +299,7 @@ def summarise_energy(
|
|
|
285
299
|
and isinstance(points[0], tuple)
|
|
286
300
|
and len(points[0]) == 2
|
|
287
301
|
):
|
|
288
|
-
parsed = list(points)
|
|
302
|
+
parsed = list(points)
|
|
289
303
|
else:
|
|
290
304
|
parsed = parse_telemetry_points(points, value_keys=value_keys)
|
|
291
305
|
|
|
@@ -306,8 +320,16 @@ def summarise_energy(
|
|
|
306
320
|
mode="daily",
|
|
307
321
|
)
|
|
308
322
|
|
|
309
|
-
# lifetime —
|
|
310
|
-
|
|
323
|
+
# lifetime / window — totals must reconcile with the per-day totals, so a point
|
|
324
|
+
# with no parseable timestamp is dropped from both. A ``ts is None`` point cannot
|
|
325
|
+
# be placed on any calendar day (``daily``/``window`` already drop it via
|
|
326
|
+
# ``filter_telemetry_points``), so counting it only in the lifetime total made the
|
|
327
|
+
# cumulative meter disagree with the sum of its days — silent drift into the HA
|
|
328
|
+
# Energy Dashboard's TOTAL_INCREASING sensor (dimplex-controller-py#120).
|
|
329
|
+
if mode == "window":
|
|
330
|
+
selected = filter_telemetry_points(parsed, start=start, end=end, tz=tz)
|
|
331
|
+
else:
|
|
332
|
+
selected = [(ts, value) for ts, value in parsed if ts is not None]
|
|
311
333
|
|
|
312
334
|
timestamps = [ts for ts, _ in selected if ts is not None]
|
|
313
335
|
total = sum(v for _, v in selected)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dimplex-controller"
|
|
3
|
+
version = "0.14.0"
|
|
4
|
+
description = "Unofficial async Python client for Dimplex (GDHV IoT) heating appliances — not affiliated with Glen Dimplex"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
license-files = ["LICENSE"]
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.10,<4.0"
|
|
9
|
+
keywords = [
|
|
10
|
+
"dimplex",
|
|
11
|
+
"heating",
|
|
12
|
+
"control",
|
|
13
|
+
"iot",
|
|
14
|
+
"asyncio",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Natural Language :: English",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Topic :: Home Automation",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"aiohttp>=3.14.3,<4",
|
|
31
|
+
"pydantic>=2.13.5,<3",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[[project.authors]]
|
|
35
|
+
name = "Kieran Roper"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/KRoperUK/dimplex-controller-py"
|
|
39
|
+
Repository = "https://github.com/KRoperUK/dimplex-controller-py"
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
dimplex = "dimplex_controller.cli:main"
|
|
43
|
+
|
|
44
|
+
[dependency-groups]
|
|
45
|
+
dev = [
|
|
46
|
+
"pytest>=9.1.1,<10",
|
|
47
|
+
"pytest-asyncio>=1.4.0,<2",
|
|
48
|
+
"pytest-cov>=7.1.0,<8",
|
|
49
|
+
"aresponses>=3.0.0,<4",
|
|
50
|
+
"ruff>=0.16.7,<0.17",
|
|
51
|
+
"pre-commit>=4.6.2,<5",
|
|
52
|
+
"twine>=7.0.0,<8",
|
|
53
|
+
"python-dotenv>=1.2.3,<2",
|
|
54
|
+
"mypy>=2.3.1,<3",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
line-length = 120
|
|
59
|
+
target-version = "py310"
|
|
60
|
+
extend-exclude = ["*.md"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint]
|
|
63
|
+
select = [
|
|
64
|
+
"E",
|
|
65
|
+
"F",
|
|
66
|
+
"I",
|
|
67
|
+
"W",
|
|
68
|
+
"UP",
|
|
69
|
+
"B",
|
|
70
|
+
"SIM",
|
|
71
|
+
]
|
|
72
|
+
ignore = ["E501"]
|
|
73
|
+
|
|
74
|
+
[tool.mypy]
|
|
75
|
+
python_version = "3.10"
|
|
76
|
+
files = ["dimplex_controller"]
|
|
77
|
+
ignore_missing_imports = true
|
|
78
|
+
strict = true
|
|
79
|
+
|
|
80
|
+
[tool.pytest.ini_options]
|
|
81
|
+
asyncio_mode = "auto"
|
|
82
|
+
testpaths = ["tests"]
|
|
83
|
+
|
|
84
|
+
[tool.coverage.run]
|
|
85
|
+
source = ["dimplex_controller"]
|
|
86
|
+
|
|
87
|
+
[tool.coverage.report]
|
|
88
|
+
show_missing = true
|
|
89
|
+
fail_under = 80
|
|
90
|
+
|
|
91
|
+
[tool.uv.build-backend]
|
|
92
|
+
module-root = ""
|
|
93
|
+
|
|
94
|
+
[build-system]
|
|
95
|
+
requires = ["uv_build>=0.11.19,<0.13.0"]
|
|
96
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dimplex-controller"
|
|
3
|
+
version = "0.14.0"
|
|
4
|
+
description = "Unofficial async Python client for Dimplex (GDHV IoT) heating appliances — not affiliated with Glen Dimplex"
|
|
5
|
+
authors = [{ name = "Kieran Roper" }]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.10,<4.0"
|
|
10
|
+
keywords = ["dimplex", "heating", "control", "iot", "asyncio"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 5 - Production/Stable",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Natural Language :: English",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
|
+
"Topic :: Home Automation",
|
|
23
|
+
]
|
|
24
|
+
# Lower bounds are the versions this release was developed and tested against;
|
|
25
|
+
# the upper bounds keep the previous caret semantics (a caret range is
|
|
26
|
+
# `>=X,<next-major`), so consumers gain no surprise from the migration.
|
|
27
|
+
dependencies = [
|
|
28
|
+
"aiohttp>=3.14.3,<4",
|
|
29
|
+
"pydantic>=2.13.5,<3",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/KRoperUK/dimplex-controller-py"
|
|
34
|
+
Repository = "https://github.com/KRoperUK/dimplex-controller-py"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
dimplex = "dimplex_controller.cli:main"
|
|
38
|
+
|
|
39
|
+
# PEP 735 dependency groups: not installed into a consumer's environment, only
|
|
40
|
+
# into a development one.
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
dev = [
|
|
43
|
+
"pytest>=9.1.1,<10",
|
|
44
|
+
"pytest-asyncio>=1.4.0,<2",
|
|
45
|
+
"pytest-cov>=7.1.0,<8",
|
|
46
|
+
"aresponses>=3.0.0,<4",
|
|
47
|
+
"ruff>=0.16.7,<0.17",
|
|
48
|
+
"pre-commit>=4.6.2,<5",
|
|
49
|
+
"twine>=7.0.0,<8",
|
|
50
|
+
"python-dotenv>=1.2.3,<2",
|
|
51
|
+
"mypy>=2.3.1,<3",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
line-length = 120
|
|
56
|
+
target-version = "py310"
|
|
57
|
+
# Python inside Markdown is not formatted here. The code blocks in README.md and
|
|
58
|
+
# docs/ carry deliberate column alignment (see the temperature-constant table in
|
|
59
|
+
# docs/api-reference.md) and ruff's formatter normalises comment spacing, so
|
|
60
|
+
# formatting them would trade readable docs for tool uniformity. This also keeps a
|
|
61
|
+
# bare `ruff format .` in step with the pre-commit hooks, which are python/pyi only.
|
|
62
|
+
extend-exclude = ["*.md"]
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = ["E", "F", "I", "W", "UP", "B", "SIM"]
|
|
66
|
+
# E501 (line length) is handled by the formatter; long strings/comments are fine.
|
|
67
|
+
ignore = ["E501"]
|
|
68
|
+
|
|
69
|
+
[tool.mypy]
|
|
70
|
+
python_version = "3.10"
|
|
71
|
+
files = ["dimplex_controller"]
|
|
72
|
+
ignore_missing_imports = true
|
|
73
|
+
# `strict` rather than the four flags this used to carry (disallow_untyped_defs,
|
|
74
|
+
# warn_return_any, warn_unused_configs, warn_redundant_casts). The library is the
|
|
75
|
+
# layer the Home Assistant integration trusts for models, capabilities and error
|
|
76
|
+
# types, and that consumer runs strict mypy over its own code — so a library
|
|
77
|
+
# checked more loosely could hand it types that do not hold
|
|
78
|
+
# (dimplex-controller-hass#202).
|
|
79
|
+
#
|
|
80
|
+
# Raising it surfaced seven stale `# type: ignore` comments, all of which mypy
|
|
81
|
+
# reported as unused, plus one missing generic argument. No real type errors.
|
|
82
|
+
strict = true
|
|
83
|
+
|
|
84
|
+
[tool.pytest.ini_options]
|
|
85
|
+
# All async tests use asyncio; "auto" means new tests don't each need the
|
|
86
|
+
# @pytest.mark.asyncio decorator (existing markers remain valid).
|
|
87
|
+
asyncio_mode = "auto"
|
|
88
|
+
testpaths = ["tests"]
|
|
89
|
+
|
|
90
|
+
[tool.coverage.run]
|
|
91
|
+
source = ["dimplex_controller"]
|
|
92
|
+
|
|
93
|
+
[tool.coverage.report]
|
|
94
|
+
show_missing = true
|
|
95
|
+
# Guardrail so coverage can't silently regress. Raise as coverage improves.
|
|
96
|
+
fail_under = 80
|
|
97
|
+
|
|
98
|
+
[build-system]
|
|
99
|
+
requires = ["uv_build>=0.11.19,<0.13.0"]
|
|
100
|
+
build-backend = "uv_build"
|
|
101
|
+
|
|
102
|
+
[tool.uv.build-backend]
|
|
103
|
+
# The package is at the repository root, not under src/, which is what uv_build
|
|
104
|
+
# assumes by default. The module name is inferred from the project name
|
|
105
|
+
# (dimplex-controller -> dimplex_controller), so it needs no override.
|
|
106
|
+
module-root = ""
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
[tool.poetry]
|
|
2
|
-
name = "dimplex-controller"
|
|
3
|
-
version = "0.13.0"
|
|
4
|
-
description = "Python client for Dimplex heating controllers (GDHV IoT)"
|
|
5
|
-
authors = ["Kieran Roper"]
|
|
6
|
-
license = "MIT"
|
|
7
|
-
readme = "README.md"
|
|
8
|
-
homepage = "https://github.com/KRoperUK/dimplex-controller-py"
|
|
9
|
-
repository = "https://github.com/KRoperUK/dimplex-controller-py"
|
|
10
|
-
keywords = ["dimplex", "heating", "control", "iot", "asyncio"]
|
|
11
|
-
classifiers = [
|
|
12
|
-
"Development Status :: 5 - Production/Stable",
|
|
13
|
-
"Intended Audience :: Developers",
|
|
14
|
-
"License :: OSI Approved :: MIT License",
|
|
15
|
-
"Natural Language :: English",
|
|
16
|
-
"Operating System :: OS Independent",
|
|
17
|
-
"Programming Language :: Python :: 3",
|
|
18
|
-
"Programming Language :: Python :: 3.10",
|
|
19
|
-
"Programming Language :: Python :: 3.11",
|
|
20
|
-
"Programming Language :: Python :: 3.12",
|
|
21
|
-
"Programming Language :: Python :: 3.13",
|
|
22
|
-
"Topic :: Home Automation",
|
|
23
|
-
]
|
|
24
|
-
packages = [{ include = "dimplex_controller" }]
|
|
25
|
-
|
|
26
|
-
[tool.poetry.scripts]
|
|
27
|
-
dimplex = "dimplex_controller.cli:main"
|
|
28
|
-
|
|
29
|
-
[tool.poetry.dependencies]
|
|
30
|
-
python = "^3.10"
|
|
31
|
-
aiohttp = "^3.9.0"
|
|
32
|
-
pydantic = "^2.0.0"
|
|
33
|
-
|
|
34
|
-
[tool.poetry.group.dev.dependencies]
|
|
35
|
-
pytest = "^8.0.0"
|
|
36
|
-
pytest-asyncio = "^1.0.0"
|
|
37
|
-
pytest-cov = "^6.0.0"
|
|
38
|
-
aresponses = "^3.0.0"
|
|
39
|
-
ruff = "^0.15.0"
|
|
40
|
-
pre-commit = "^3.6.0"
|
|
41
|
-
twine = "^6.2.0"
|
|
42
|
-
python-dotenv = "^1.2.1"
|
|
43
|
-
mypy = "^1.0.0"
|
|
44
|
-
|
|
45
|
-
[tool.ruff]
|
|
46
|
-
line-length = 120
|
|
47
|
-
target-version = "py310"
|
|
48
|
-
|
|
49
|
-
[tool.ruff.lint]
|
|
50
|
-
select = ["E", "F", "I", "W", "UP", "B", "SIM"]
|
|
51
|
-
# E501 (line length) is handled by the formatter; long strings/comments are fine.
|
|
52
|
-
ignore = ["E501"]
|
|
53
|
-
|
|
54
|
-
[tool.mypy]
|
|
55
|
-
python_version = "3.10"
|
|
56
|
-
files = ["dimplex_controller"]
|
|
57
|
-
ignore_missing_imports = true
|
|
58
|
-
# Incrementally strict — these catch real bugs without requiring a full rewrite.
|
|
59
|
-
disallow_untyped_defs = true
|
|
60
|
-
warn_return_any = true
|
|
61
|
-
warn_unused_configs = true
|
|
62
|
-
warn_redundant_casts = true
|
|
63
|
-
|
|
64
|
-
[tool.pytest.ini_options]
|
|
65
|
-
# All async tests use asyncio; "auto" means new tests don't each need the
|
|
66
|
-
# @pytest.mark.asyncio decorator (existing markers remain valid).
|
|
67
|
-
asyncio_mode = "auto"
|
|
68
|
-
testpaths = ["tests"]
|
|
69
|
-
|
|
70
|
-
[tool.coverage.run]
|
|
71
|
-
source = ["dimplex_controller"]
|
|
72
|
-
|
|
73
|
-
[tool.coverage.report]
|
|
74
|
-
show_missing = true
|
|
75
|
-
# Guardrail so coverage can't silently regress. Raise as coverage improves.
|
|
76
|
-
fail_under = 80
|
|
77
|
-
|
|
78
|
-
[build-system]
|
|
79
|
-
requires = ["poetry-core"]
|
|
80
|
-
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|