dimplex-controller 0.13.1__tar.gz → 0.15.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.15.0}/PKG-INFO +14 -11
- {dimplex_controller-0.13.1 → dimplex_controller-0.15.0}/README.md +8 -3
- {dimplex_controller-0.13.1 → dimplex_controller-0.15.0}/dimplex_controller/__init__.py +6 -0
- {dimplex_controller-0.13.1 → dimplex_controller-0.15.0}/dimplex_controller/capabilities.py +27 -13
- {dimplex_controller-0.13.1 → dimplex_controller-0.15.0}/dimplex_controller/client.py +53 -7
- {dimplex_controller-0.13.1 → dimplex_controller-0.15.0}/dimplex_controller/exceptions.py +22 -2
- {dimplex_controller-0.13.1 → dimplex_controller-0.15.0}/dimplex_controller/models.py +48 -0
- {dimplex_controller-0.13.1 → dimplex_controller-0.15.0}/dimplex_controller/telemetry.py +52 -2
- dimplex_controller-0.15.0/pyproject.toml +96 -0
- dimplex_controller-0.15.0/pyproject.toml.orig +115 -0
- dimplex_controller-0.13.1/pyproject.toml +0 -85
- {dimplex_controller-0.13.1 → dimplex_controller-0.15.0}/LICENSE +0 -0
- {dimplex_controller-0.13.1 → dimplex_controller-0.15.0}/dimplex_controller/auth.py +0 -0
- {dimplex_controller-0.13.1 → dimplex_controller-0.15.0}/dimplex_controller/cli.py +0 -0
- {dimplex_controller-0.13.1 → dimplex_controller-0.15.0}/dimplex_controller/const.py +0 -0
- {dimplex_controller-0.13.1 → dimplex_controller-0.15.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.15.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.9.0,<4
|
|
21
|
+
Requires-Dist: pydantic>=2.0.0,<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
|
|
@@ -283,7 +285,9 @@ Main client class. Construct with an `aiohttp.ClientSession` and a `refresh_toke
|
|
|
283
285
|
| `set_setback_temperature(...)` | Write the setback temperature (untested). |
|
|
284
286
|
| `set_eco_start(hub_id, appliance_ids, enable)` | Toggle EcoStart. |
|
|
285
287
|
| `set_open_window_detection(hub_id, appliance_ids, enable)` | Toggle Open Window Detection. |
|
|
286
|
-
| `
|
|
288
|
+
| `set_hot_water_holiday(hub_id, appliance_ids, until, *, enable, heat_pump)` | Cylinder Holiday on/off (validated on an ASHW cylinder). |
|
|
289
|
+
| `get_heat_pump_hot_water_schedule(...)` | ASHW cylinder schedule as `HeatPumpHwcSchedule` (validated). Writing it answers 403. |
|
|
290
|
+
| `set_hot_water_*(...)` | Remaining cylinder controls (untested — no hardware). |
|
|
287
291
|
| `get_tsi_energy_report(hub_id, ...)` | Returns `TsiEnergyReport`. |
|
|
288
292
|
| `capabilities_for(appliance, *, status, product)` | Derive an `ApplianceCapabilities` matrix. |
|
|
289
293
|
| `export_tokens()` / `apply_tokens(bundle)` | Token persistence helpers. |
|
|
@@ -303,7 +307,7 @@ Main client class. Construct with an `aiohttp.ClientSession` and a `refresh_toke
|
|
|
303
307
|
|
|
304
308
|
- **`DimplexError`** — Base exception.
|
|
305
309
|
- **`DimplexAuthError`** — Authentication or token errors.
|
|
306
|
-
- **`DimplexApiError`** — API returned a non-success status. Contains `status` and `
|
|
310
|
+
- **`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
311
|
- **`DimplexConnectionError`** — Network-level failures.
|
|
308
312
|
|
|
309
313
|
## Troubleshooting
|
|
@@ -400,4 +404,3 @@ Key points:
|
|
|
400
404
|
## Changelog
|
|
401
405
|
|
|
402
406
|
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
|
|
@@ -256,7 +259,9 @@ Main client class. Construct with an `aiohttp.ClientSession` and a `refresh_toke
|
|
|
256
259
|
| `set_setback_temperature(...)` | Write the setback temperature (untested). |
|
|
257
260
|
| `set_eco_start(hub_id, appliance_ids, enable)` | Toggle EcoStart. |
|
|
258
261
|
| `set_open_window_detection(hub_id, appliance_ids, enable)` | Toggle Open Window Detection. |
|
|
259
|
-
| `
|
|
262
|
+
| `set_hot_water_holiday(hub_id, appliance_ids, until, *, enable, heat_pump)` | Cylinder Holiday on/off (validated on an ASHW cylinder). |
|
|
263
|
+
| `get_heat_pump_hot_water_schedule(...)` | ASHW cylinder schedule as `HeatPumpHwcSchedule` (validated). Writing it answers 403. |
|
|
264
|
+
| `set_hot_water_*(...)` | Remaining cylinder controls (untested — no hardware). |
|
|
260
265
|
| `get_tsi_energy_report(hub_id, ...)` | Returns `TsiEnergyReport`. |
|
|
261
266
|
| `capabilities_for(appliance, *, status, product)` | Derive an `ApplianceCapabilities` matrix. |
|
|
262
267
|
| `export_tokens()` / `apply_tokens(bundle)` | Token persistence helpers. |
|
|
@@ -276,7 +281,7 @@ Main client class. Construct with an `aiohttp.ClientSession` and a `refresh_toke
|
|
|
276
281
|
|
|
277
282
|
- **`DimplexError`** — Base exception.
|
|
278
283
|
- **`DimplexAuthError`** — Authentication or token errors.
|
|
279
|
-
- **`DimplexApiError`** — API returned a non-success status. Contains `status` and `
|
|
284
|
+
- **`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
285
|
- **`DimplexConnectionError`** — Network-level failures.
|
|
281
286
|
|
|
282
287
|
## Troubleshooting
|
|
@@ -45,6 +45,8 @@ from .models import (
|
|
|
45
45
|
ApplianceModeStatus,
|
|
46
46
|
ApplianceStatus,
|
|
47
47
|
AutomaticProvisioning,
|
|
48
|
+
HeatPumpHwcPeriod,
|
|
49
|
+
HeatPumpHwcSchedule,
|
|
48
50
|
Hub,
|
|
49
51
|
HygieneFrequency,
|
|
50
52
|
ProductModel,
|
|
@@ -62,6 +64,7 @@ from .telemetry import (
|
|
|
62
64
|
EnergySummary,
|
|
63
65
|
filter_telemetry_points,
|
|
64
66
|
parse_telemetry_points,
|
|
67
|
+
reported_energy,
|
|
65
68
|
summarise_energy,
|
|
66
69
|
)
|
|
67
70
|
|
|
@@ -83,6 +86,8 @@ __all__ = [
|
|
|
83
86
|
"SetbackStatus",
|
|
84
87
|
"TimerMode",
|
|
85
88
|
"TimerModeSettings",
|
|
89
|
+
"HeatPumpHwcPeriod",
|
|
90
|
+
"HeatPumpHwcSchedule",
|
|
86
91
|
"TimerPeriod",
|
|
87
92
|
"AutomaticProvisioning",
|
|
88
93
|
"ProductModel",
|
|
@@ -98,6 +103,7 @@ __all__ = [
|
|
|
98
103
|
"NULL_DATETIME",
|
|
99
104
|
"parse_telemetry_points",
|
|
100
105
|
"filter_telemetry_points",
|
|
106
|
+
"reported_energy",
|
|
101
107
|
"summarise_energy",
|
|
102
108
|
"EnergySummary",
|
|
103
109
|
"VALUE_KEY_T1",
|
|
@@ -24,6 +24,14 @@ class ApplianceCapabilities:
|
|
|
24
24
|
Flags are best-effort: when status/product metadata is incomplete the
|
|
25
25
|
library prefers enabling known cloud control paths (boost/away/OWD) so
|
|
26
26
|
users are not locked out of working RPCs.
|
|
27
|
+
|
|
28
|
+
One flag is weaker than the rest and must not be used as a gate:
|
|
29
|
+
:attr:`energy_meter` is a *family hint* derived from the nameplate, so it is
|
|
30
|
+
true for essentially every appliance with a rated power. When the question is
|
|
31
|
+
"has this appliance ever reported energy?", ask the telemetry —
|
|
32
|
+
:func:`dimplex_controller.reported_energy`, or
|
|
33
|
+
:func:`dimplex_controller.summarise_energy` when the totals are wanted too
|
|
34
|
+
(dimplex-controller-py#113).
|
|
27
35
|
"""
|
|
28
36
|
|
|
29
37
|
boost: bool = True
|
|
@@ -39,6 +47,7 @@ class ApplianceCapabilities:
|
|
|
39
47
|
# setpoint path. Preferred over rewriting the schedule via SetTimerMode,
|
|
40
48
|
# which Quantum rejects with HTTP 403.
|
|
41
49
|
setpoint_write: bool = True
|
|
50
|
+
# Family hint, not proof of metering — see the class docstring before gating on it.
|
|
42
51
|
energy_meter: bool = False
|
|
43
52
|
storage: bool = False
|
|
44
53
|
hot_water: bool = False
|
|
@@ -162,15 +171,23 @@ def capabilities_for(
|
|
|
162
171
|
heat_pump = True
|
|
163
172
|
hot_water = True
|
|
164
173
|
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
if status is not None and status.AvailableHotWater is not None:
|
|
175
|
+
hot_water = True
|
|
176
|
+
|
|
177
|
+
# A cylinder reports its *water* target in ActiveSetPointTemperature (45 °C
|
|
178
|
+
# on a live ASHW cylinder), so only a room temperature makes it room climate.
|
|
179
|
+
# Without one, the room-heater features default to off — a live cylinder
|
|
180
|
+
# sends null for every one of them — unless its status reports them.
|
|
181
|
+
climate = not hot_water or (status is not None and status.RoomTemperature is not None)
|
|
182
|
+
|
|
183
|
+
boost = climate
|
|
184
|
+
away = climate
|
|
167
185
|
advance = True
|
|
168
|
-
open_window =
|
|
169
|
-
eco_start =
|
|
170
|
-
setback_read =
|
|
171
|
-
frost =
|
|
186
|
+
open_window = climate
|
|
187
|
+
eco_start = climate
|
|
188
|
+
setback_read = climate
|
|
189
|
+
frost = climate
|
|
172
190
|
timer = True
|
|
173
|
-
climate = not hot_water # cylinder-only appliances are not room climate
|
|
174
191
|
|
|
175
192
|
if status is not None:
|
|
176
193
|
if status.BoostDuration is not None or status.BoostTemperature is not None:
|
|
@@ -183,10 +200,6 @@ def capabilities_for(
|
|
|
183
200
|
eco_start = True
|
|
184
201
|
if status.SetbackEnabled is not None or status.SetbackTemperature is not None:
|
|
185
202
|
setback_read = True
|
|
186
|
-
if status.AvailableHotWater is not None:
|
|
187
|
-
hot_water = True
|
|
188
|
-
if status.RoomTemperature is not None or status.ActiveSetPointTemperature is not None:
|
|
189
|
-
climate = True
|
|
190
203
|
|
|
191
204
|
# Advance only means something for a scheduled room heater; a cylinder has
|
|
192
205
|
# no "next comfort period" to jump to.
|
|
@@ -200,10 +213,11 @@ def capabilities_for(
|
|
|
200
213
|
open_window=open_window,
|
|
201
214
|
eco_start=eco_start,
|
|
202
215
|
setback_read=setback_read,
|
|
203
|
-
setback_write=
|
|
216
|
+
setback_write=climate,
|
|
204
217
|
frost=frost,
|
|
205
218
|
timer=timer,
|
|
206
|
-
|
|
219
|
+
# The room setpoint endpoint; cylinders have their own (Set*TemperatureHwc).
|
|
220
|
+
setpoint_write=climate,
|
|
207
221
|
energy_meter=energy_meter,
|
|
208
222
|
storage=storage,
|
|
209
223
|
hot_water=hot_water,
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
|
+
import math
|
|
6
7
|
import random
|
|
7
8
|
from datetime import datetime, timedelta, timezone
|
|
8
9
|
from typing import Any
|
|
@@ -36,6 +37,7 @@ from .models import (
|
|
|
36
37
|
ApplianceModeSettings,
|
|
37
38
|
ApplianceModeStatus,
|
|
38
39
|
ApplianceStatus,
|
|
40
|
+
HeatPumpHwcSchedule,
|
|
39
41
|
Hub,
|
|
40
42
|
HygieneFrequency,
|
|
41
43
|
ProductModel,
|
|
@@ -274,12 +276,14 @@ class DimplexControl:
|
|
|
274
276
|
attempts - 1,
|
|
275
277
|
delay,
|
|
276
278
|
)
|
|
277
|
-
last_error = DimplexApiError(resp.status, text)
|
|
279
|
+
last_error = DimplexApiError(resp.status, text, transient=resp.status in _RETRYABLE_STATUS)
|
|
278
280
|
await asyncio.sleep(delay)
|
|
279
281
|
continue
|
|
280
282
|
|
|
281
283
|
_LOGGER.error("API request failed: %s - %s", resp.status, text)
|
|
282
|
-
|
|
284
|
+
# A 5xx/429 that reaches here was retried and still failed, so it
|
|
285
|
+
# is transient (back off); a 4xx is a hard failure (surface it).
|
|
286
|
+
raise DimplexApiError(resp.status, text, transient=resp.status in _RETRYABLE_STATUS)
|
|
283
287
|
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
|
284
288
|
if allow_retry and attempt + 1 < attempts:
|
|
285
289
|
delay = self._backoff_seconds(attempt)
|
|
@@ -838,6 +842,42 @@ class DimplexControl:
|
|
|
838
842
|
endpoint=endpoint,
|
|
839
843
|
)
|
|
840
844
|
|
|
845
|
+
async def set_hot_water_holiday(
|
|
846
|
+
self,
|
|
847
|
+
hub_id: str,
|
|
848
|
+
appliance_ids: list[str],
|
|
849
|
+
until: datetime | None = None,
|
|
850
|
+
*,
|
|
851
|
+
enable: bool = True,
|
|
852
|
+
heat_pump: bool = False,
|
|
853
|
+
) -> None:
|
|
854
|
+
"""Put a cylinder on Holiday (standby) until ``until``, or clear it.
|
|
855
|
+
|
|
856
|
+
Verified on a live ASHW cylinder (EDL200UK-630RF) with ``heat_pump=True``:
|
|
857
|
+
engaging moved ``ApplianceModes`` 8192 → 8200 and set
|
|
858
|
+
``HolidayNumOfDays``, heating stopped, and clearing restored 8192. The
|
|
859
|
+
cylinder counts holidays in whole days, so ``until`` is also sent as a
|
|
860
|
+
rounded-up day count; a caller that wants a time of day has to clear
|
|
861
|
+
Holiday itself at that time.
|
|
862
|
+
"""
|
|
863
|
+
days = 0
|
|
864
|
+
if enable:
|
|
865
|
+
if until is None:
|
|
866
|
+
raise ValueError("a holiday needs an end time")
|
|
867
|
+
end = until if until.tzinfo is not None else until.replace(tzinfo=timezone.utc)
|
|
868
|
+
seconds = (end - datetime.now(timezone.utc)).total_seconds()
|
|
869
|
+
days = max(1, math.ceil(seconds / 86400))
|
|
870
|
+
endpoint = "/RemoteControl/SetApplianceModeHeatPumpHwc" if heat_pump else "/RemoteControl/SetApplianceModeHwc"
|
|
871
|
+
await self.set_mode_flag(
|
|
872
|
+
hub_id,
|
|
873
|
+
appliance_ids,
|
|
874
|
+
ApplianceModeFlag.HOLIDAY,
|
|
875
|
+
enable=enable,
|
|
876
|
+
until=until,
|
|
877
|
+
number_of_days=days,
|
|
878
|
+
endpoint=endpoint,
|
|
879
|
+
)
|
|
880
|
+
|
|
841
881
|
async def set_hot_water_boost_temperature(
|
|
842
882
|
self, hub_id: str, appliance_ids: list[str], temperature: float, *, enable: bool = True
|
|
843
883
|
) -> None:
|
|
@@ -900,10 +940,12 @@ class DimplexControl:
|
|
|
900
940
|
endpoint=endpoint,
|
|
901
941
|
)
|
|
902
942
|
|
|
903
|
-
async def get_heat_pump_hot_water_schedule(self, hub_id: str, appliance_id: str) ->
|
|
904
|
-
"""Read an ASHW cylinder's schedule.
|
|
943
|
+
async def get_heat_pump_hot_water_schedule(self, hub_id: str, appliance_id: str) -> HeatPumpHwcSchedule:
|
|
944
|
+
"""Read an ASHW cylinder's weekly schedule.
|
|
905
945
|
|
|
906
|
-
|
|
946
|
+
Verified on a live ASHW cylinder (EDL200UK-630RF). The reply does not fit
|
|
947
|
+
:class:`TimerModeSettings` — there is no ``TimerMode`` and each period is
|
|
948
|
+
``{DayOfWeek, StartTime, Mode}`` — hence :class:`HeatPumpHwcSchedule`.
|
|
907
949
|
"""
|
|
908
950
|
payload = {"HubId": hub_id, "ApplianceId": appliance_id}
|
|
909
951
|
data = await self._request(
|
|
@@ -911,12 +953,16 @@ class DimplexControl:
|
|
|
911
953
|
"/RemoteControl/ApiGetTimerModeDetailsForHeatPumpHwcAppliance",
|
|
912
954
|
json=payload,
|
|
913
955
|
)
|
|
914
|
-
return
|
|
956
|
+
return HeatPumpHwcSchedule.model_validate(data)
|
|
915
957
|
|
|
916
958
|
async def set_heat_pump_hot_water_schedule(self, settings: TimerModeSettings) -> TimerModeSettings:
|
|
917
959
|
"""Write an ASHW cylinder's schedule periods.
|
|
918
960
|
|
|
919
|
-
.. warning::
|
|
961
|
+
.. warning:: Does not work yet. On a live ASHW cylinder this endpoint
|
|
962
|
+
answered HTTP 403 to every body tried — wrapped in ``TimerModeSettings``
|
|
963
|
+
or ``Settings``, with or without ``TimerMode``, integer or ``HH:MM:SS``
|
|
964
|
+
times, and app versions up to 3.0.0 — while unwrapped bodies got 400.
|
|
965
|
+
The request the official app sends has not been captured.
|
|
920
966
|
"""
|
|
921
967
|
payload = {"TimerModeSettings": settings.model_dump(mode="json")}
|
|
922
968
|
await self._request("POST", "/RemoteControl/UpdateHeatPumpHwcSchedulePeriods", json=payload)
|
|
@@ -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
|
|
|
@@ -255,6 +255,31 @@ class TimerModeSettings(BaseModel):
|
|
|
255
255
|
TimerPeriods: list[TimerPeriod] = Field(default_factory=list)
|
|
256
256
|
|
|
257
257
|
|
|
258
|
+
class HeatPumpHwcPeriod(BaseModel):
|
|
259
|
+
"""One period of an ASHW cylinder's weekly schedule.
|
|
260
|
+
|
|
261
|
+
A live cylinder returns one ``{DayOfWeek, StartTime: 0, Mode: 2}`` per day.
|
|
262
|
+
``DayOfWeek`` is .NET's (0 = Sunday). ``StartTime`` is an integer, presumed
|
|
263
|
+
minutes after midnight, and what each ``Mode`` value means is not yet known.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
DayOfWeek: int
|
|
267
|
+
StartTime: int
|
|
268
|
+
Mode: int
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class HeatPumpHwcSchedule(BaseModel):
|
|
272
|
+
"""``ApiGetTimerModeDetailsForHeatPumpHwcAppliance`` reply.
|
|
273
|
+
|
|
274
|
+
Unlike the room-heater :class:`TimerModeSettings` it has no ``TimerMode``
|
|
275
|
+
and its periods carry a mode instead of an end time and temperature.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
HubId: str
|
|
279
|
+
ApplianceId: str
|
|
280
|
+
TimerPeriods: list[HeatPumpHwcPeriod] = Field(default_factory=list)
|
|
281
|
+
|
|
282
|
+
|
|
258
283
|
class UserContext(BaseModel):
|
|
259
284
|
Id: str
|
|
260
285
|
EmailAddress: str | None = None
|
|
@@ -286,6 +311,29 @@ class ApplianceStatus(BaseModel):
|
|
|
286
311
|
LockStatus: int | None = None
|
|
287
312
|
ErrorCode: str | None = None
|
|
288
313
|
WarningCode: str | None = None
|
|
314
|
+
# Hot-water cylinder fields, as a live ASHW cylinder (EDL200UK-630RF) sends
|
|
315
|
+
# them. AvailableHotWater above is litres of usable hot water (the app shows
|
|
316
|
+
# it as "336 L Hot water").
|
|
317
|
+
Capacity: int | None = None # tank size, litres
|
|
318
|
+
HeatingUp: bool | None = None
|
|
319
|
+
IsGreenEnergyActive: bool | None = None
|
|
320
|
+
HygieneFrequency: int | None = None # EFrequency, see HygieneFrequency
|
|
321
|
+
HygieneTemperature: float | None = None
|
|
322
|
+
HolidayNumOfDays: int | None = None
|
|
323
|
+
# Present in the same reply, always null on the cylinder seen so far.
|
|
324
|
+
DelayedStartTime: str | None = None
|
|
325
|
+
DsmStatus: int | None = None
|
|
326
|
+
ModifiedPower: float | None = None
|
|
327
|
+
ConnectedChargeProfiles: Any = None
|
|
328
|
+
StandaloneChargeProfiles: Any = None
|
|
329
|
+
|
|
330
|
+
@field_validator("ErrorCode", "WarningCode", mode="before")
|
|
331
|
+
@classmethod
|
|
332
|
+
def _coerce_code(cls, value: Any) -> Any:
|
|
333
|
+
# GetApplianceOverview sends these as integers (0 when healthy).
|
|
334
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
335
|
+
return str(value)
|
|
336
|
+
return value
|
|
289
337
|
|
|
290
338
|
@property
|
|
291
339
|
def mode_flags(self) -> ApplianceModeFlag:
|
|
@@ -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:
|
|
@@ -213,6 +222,34 @@ def parse_telemetry_points(
|
|
|
213
222
|
return out
|
|
214
223
|
|
|
215
224
|
|
|
225
|
+
def reported_energy(
|
|
226
|
+
points: Any,
|
|
227
|
+
*,
|
|
228
|
+
value_keys: tuple[str, ...] | None = None,
|
|
229
|
+
) -> bool:
|
|
230
|
+
"""Return True when telemetry carries usable points for an appliance.
|
|
231
|
+
|
|
232
|
+
The evidence-based counterpart to
|
|
233
|
+
:attr:`ApplianceCapabilities.energy_meter <.capabilities.ApplianceCapabilities>`,
|
|
234
|
+
which is a family hint derived from the nameplate and is therefore true for
|
|
235
|
+
essentially every appliance — it cannot separate a genuinely metered one from an
|
|
236
|
+
appliance the hub never reports energy for (dimplex-controller-py#113).
|
|
237
|
+
|
|
238
|
+
Pass the appliance's entry from
|
|
239
|
+
:attr:`TsiEnergyReport.ApplianceTelemetryData
|
|
240
|
+
<.models.TsiEnergyReport>` (raw entries are normalised by
|
|
241
|
+
:func:`parse_telemetry_points`, so both raw and already-parsed points are
|
|
242
|
+
accepted). Nothing recognised, an empty list, or a value the parser skips all
|
|
243
|
+
answer ``False`` — which is the question a consumer gating entity creation
|
|
244
|
+
actually has.
|
|
245
|
+
|
|
246
|
+
Only presence is decided here. A caller that also wants the figures should call
|
|
247
|
+
:func:`parse_telemetry_points` or :func:`summarise_energy` once and test the
|
|
248
|
+
result, rather than parse a series twice.
|
|
249
|
+
"""
|
|
250
|
+
return bool(parse_telemetry_points(points, value_keys=value_keys))
|
|
251
|
+
|
|
252
|
+
|
|
216
253
|
def _ensure_aware(ts: datetime, default_tz: tzinfo) -> datetime:
|
|
217
254
|
"""Return ``ts`` with a timezone; naive datetimes are assumed ``default_tz``."""
|
|
218
255
|
if ts.tzinfo is None:
|
|
@@ -276,6 +313,11 @@ def summarise_energy(
|
|
|
276
313
|
``points`` may be raw cloud payloads or already-parsed
|
|
277
314
|
:data:`TelemetryPoint` tuples. When raw, they are passed through
|
|
278
315
|
:func:`parse_telemetry_points` first.
|
|
316
|
+
|
|
317
|
+
Points with no parseable timestamp (``ts is None``) are excluded from the
|
|
318
|
+
total in **every** mode. They cannot be placed on a calendar day, so
|
|
319
|
+
counting them only in ``lifetime`` would make the cumulative total disagree
|
|
320
|
+
with the sum of the per-day totals (dimplex-controller-py#120).
|
|
279
321
|
"""
|
|
280
322
|
parsed: list[TelemetryPoint]
|
|
281
323
|
if (
|
|
@@ -306,8 +348,16 @@ def summarise_energy(
|
|
|
306
348
|
mode="daily",
|
|
307
349
|
)
|
|
308
350
|
|
|
309
|
-
# lifetime —
|
|
310
|
-
|
|
351
|
+
# lifetime / window — totals must reconcile with the per-day totals, so a point
|
|
352
|
+
# with no parseable timestamp is dropped from both. A ``ts is None`` point cannot
|
|
353
|
+
# be placed on any calendar day (``daily``/``window`` already drop it via
|
|
354
|
+
# ``filter_telemetry_points``), so counting it only in the lifetime total made the
|
|
355
|
+
# cumulative meter disagree with the sum of its days — silent drift into the HA
|
|
356
|
+
# Energy Dashboard's TOTAL_INCREASING sensor (dimplex-controller-py#120).
|
|
357
|
+
if mode == "window":
|
|
358
|
+
selected = filter_telemetry_points(parsed, start=start, end=end, tz=tz)
|
|
359
|
+
else:
|
|
360
|
+
selected = [(ts, value) for ts, value in parsed if ts is not None]
|
|
311
361
|
|
|
312
362
|
timestamps = [ts for ts, _ in selected if ts is not None]
|
|
313
363
|
total = sum(v for _, v in selected)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dimplex-controller"
|
|
3
|
+
version = "0.15.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.9.0,<4",
|
|
31
|
+
"pydantic>=2.0.0,<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,115 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dimplex-controller"
|
|
3
|
+
version = "0.15.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
|
+
# The upper bounds keep the previous caret semantics (a caret range is
|
|
25
|
+
# `>=X,<next-major`), so consumers gain no surprise from the uv migration.
|
|
26
|
+
dependencies = [
|
|
27
|
+
# Lower bounds are what the code needs, not what uv.lock happens to resolve.
|
|
28
|
+
# They were raised to the locked versions when the project migrated to uv and
|
|
29
|
+
# bumped everything in one pass (#110), which made this library uninstallable
|
|
30
|
+
# alongside Home Assistant: the integration's test stack pins pydantic 2.13.4
|
|
31
|
+
# while this demanded >=2.13.5, so `dimplex-controller>=0.14.0` resolved to
|
|
32
|
+
# nothing and the HA integration could not adopt the 0.14.0 ErrorCode/WarningCode
|
|
33
|
+
# fix (dimplex-controller-hass#256).
|
|
34
|
+
#
|
|
35
|
+
# Nothing here needs either new API: plain ``BaseModel``, ``field_validator``,
|
|
36
|
+
# ``ConfigDict`` and an aiohttp ``ClientSession`` are all 2.0/3.9-era.
|
|
37
|
+
"aiohttp>=3.9.0,<4",
|
|
38
|
+
"pydantic>=2.0.0,<3",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/KRoperUK/dimplex-controller-py"
|
|
43
|
+
Repository = "https://github.com/KRoperUK/dimplex-controller-py"
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
dimplex = "dimplex_controller.cli:main"
|
|
47
|
+
|
|
48
|
+
# PEP 735 dependency groups: not installed into a consumer's environment, only
|
|
49
|
+
# into a development one.
|
|
50
|
+
[dependency-groups]
|
|
51
|
+
dev = [
|
|
52
|
+
"pytest>=9.1.1,<10",
|
|
53
|
+
"pytest-asyncio>=1.4.0,<2",
|
|
54
|
+
"pytest-cov>=7.1.0,<8",
|
|
55
|
+
"aresponses>=3.0.0,<4",
|
|
56
|
+
"ruff>=0.16.7,<0.17",
|
|
57
|
+
"pre-commit>=4.6.2,<5",
|
|
58
|
+
"twine>=7.0.0,<8",
|
|
59
|
+
"python-dotenv>=1.2.3,<2",
|
|
60
|
+
"mypy>=2.3.1,<3",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
line-length = 120
|
|
65
|
+
target-version = "py310"
|
|
66
|
+
# Python inside Markdown is not formatted here. The code blocks in README.md and
|
|
67
|
+
# docs/ carry deliberate column alignment (see the temperature-constant table in
|
|
68
|
+
# docs/api-reference.md) and ruff's formatter normalises comment spacing, so
|
|
69
|
+
# formatting them would trade readable docs for tool uniformity. This also keeps a
|
|
70
|
+
# bare `ruff format .` in step with the pre-commit hooks, which are python/pyi only.
|
|
71
|
+
extend-exclude = ["*.md"]
|
|
72
|
+
|
|
73
|
+
[tool.ruff.lint]
|
|
74
|
+
select = ["E", "F", "I", "W", "UP", "B", "SIM"]
|
|
75
|
+
# E501 (line length) is handled by the formatter; long strings/comments are fine.
|
|
76
|
+
ignore = ["E501"]
|
|
77
|
+
|
|
78
|
+
[tool.mypy]
|
|
79
|
+
python_version = "3.10"
|
|
80
|
+
files = ["dimplex_controller"]
|
|
81
|
+
ignore_missing_imports = true
|
|
82
|
+
# `strict` rather than the four flags this used to carry (disallow_untyped_defs,
|
|
83
|
+
# warn_return_any, warn_unused_configs, warn_redundant_casts). The library is the
|
|
84
|
+
# layer the Home Assistant integration trusts for models, capabilities and error
|
|
85
|
+
# types, and that consumer runs strict mypy over its own code — so a library
|
|
86
|
+
# checked more loosely could hand it types that do not hold
|
|
87
|
+
# (dimplex-controller-hass#202).
|
|
88
|
+
#
|
|
89
|
+
# Raising it surfaced seven stale `# type: ignore` comments, all of which mypy
|
|
90
|
+
# reported as unused, plus one missing generic argument. No real type errors.
|
|
91
|
+
strict = true
|
|
92
|
+
|
|
93
|
+
[tool.pytest.ini_options]
|
|
94
|
+
# All async tests use asyncio; "auto" means new tests don't each need the
|
|
95
|
+
# @pytest.mark.asyncio decorator (existing markers remain valid).
|
|
96
|
+
asyncio_mode = "auto"
|
|
97
|
+
testpaths = ["tests"]
|
|
98
|
+
|
|
99
|
+
[tool.coverage.run]
|
|
100
|
+
source = ["dimplex_controller"]
|
|
101
|
+
|
|
102
|
+
[tool.coverage.report]
|
|
103
|
+
show_missing = true
|
|
104
|
+
# Guardrail so coverage can't silently regress. Raise as coverage improves.
|
|
105
|
+
fail_under = 80
|
|
106
|
+
|
|
107
|
+
[build-system]
|
|
108
|
+
requires = ["uv_build>=0.11.19,<0.13.0"]
|
|
109
|
+
build-backend = "uv_build"
|
|
110
|
+
|
|
111
|
+
[tool.uv.build-backend]
|
|
112
|
+
# The package is at the repository root, not under src/, which is what uv_build
|
|
113
|
+
# assumes by default. The module name is inferred from the project name
|
|
114
|
+
# (dimplex-controller -> dimplex_controller), so it needs no override.
|
|
115
|
+
module-root = ""
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
[tool.poetry]
|
|
2
|
-
name = "dimplex-controller"
|
|
3
|
-
version = "0.13.1"
|
|
4
|
-
description = "Unofficial async Python client for Dimplex (GDHV IoT) heating appliances — not affiliated with Glen Dimplex"
|
|
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
|
-
# `strict` rather than the four flags this used to carry (disallow_untyped_defs,
|
|
59
|
-
# warn_return_any, warn_unused_configs, warn_redundant_casts). The library is the
|
|
60
|
-
# layer the Home Assistant integration trusts for models, capabilities and error
|
|
61
|
-
# types, and that consumer runs strict mypy over its own code — so a library
|
|
62
|
-
# checked more loosely could hand it types that do not hold
|
|
63
|
-
# (dimplex-controller-hass#202).
|
|
64
|
-
#
|
|
65
|
-
# Raising it surfaced seven stale `# type: ignore` comments, all of which mypy
|
|
66
|
-
# reported as unused, plus one missing generic argument. No real type errors.
|
|
67
|
-
strict = true
|
|
68
|
-
|
|
69
|
-
[tool.pytest.ini_options]
|
|
70
|
-
# All async tests use asyncio; "auto" means new tests don't each need the
|
|
71
|
-
# @pytest.mark.asyncio decorator (existing markers remain valid).
|
|
72
|
-
asyncio_mode = "auto"
|
|
73
|
-
testpaths = ["tests"]
|
|
74
|
-
|
|
75
|
-
[tool.coverage.run]
|
|
76
|
-
source = ["dimplex_controller"]
|
|
77
|
-
|
|
78
|
-
[tool.coverage.report]
|
|
79
|
-
show_missing = true
|
|
80
|
-
# Guardrail so coverage can't silently regress. Raise as coverage improves.
|
|
81
|
-
fail_under = 80
|
|
82
|
-
|
|
83
|
-
[build-system]
|
|
84
|
-
requires = ["poetry-core"]
|
|
85
|
-
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|