dimplex-controller 0.13.1__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.1 → dimplex_controller-0.14.0}/PKG-INFO +11 -10
- {dimplex_controller-0.13.1 → dimplex_controller-0.14.0}/README.md +5 -2
- {dimplex_controller-0.13.1 → dimplex_controller-0.14.0}/dimplex_controller/client.py +4 -2
- {dimplex_controller-0.13.1 → dimplex_controller-0.14.0}/dimplex_controller/exceptions.py +22 -2
- {dimplex_controller-0.13.1 → dimplex_controller-0.14.0}/dimplex_controller/models.py +8 -0
- {dimplex_controller-0.13.1 → dimplex_controller-0.14.0}/dimplex_controller/telemetry.py +24 -2
- dimplex_controller-0.14.0/pyproject.toml +96 -0
- dimplex_controller-0.13.1/pyproject.toml → dimplex_controller-0.14.0/pyproject.toml.orig +46 -25
- {dimplex_controller-0.13.1 → dimplex_controller-0.14.0}/LICENSE +0 -0
- {dimplex_controller-0.13.1 → dimplex_controller-0.14.0}/dimplex_controller/__init__.py +0 -0
- {dimplex_controller-0.13.1 → dimplex_controller-0.14.0}/dimplex_controller/auth.py +0 -0
- {dimplex_controller-0.13.1 → dimplex_controller-0.14.0}/dimplex_controller/capabilities.py +0 -0
- {dimplex_controller-0.13.1 → dimplex_controller-0.14.0}/dimplex_controller/cli.py +0 -0
- {dimplex_controller-0.13.1 → dimplex_controller-0.14.0}/dimplex_controller/const.py +0 -0
- {dimplex_controller-0.13.1 → 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.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: Unofficial async Python client for Dimplex (GDHV IoT) heating appliances — not affiliated with Glen Dimplex
|
|
5
|
-
License: MIT
|
|
6
|
-
License-File: LICENSE
|
|
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
|
|
@@ -104,9 +103,12 @@ pip install .
|
|
|
104
103
|
```bash
|
|
105
104
|
git clone https://github.com/KRoperUK/dimplex-controller-py.git
|
|
106
105
|
cd dimplex-controller-py
|
|
107
|
-
|
|
106
|
+
uv sync
|
|
108
107
|
```
|
|
109
108
|
|
|
109
|
+
`uv sync` creates `.venv`, installs the library in editable mode, and installs the
|
|
110
|
+
dev dependency group from `uv.lock`.
|
|
111
|
+
|
|
110
112
|
> **Requires:** Python 3.10 or later.
|
|
111
113
|
|
|
112
114
|
## Quick start
|
|
@@ -303,7 +305,7 @@ Main client class. Construct with an `aiohttp.ClientSession` and a `refresh_toke
|
|
|
303
305
|
|
|
304
306
|
- **`DimplexError`** — Base exception.
|
|
305
307
|
- **`DimplexAuthError`** — Authentication or token errors.
|
|
306
|
-
- **`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`.
|
|
307
309
|
- **`DimplexConnectionError`** — Network-level failures.
|
|
308
310
|
|
|
309
311
|
## Troubleshooting
|
|
@@ -400,4 +402,3 @@ Key points:
|
|
|
400
402
|
## Changelog
|
|
401
403
|
|
|
402
404
|
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
403
|
-
|
|
@@ -77,9 +77,12 @@ pip install .
|
|
|
77
77
|
```bash
|
|
78
78
|
git clone https://github.com/KRoperUK/dimplex-controller-py.git
|
|
79
79
|
cd dimplex-controller-py
|
|
80
|
-
|
|
80
|
+
uv sync
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
`uv sync` creates `.venv`, installs the library in editable mode, and installs the
|
|
84
|
+
dev dependency group from `uv.lock`.
|
|
85
|
+
|
|
83
86
|
> **Requires:** Python 3.10 or later.
|
|
84
87
|
|
|
85
88
|
## Quick start
|
|
@@ -276,7 +279,7 @@ Main client class. Construct with an `aiohttp.ClientSession` and a `refresh_toke
|
|
|
276
279
|
|
|
277
280
|
- **`DimplexError`** — Base exception.
|
|
278
281
|
- **`DimplexAuthError`** — Authentication or token errors.
|
|
279
|
-
- **`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`.
|
|
280
283
|
- **`DimplexConnectionError`** — Network-level failures.
|
|
281
284
|
|
|
282
285
|
## Troubleshooting
|
|
@@ -274,12 +274,14 @@ class DimplexControl:
|
|
|
274
274
|
attempts - 1,
|
|
275
275
|
delay,
|
|
276
276
|
)
|
|
277
|
-
last_error = DimplexApiError(resp.status, text)
|
|
277
|
+
last_error = DimplexApiError(resp.status, text, transient=resp.status in _RETRYABLE_STATUS)
|
|
278
278
|
await asyncio.sleep(delay)
|
|
279
279
|
continue
|
|
280
280
|
|
|
281
281
|
_LOGGER.error("API request failed: %s - %s", resp.status, text)
|
|
282
|
-
|
|
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)
|
|
283
285
|
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
|
284
286
|
if allow_retry and attempt + 1 < attempts:
|
|
285
287
|
delay = self._backoff_seconds(attempt)
|
|
@@ -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
|
|
|
@@ -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."""
|
|
@@ -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 (
|
|
@@ -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"
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
[
|
|
1
|
+
[project]
|
|
2
2
|
name = "dimplex-controller"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.14.0"
|
|
4
4
|
description = "Unofficial async Python client for Dimplex (GDHV IoT) heating appliances — not affiliated with Glen Dimplex"
|
|
5
|
-
authors = ["Kieran Roper"]
|
|
5
|
+
authors = [{ name = "Kieran Roper" }]
|
|
6
6
|
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
7
8
|
readme = "README.md"
|
|
8
|
-
|
|
9
|
-
repository = "https://github.com/KRoperUK/dimplex-controller-py"
|
|
9
|
+
requires-python = ">=3.10,<4.0"
|
|
10
10
|
keywords = ["dimplex", "heating", "control", "iot", "asyncio"]
|
|
11
11
|
classifiers = [
|
|
12
12
|
"Development Status :: 5 - Production/Stable",
|
|
13
13
|
"Intended Audience :: Developers",
|
|
14
|
-
"License :: OSI Approved :: MIT License",
|
|
15
14
|
"Natural Language :: English",
|
|
16
15
|
"Operating System :: OS Independent",
|
|
17
16
|
"Programming Language :: Python :: 3",
|
|
@@ -19,32 +18,48 @@ classifiers = [
|
|
|
19
18
|
"Programming Language :: Python :: 3.11",
|
|
20
19
|
"Programming Language :: Python :: 3.12",
|
|
21
20
|
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
22
|
"Topic :: Home Automation",
|
|
23
23
|
]
|
|
24
|
-
|
|
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
|
+
]
|
|
25
31
|
|
|
26
|
-
[
|
|
27
|
-
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/KRoperUK/dimplex-controller-py"
|
|
34
|
+
Repository = "https://github.com/KRoperUK/dimplex-controller-py"
|
|
28
35
|
|
|
29
|
-
[
|
|
30
|
-
|
|
31
|
-
aiohttp = "^3.9.0"
|
|
32
|
-
pydantic = "^2.0.0"
|
|
36
|
+
[project.scripts]
|
|
37
|
+
dimplex = "dimplex_controller.cli:main"
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
]
|
|
44
53
|
|
|
45
54
|
[tool.ruff]
|
|
46
55
|
line-length = 120
|
|
47
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"]
|
|
48
63
|
|
|
49
64
|
[tool.ruff.lint]
|
|
50
65
|
select = ["E", "F", "I", "W", "UP", "B", "SIM"]
|
|
@@ -81,5 +96,11 @@ show_missing = true
|
|
|
81
96
|
fail_under = 80
|
|
82
97
|
|
|
83
98
|
[build-system]
|
|
84
|
-
requires = ["
|
|
85
|
-
build-backend = "
|
|
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 = ""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|