dimplex-controller 0.3.0__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dimplex-controller
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Python client for Dimplex heating controllers (GDHV IoT)
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -2,7 +2,8 @@
2
2
 
3
3
  from .client import DimplexControl
4
4
  from .exceptions import DimplexApiError, DimplexAuthError, DimplexConnectionError, DimplexError
5
- from .models import Appliance, ApplianceModeSettings, ApplianceStatus, Hub, Zone
5
+ from .models import Appliance, ApplianceModeSettings, ApplianceStatus, Hub, TsiEnergyReport, Zone
6
+ from .telemetry import parse_telemetry_points
6
7
 
7
8
  __all__ = [
8
9
  "DimplexControl",
@@ -11,6 +12,8 @@ __all__ = [
11
12
  "Appliance",
12
13
  "ApplianceStatus",
13
14
  "ApplianceModeSettings",
15
+ "TsiEnergyReport",
16
+ "parse_telemetry_points",
14
17
  "DimplexError",
15
18
  "DimplexApiError",
16
19
  "DimplexAuthError",
@@ -3,7 +3,6 @@ import logging
3
3
  import os
4
4
  import re
5
5
  import time
6
- from typing import Dict, Optional
7
6
  from urllib.parse import parse_qs, urlparse
8
7
 
9
8
  import aiohttp
@@ -24,11 +23,11 @@ _LOGGER = logging.getLogger(__name__)
24
23
  class AuthManager:
25
24
  """Manages authentication for Dimplex Control."""
26
25
 
27
- def __init__(self, session: aiohttp.ClientSession, token_data: Optional[Dict] = None):
26
+ def __init__(self, session: aiohttp.ClientSession, token_data: dict | None = None):
28
27
  """Initialize the auth manager."""
29
28
  self._session = session
30
- self._access_token: Optional[str] = token_data.get("access_token") if token_data else None
31
- self._refresh_token: Optional[str] = token_data.get("refresh_token") if token_data else None
29
+ self._access_token: str | None = token_data.get("access_token") if token_data else None
30
+ self._refresh_token: str | None = token_data.get("refresh_token") if token_data else None
32
31
  self._expires_at: float = token_data.get("expires_at", 0) if token_data else 0
33
32
 
34
33
  @property
@@ -42,11 +41,11 @@ class AuthManager:
42
41
  raise DimplexAuthError("No refresh token available. User must authenticate first.")
43
42
 
44
43
  if self.is_authenticated:
45
- return self._access_token
44
+ return self._access_token # type: ignore[return-value]
46
45
 
47
46
  # Token expired or missing, try refresh
48
47
  await self.refresh_tokens()
49
- return self._access_token
48
+ return self._access_token # type: ignore[return-value]
50
49
 
51
50
  async def refresh_tokens(self) -> None:
52
51
  """Refresh the access token using the refresh token."""
@@ -68,7 +67,7 @@ class AuthManager:
68
67
  data = await resp.json()
69
68
  self._update_tokens(data)
70
69
 
71
- def _update_tokens(self, data: Dict) -> None:
70
+ def _update_tokens(self, data: dict) -> None:
72
71
  """Update internal token state from API response."""
73
72
  self._access_token = data.get("access_token")
74
73
  self._refresh_token = data.get("refresh_token")
@@ -318,12 +317,12 @@ class AuthManager:
318
317
  _LOGGER.info("Tokens saved to %s", file_path)
319
318
 
320
319
  @classmethod
321
- def load_tokens(cls, file_path: str) -> Optional[Dict]:
320
+ def load_tokens(cls, file_path: str) -> dict | None:
322
321
  """Load tokens from a JSON file."""
323
322
  if not os.path.exists(file_path):
324
323
  return None
325
324
  try:
326
- with open(file_path, "r") as f:
325
+ with open(file_path) as f:
327
326
  return json.load(f)
328
327
  except Exception as e:
329
328
  _LOGGER.error("Failed to load tokens from %s: %s", file_path, e)
@@ -1,5 +1,5 @@
1
1
  import logging
2
- from typing import Dict, List, Optional
2
+ from datetime import datetime, timedelta, timezone
3
3
 
4
4
  import aiohttp
5
5
 
@@ -21,12 +21,25 @@ from .models import (
21
21
  ApplianceStatus,
22
22
  Hub,
23
23
  TimerModeSettings,
24
+ TsiEnergyReport,
24
25
  UserContext,
25
26
  Zone,
26
27
  )
27
28
 
28
29
  _LOGGER = logging.getLogger(__name__)
29
30
 
31
+ # Default lookback when the caller does not pin a start date. The energy
32
+ # endpoint is paginated server-side; 30 days is a reasonable default that
33
+ # keeps the response small while still giving the caller a meaningful window.
34
+ DEFAULT_TSI_REPORT_DAYS = 30
35
+ DEFAULT_TSI_INTERVAL = "01:00:00"
36
+
37
+
38
+ def _iso_utc_days_ago(days: int) -> str:
39
+ """Return an ISO-8601 UTC timestamp ``days`` before now (no microseconds)."""
40
+ dt = datetime.now(timezone.utc) - timedelta(days=days)
41
+ return dt.replace(microsecond=0).isoformat().replace("+00:00", "Z")
42
+
30
43
 
31
44
  class DimplexControl:
32
45
  """Main client for Dimplex Control API."""
@@ -34,8 +47,8 @@ class DimplexControl:
34
47
  def __init__(
35
48
  self,
36
49
  session: aiohttp.ClientSession,
37
- refresh_token: Optional[str] = None,
38
- access_token: Optional[str] = None,
50
+ refresh_token: str | None = None,
51
+ access_token: str | None = None,
39
52
  expires_at: float = 0,
40
53
  ):
41
54
  """Initialize the client."""
@@ -45,7 +58,7 @@ class DimplexControl:
45
58
  if access_token:
46
59
  token_data["access_token"] = access_token
47
60
  if expires_at:
48
- token_data["expires_at"] = expires_at
61
+ token_data["expires_at"] = expires_at # type: ignore[assignment]
49
62
 
50
63
  self._session = session
51
64
  self.auth = AuthManager(session, token_data)
@@ -55,7 +68,7 @@ class DimplexControl:
55
68
  """Check if authenticated."""
56
69
  return self.auth.is_authenticated
57
70
 
58
- async def _request(self, method: str, endpoint: str, **kwargs) -> Dict:
71
+ async def _request(self, method: str, endpoint: str, **kwargs) -> dict:
59
72
  """Make an authenticated request."""
60
73
  token = await self.auth.get_access_token()
61
74
  headers = kwargs.pop("headers", {})
@@ -92,13 +105,13 @@ class DimplexControl:
92
105
  _LOGGER.error("Connection error during API request: %s", e)
93
106
  raise DimplexConnectionError(f"Connection error: {e}") from e
94
107
 
95
- async def get_hubs(self) -> List[Hub]:
108
+ async def get_hubs(self) -> list[Hub]:
96
109
  """Get all hubs for the user."""
97
110
  data = await self._request("GET", "/Hubs/GetUserHubs")
98
111
  # Log analysis shows list of objects
99
112
  return [Hub(**h) for h in data]
100
113
 
101
- async def get_hub_zones(self, hub_id: str) -> List[Zone]:
114
+ async def get_hub_zones(self, hub_id: str) -> list[Zone]:
102
115
  """Get zones and appliances for a hub."""
103
116
  data = await self._request("GET", "/Zones/GetZonesAndAppliancesForHubId", params={"HubId": hub_id})
104
117
  return [Zone(**z) for z in data]
@@ -109,7 +122,7 @@ class DimplexControl:
109
122
  data = await self._request("POST", "/Zones/GetZone", json=payload)
110
123
  return Zone(**data)
111
124
 
112
- async def get_appliance_overview(self, hub_id: str, appliance_ids: List[str]) -> List[ApplianceStatus]:
125
+ async def get_appliance_overview(self, hub_id: str, appliance_ids: list[str]) -> list[ApplianceStatus]:
113
126
  """Get status overview for specific appliances."""
114
127
  payload = {"HubId": hub_id, "ApplianceIds": appliance_ids}
115
128
  data = await self._request("POST", "/RemoteControl/GetApplianceOverview", json=payload)
@@ -159,18 +172,55 @@ class DimplexControl:
159
172
  pass
160
173
 
161
174
  async def set_appliance_mode(
162
- self, hub_id: str, appliance_ids: List[str], mode_settings: ApplianceModeSettings
175
+ self, hub_id: str, appliance_ids: list[str], mode_settings: ApplianceModeSettings
163
176
  ) -> None:
164
177
  """Set appliance mode (Boost, Away, etc.)."""
165
178
  payload = {"Settings": mode_settings.dict(), "HubId": hub_id, "ApplianceIds": appliance_ids}
166
179
  await self._request("POST", "/RemoteControl/SetApplianceMode", json=payload)
167
180
 
168
- async def set_eco_start(self, hub_id: str, appliance_ids: List[str], enable: bool) -> None:
181
+ async def set_eco_start(self, hub_id: str, appliance_ids: list[str], enable: bool) -> None:
169
182
  """Enable/Disable EcoStart."""
170
183
  payload = {"Enable": enable, "HubId": hub_id, "ApplianceIds": appliance_ids}
171
184
  await self._request("POST", "/RemoteControl/SetEcoStart", json=payload)
172
185
 
173
- async def set_open_window_detection(self, hub_id: str, appliance_ids: List[str], enable: bool) -> None:
186
+ async def set_open_window_detection(self, hub_id: str, appliance_ids: list[str], enable: bool) -> None:
174
187
  """Enable/Disable Open Window Detection."""
175
188
  payload = {"Enable": enable, "HubId": hub_id, "ApplianceIds": appliance_ids}
176
189
  await self._request("POST", "/RemoteControl/SetOpenWindowDetection", json=payload)
190
+
191
+ async def get_tsi_energy_report(
192
+ self,
193
+ hub_id: str | None = None,
194
+ report_type: int = 1,
195
+ interval: str = DEFAULT_TSI_INTERVAL,
196
+ start_date: str | None = None,
197
+ include_previous_period: bool = False,
198
+ days_back: int = DEFAULT_TSI_REPORT_DAYS,
199
+ ) -> TsiEnergyReport:
200
+ """Fetch the Time Series Insights energy report for a hub.
201
+
202
+ Returns a :class:`~dimplex_controller.models.TsiEnergyReport`. The
203
+ cloud response is the same regardless of ``hub_id`` (the field is
204
+ informational), so ``hub_id`` is only required to populate the
205
+ returned model. Each per-appliance list is left as the raw payload —
206
+ use :func:`dimplex_controller.telemetry.parse_telemetry_points` to
207
+ normalise the points into ``(timestamp, value)`` tuples.
208
+
209
+ When the hub has no metered appliances (e.g. non-QRAD heaters, or a
210
+ quiet summer hub) the per-appliance lists come back empty; that is
211
+ treated as success, not an error.
212
+ """
213
+ if start_date is None:
214
+ start_date = _iso_utc_days_ago(days_back)
215
+
216
+ payload = {
217
+ "TsiReportType": report_type,
218
+ "Interval": interval,
219
+ "StartDate": start_date,
220
+ "IncludePreviousPeriod": include_previous_period,
221
+ }
222
+ data = await self._request("POST", "/Reports/GetTsiEnergyReportDataForHub", json=payload)
223
+ return TsiEnergyReport(
224
+ HubId=(hub_id or data.get("HubId", "")),
225
+ ApplianceTelemetryData=data.get("ApplianceTelemetryData", {}) or {},
226
+ )
@@ -0,0 +1,117 @@
1
+ from datetime import datetime, time
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class Appliance(BaseModel):
7
+ ApplianceId: str
8
+ ApplianceType: str
9
+ ApplianceModel: str | None = None
10
+ ZoneId: str
11
+ FriendlyName: str
12
+ ZoneName: str
13
+ Icon: str | None = None
14
+ IconColor: str | None = None
15
+ InstallationDate: datetime | None = None
16
+ HasConnectivity: bool | None = None
17
+
18
+
19
+ class Zone(BaseModel):
20
+ ZoneId: str
21
+ ZoneName: str
22
+ HubId: str
23
+ ZoneType: str
24
+ Appliances: list[Appliance] = Field(default_factory=list)
25
+
26
+
27
+ class Hub(BaseModel):
28
+ HubId: str
29
+ Name: str | None = Field(None, alias="HubName")
30
+ FriendlyName: str | None = None
31
+
32
+
33
+ class TimerPeriod(BaseModel):
34
+ DayOfWeek: int
35
+ StartTime: str # Kept as str for easy JSON serialization
36
+ EndTime: str
37
+ Temperature: float
38
+
39
+ @property
40
+ def start_time_obj(self) -> time:
41
+ return datetime.strptime(self.StartTime, "%H:%M:%S").time()
42
+
43
+ @property
44
+ def end_time_obj(self) -> time:
45
+ return datetime.strptime(self.EndTime, "%H:%M:%S").time()
46
+
47
+
48
+ class TimerModeSettings(BaseModel):
49
+ HubId: str
50
+ ApplianceId: str
51
+ TimerMode: int
52
+ TimerPeriods: list[TimerPeriod] = Field(default_factory=list)
53
+
54
+
55
+ class UserContext(BaseModel):
56
+ Id: str
57
+ EmailAddress: str | None = None
58
+ Name: str | None = None
59
+
60
+
61
+ class ApplianceStatus(BaseModel):
62
+ """Represents the real-time status of an appliance as returned by GetApplianceOverview."""
63
+
64
+ HubId: str
65
+ ApplianceId: str
66
+ ZoneId: str
67
+ StatusTwo: int | None = None
68
+ ApplianceModes: int | None = None
69
+ RoomTemperature: float | None = None
70
+ ActiveSetPointTemperature: int | None = None
71
+ NormalTemperature: float | None = None
72
+ AwayDateTime: str | None = None
73
+ AwayTemperature: float | None = None
74
+ BoostDuration: int | None = None
75
+ BoostTemperature: float | None = None
76
+ OpenWindowEnabled: bool | None = None
77
+ EcoStartEnabled: bool | None = None
78
+ SetbackEnabled: bool | None = None
79
+ SetbackEnabledInStatusFrame: bool | None = None
80
+ SetbackTemperature: float | None = None
81
+ ComfortStatus: bool | None = None
82
+ AvailableHotWater: float | None = None
83
+ LockStatus: int | None = None
84
+ ErrorCode: str | None = None
85
+ WarningCode: str | None = None
86
+
87
+
88
+ class ApplianceModeSettings(BaseModel):
89
+ """Settings used to control appliance modes like Boost or Away."""
90
+
91
+ ApplianceModes: int
92
+ Status: int
93
+ Temperature: float = 23.0
94
+ Time: int = 0
95
+ Date: str = "0001-01-01T00:00:00"
96
+ StatusTwo: int = 0
97
+ NumberOfDays: int = 0
98
+ Frequency: int = 0
99
+
100
+
101
+ class TsiEnergyReport(BaseModel):
102
+ """Response from `POST /Reports/GetTsiEnergyReportDataForHub`.
103
+
104
+ The cloud returns one telemetry bucket per appliance registered to the hub.
105
+ Each bucket is a list of points whose individual shape is undocumented and
106
+ appears to vary by firmware; entries are normalised by
107
+ :func:`dimplex_controller.telemetry.parse_telemetry_points` so callers do
108
+ not have to know the wire format.
109
+ """
110
+
111
+ HubId: str
112
+ ApplianceTelemetryData: dict[str, list] = Field(default_factory=dict)
113
+
114
+
115
+ # `TsiReportType` integer values understood by the API. We do not know the
116
+ # canonical mapping; these are observed in traffic captures.
117
+ TSI_REPORT_TYPE_ENERGY = 1
@@ -0,0 +1,133 @@
1
+ """Telemetry parsing for the Dimplex Reports API.
2
+
3
+ The response from `POST /Reports/GetTsiEnergyReportDataForHub` contains
4
+ ``ApplianceTelemetryData``: a dict keyed by appliance id, with one list of
5
+ ``telemetry points`` per appliance. The shape of each point is not documented
6
+ and varies between firmware versions, so we normalise whatever the cloud
7
+ sends into ``(timestamp, value)`` tuples.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Iterable
13
+ from datetime import datetime, timezone
14
+ from typing import Any
15
+
16
+ # Keys the cloud has been observed using, in priority order. The first match
17
+ # wins. Case-insensitive lookup is done by lowercasing the dict before
18
+ # scanning, so callers do not have to worry about casing.
19
+ _TIMESTAMP_KEYS = (
20
+ "timestamp",
21
+ "time",
22
+ "ts",
23
+ "t",
24
+ "datetime",
25
+ "date",
26
+ "from",
27
+ "start",
28
+ )
29
+ _VALUE_KEYS = (
30
+ "value",
31
+ "kwh",
32
+ "energy",
33
+ "consumption",
34
+ "energykwh",
35
+ "amount",
36
+ "v",
37
+ )
38
+
39
+
40
+ def _coerce_timestamp(raw: Any) -> datetime | None:
41
+ """Best-effort conversion of ``raw`` into a ``datetime``.
42
+
43
+ Accepts ``datetime`` instances, ISO-8601 strings, and Unix epoch numbers
44
+ (seconds or milliseconds). Returns ``None`` if the value cannot be parsed.
45
+ """
46
+ if raw is None:
47
+ return None
48
+ if isinstance(raw, datetime):
49
+ return raw
50
+ if isinstance(raw, int | float):
51
+ ts = float(raw)
52
+ if ts > 1e12:
53
+ ts = ts / 1000.0
54
+ try:
55
+ return datetime.fromtimestamp(ts, tz=timezone.utc)
56
+ except (OverflowError, OSError, ValueError):
57
+ return None
58
+ if isinstance(raw, str):
59
+ text = raw.strip()
60
+ if not text:
61
+ return None
62
+ try:
63
+ return datetime.fromisoformat(text.replace("Z", "+00:00"))
64
+ except ValueError:
65
+ return None
66
+ return None
67
+
68
+
69
+ def _coerce_value(raw: Any) -> float | None:
70
+ """Best-effort conversion of ``raw`` into a ``float``."""
71
+ if raw is None:
72
+ return None
73
+ if isinstance(raw, bool):
74
+ return None
75
+ if isinstance(raw, int | float):
76
+ return float(raw)
77
+ if isinstance(raw, str):
78
+ try:
79
+ return float(raw.strip())
80
+ except ValueError:
81
+ return None
82
+ return None
83
+
84
+
85
+ def _iter_items(point: Any) -> Iterable[tuple[str, Any]] | None:
86
+ """Yield ``(key, value)`` pairs from a dict-like ``point``.
87
+
88
+ Returns ``None`` for non-dict points so the caller can fall through to the
89
+ list / scalar branches.
90
+ """
91
+ if not isinstance(point, dict):
92
+ return None
93
+ lower = {str(k).lower(): v for k, v in point.items()}
94
+ return ((k, lower.get(k)) for k in (*_TIMESTAMP_KEYS, *_VALUE_KEYS))
95
+
96
+
97
+ def parse_telemetry_points(points: Any) -> list[tuple[datetime | None, float]]:
98
+ """Normalise a list of telemetry points into ``(timestamp, value)`` pairs.
99
+
100
+ Each entry in ``points`` may be:
101
+
102
+ * a ``dict`` with one of the recognised timestamp/value keys
103
+ * a 2-element ``[timestamp, value]`` list or tuple
104
+ * a bare scalar (treated as a cumulative value at an unknown timestamp)
105
+
106
+ Unparseable entries are silently skipped. The order of the input list is
107
+ preserved.
108
+ """
109
+ if not isinstance(points, list):
110
+ return []
111
+
112
+ out: list[tuple[datetime | None, float]] = []
113
+ for point in points:
114
+ ts: datetime | None = None
115
+ value: float | None = None
116
+
117
+ items = _iter_items(point)
118
+ if items is not None:
119
+ for key, raw in items:
120
+ if key in _TIMESTAMP_KEYS and ts is None:
121
+ ts = _coerce_timestamp(raw)
122
+ elif key in _VALUE_KEYS and value is None:
123
+ value = _coerce_value(raw)
124
+ elif isinstance(point, list | tuple) and len(point) == 2:
125
+ ts = _coerce_timestamp(point[0])
126
+ value = _coerce_value(point[1])
127
+ else:
128
+ value = _coerce_value(point)
129
+
130
+ if value is None:
131
+ continue
132
+ out.append((ts, value))
133
+ return out
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dimplex-controller"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "Python client for Dimplex heating controllers (GDHV IoT)"
5
5
  authors = ["Kieran Roper"]
6
6
  license = "MIT"
@@ -18,6 +18,7 @@ classifiers = [
18
18
  "Programming Language :: Python :: 3.10",
19
19
  "Programming Language :: Python :: 3.11",
20
20
  "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
21
22
  "Topic :: Home Automation",
22
23
  ]
23
24
  packages = [{ include = "dimplex_controller" }]
@@ -31,6 +32,7 @@ python-dotenv = "^1.2.1"
31
32
  [tool.poetry.group.dev.dependencies]
32
33
  pytest = "^8.0.0"
33
34
  pytest-asyncio = "^0.23.0"
35
+ pytest-cov = "^6.0.0"
34
36
  aresponses = "^3.0.0"
35
37
  ruff = "^0.3.0"
36
38
  pre-commit = "^3.6.0"
@@ -41,7 +43,18 @@ line-length = 120
41
43
  target-version = "py310"
42
44
 
43
45
  [tool.ruff.lint]
44
- select = ["E", "F", "I", "W"]
46
+ select = ["E", "F", "I", "W", "UP", "B", "SIM"]
47
+ # E501 (line length) is handled by the formatter; long strings/comments are fine.
48
+ ignore = ["E501"]
49
+
50
+ [tool.mypy]
51
+ # Loose by design — the library is mostly untyped and a strict pass is out of
52
+ # scope for the CI modernisation. The two auth.py return-narrowing issues and
53
+ # the client.py expires_at assignment are pre-existing and either fixed in
54
+ # place or # type: ignore-d; remove when a full typing pass lands.
55
+ python_version = "3.10"
56
+ files = ["dimplex_controller"]
57
+ ignore_missing_imports = true
45
58
 
46
59
  [build-system]
47
60
  requires = ["poetry-core"]
@@ -1,99 +0,0 @@
1
- from datetime import datetime, time
2
- from typing import List, Optional
3
-
4
- from pydantic import BaseModel, Field
5
-
6
-
7
- class Appliance(BaseModel):
8
- ApplianceId: str
9
- ApplianceType: str
10
- ApplianceModel: Optional[str] = None
11
- ZoneId: str
12
- FriendlyName: str
13
- ZoneName: str
14
- Icon: Optional[str] = None
15
- IconColor: Optional[str] = None
16
- InstallationDate: Optional[datetime] = None
17
- HasConnectivity: Optional[bool] = None
18
-
19
-
20
- class Zone(BaseModel):
21
- ZoneId: str
22
- ZoneName: str
23
- HubId: str
24
- ZoneType: str
25
- Appliances: List[Appliance] = Field(default_factory=list)
26
-
27
-
28
- class Hub(BaseModel):
29
- HubId: str
30
- Name: Optional[str] = Field(None, alias="HubName")
31
- FriendlyName: Optional[str] = None
32
-
33
-
34
- class TimerPeriod(BaseModel):
35
- DayOfWeek: int
36
- StartTime: str # Kept as str for easy JSON serialization
37
- EndTime: str
38
- Temperature: float
39
-
40
- @property
41
- def start_time_obj(self) -> time:
42
- return datetime.strptime(self.StartTime, "%H:%M:%S").time()
43
-
44
- @property
45
- def end_time_obj(self) -> time:
46
- return datetime.strptime(self.EndTime, "%H:%M:%S").time()
47
-
48
-
49
- class TimerModeSettings(BaseModel):
50
- HubId: str
51
- ApplianceId: str
52
- TimerMode: int
53
- TimerPeriods: List[TimerPeriod] = Field(default_factory=list)
54
-
55
-
56
- class UserContext(BaseModel):
57
- Id: str
58
- EmailAddress: Optional[str] = None
59
- Name: Optional[str] = None
60
-
61
-
62
- class ApplianceStatus(BaseModel):
63
- """Represents the real-time status of an appliance as returned by GetApplianceOverview."""
64
-
65
- HubId: str
66
- ApplianceId: str
67
- ZoneId: str
68
- StatusTwo: Optional[int] = None
69
- ApplianceModes: Optional[int] = None
70
- RoomTemperature: Optional[float] = None
71
- ActiveSetPointTemperature: Optional[int] = None
72
- NormalTemperature: Optional[float] = None
73
- AwayDateTime: Optional[str] = None
74
- AwayTemperature: Optional[float] = None
75
- BoostDuration: Optional[int] = None
76
- BoostTemperature: Optional[float] = None
77
- OpenWindowEnabled: Optional[bool] = None
78
- EcoStartEnabled: Optional[bool] = None
79
- SetbackEnabled: Optional[bool] = None
80
- SetbackEnabledInStatusFrame: Optional[bool] = None
81
- SetbackTemperature: Optional[float] = None
82
- ComfortStatus: Optional[bool] = None
83
- AvailableHotWater: Optional[float] = None
84
- LockStatus: Optional[int] = None
85
- ErrorCode: Optional[str] = None
86
- WarningCode: Optional[str] = None
87
-
88
-
89
- class ApplianceModeSettings(BaseModel):
90
- """Settings used to control appliance modes like Boost or Away."""
91
-
92
- ApplianceModes: int
93
- Status: int
94
- Temperature: float = 23.0
95
- Time: int = 0
96
- Date: str = "0001-01-01T00:00:00"
97
- StatusTwo: int = 0
98
- NumberOfDays: int = 0
99
- Frequency: int = 0