python-hotspring 1.2.0__py3-none-any.whl → 2.0.0__py3-none-any.whl
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.
- hotspring/__init__.py +2 -0
- hotspring/const.py +176 -3
- hotspring/hotspring.py +159 -54
- hotspring/models.py +730 -288
- {python_hotspring-1.2.0.dist-info → python_hotspring-2.0.0.dist-info}/METADATA +1 -1
- python_hotspring-2.0.0.dist-info/RECORD +10 -0
- python_hotspring-1.2.0.dist-info/RECORD +0 -10
- {python_hotspring-1.2.0.dist-info → python_hotspring-2.0.0.dist-info}/WHEEL +0 -0
- {python_hotspring-1.2.0.dist-info → python_hotspring-2.0.0.dist-info}/licenses/LICENSE +0 -0
hotspring/__init__.py
CHANGED
|
@@ -6,6 +6,7 @@ from .const import (
|
|
|
6
6
|
JetSpeed,
|
|
7
7
|
LightColor,
|
|
8
8
|
LightWheelMode,
|
|
9
|
+
SpaBrand,
|
|
9
10
|
SpaFailureState,
|
|
10
11
|
TemperatureUnit,
|
|
11
12
|
)
|
|
@@ -58,6 +59,7 @@ __all__ = [
|
|
|
58
59
|
"LightZone",
|
|
59
60
|
"LogoLight",
|
|
60
61
|
"Spa",
|
|
62
|
+
"SpaBrand",
|
|
61
63
|
"SpaFailureState",
|
|
62
64
|
"SpaInfo",
|
|
63
65
|
"SpaLock",
|
hotspring/const.py
CHANGED
|
@@ -82,8 +82,7 @@ class LightColor(Enum):
|
|
|
82
82
|
"""
|
|
83
83
|
|
|
84
84
|
UNKNOWN = "unknown"
|
|
85
|
-
|
|
86
|
-
ON = "WHEEL_ON"
|
|
85
|
+
CUSTOM = "CUSTOM"
|
|
87
86
|
RED = "RED"
|
|
88
87
|
BLUE = "BLUE"
|
|
89
88
|
GREEN = "GREEN"
|
|
@@ -96,7 +95,7 @@ class LightColor(Enum):
|
|
|
96
95
|
def build(cls, value: str | None) -> LightColor:
|
|
97
96
|
"""Parse a raw API string into a LightColor.
|
|
98
97
|
|
|
99
|
-
Case-insensitive matching (real API returns e.g. "BLUE").
|
|
98
|
+
Case-insensitive matching (real API returns e.g. "BLUE", "custom").
|
|
100
99
|
|
|
101
100
|
Args:
|
|
102
101
|
----
|
|
@@ -235,3 +234,177 @@ class SpaFailureState(Enum):
|
|
|
235
234
|
|
|
236
235
|
|
|
237
236
|
_FAILURE_STATE_MAP: dict[str, SpaFailureState] = {s.value: s for s in SpaFailureState}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class SpaBrand(Enum):
|
|
240
|
+
"""Brand of the spa (e.g. HotSpring, Caldera)."""
|
|
241
|
+
|
|
242
|
+
UNKNOWN = "Unknown"
|
|
243
|
+
HOTSPRING = "HotSpring"
|
|
244
|
+
CALDERA = "Caldera"
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def build(cls, value: str | int | None) -> SpaBrand:
|
|
248
|
+
"""Parse a raw API string or integer into a SpaBrand.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
----
|
|
252
|
+
value: The raw brand ID from the API, or None.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
-------
|
|
256
|
+
The matching SpaBrand enum.
|
|
257
|
+
|
|
258
|
+
"""
|
|
259
|
+
if value is None:
|
|
260
|
+
return cls.UNKNOWN
|
|
261
|
+
try:
|
|
262
|
+
val_int = int(str(value).strip())
|
|
263
|
+
except ValueError:
|
|
264
|
+
return cls.UNKNOWN
|
|
265
|
+
|
|
266
|
+
if val_int == 0:
|
|
267
|
+
return cls.HOTSPRING
|
|
268
|
+
if val_int == 1:
|
|
269
|
+
return cls.CALDERA
|
|
270
|
+
return cls.UNKNOWN
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
SPA_COLLECTION_MAP: dict[tuple[int, int], str] = {
|
|
274
|
+
# HotSpring (Brand 0)
|
|
275
|
+
(0, 0): "HighLife",
|
|
276
|
+
(0, 1): "Limelight",
|
|
277
|
+
(0, 2): "Hot Spot",
|
|
278
|
+
# Caldera (Brand 1)
|
|
279
|
+
(1, 1): "Utopia",
|
|
280
|
+
(1, 3): "Paradise",
|
|
281
|
+
(1, 4): "Vacanza",
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
SPA_MODEL_MAP: dict[tuple[int, int, int], str] = {
|
|
285
|
+
# Brand 0: HotSpring | Collection 0: HighLife
|
|
286
|
+
(0, 0, 0): "HotSpring HighLife",
|
|
287
|
+
(0, 0, 1): "HighLife Jetsetter",
|
|
288
|
+
(0, 0, 2): "HighLife Jetsetter Canada",
|
|
289
|
+
(0, 0, 3): "HighLife Jetsetter LX",
|
|
290
|
+
(0, 0, 4): "HighLife Prodigy",
|
|
291
|
+
(0, 0, 5): "HighLife Sovereign",
|
|
292
|
+
(0, 0, 6): "HighLife Aria",
|
|
293
|
+
(0, 0, 7): "HighLife Envoy",
|
|
294
|
+
(0, 0, 8): "HighLife Vanguard",
|
|
295
|
+
(0, 0, 9): "HighLife Grandee",
|
|
296
|
+
(0, 0, 10): "HighLife Jetsetter International",
|
|
297
|
+
(0, 0, 11): "HighLife Jetsetter LX International",
|
|
298
|
+
(0, 0, 12): "HighLife Prodigy International",
|
|
299
|
+
(0, 0, 13): "HighLife Sovereign International",
|
|
300
|
+
(0, 0, 14): "HighLife Aria International",
|
|
301
|
+
(0, 0, 15): "HighLife Envoy International",
|
|
302
|
+
(0, 0, 16): "HighLife Vanguard International",
|
|
303
|
+
(0, 0, 17): "HighLife Grandee International",
|
|
304
|
+
# Brand 0: HotSpring | Collection 1: Limelight
|
|
305
|
+
(0, 1, 0): "HotSpring Limelight",
|
|
306
|
+
(0, 1, 1): "Limelight Beam",
|
|
307
|
+
(0, 1, 2): "Limelight Beam II",
|
|
308
|
+
(0, 1, 3): "Limelight Beam International",
|
|
309
|
+
(0, 1, 4): "Limelight Beam Canada",
|
|
310
|
+
(0, 1, 5): "Limelight Strobe",
|
|
311
|
+
(0, 1, 6): "Limelight Strobe International",
|
|
312
|
+
(0, 1, 7): "Limelight Flair",
|
|
313
|
+
(0, 1, 8): "Limelight Flair International",
|
|
314
|
+
(0, 1, 9): "Limelight Flash",
|
|
315
|
+
(0, 1, 10): "Limelight Flash International",
|
|
316
|
+
(0, 1, 11): "Limelight Pulse",
|
|
317
|
+
(0, 1, 12): "Limelight Pulse International",
|
|
318
|
+
(0, 1, 13): "Limelight Prism",
|
|
319
|
+
(0, 1, 14): "Limelight Prism International",
|
|
320
|
+
# Brand 0: HotSpring | Collection 2: Hot Spot
|
|
321
|
+
(0, 2, 0): "Hot Spot Sx",
|
|
322
|
+
(0, 2, 1): "Hot Spot Tx",
|
|
323
|
+
(0, 2, 2): "Hot Spot Pace",
|
|
324
|
+
(0, 2, 3): "Hot Spot Stride",
|
|
325
|
+
(0, 2, 4): "Hot Spot Relay",
|
|
326
|
+
(0, 2, 5): "Hot Spot Rhythm",
|
|
327
|
+
(0, 2, 6): "Hot Spot Sx",
|
|
328
|
+
(0, 2, 7): "Hot Spot Tx",
|
|
329
|
+
(0, 2, 8): "Hot Spot Propel",
|
|
330
|
+
(0, 2, 9): "Hot Spot Stride",
|
|
331
|
+
(0, 2, 10): "Hot Spot Relay",
|
|
332
|
+
(0, 2, 11): "Hot Spot Rhythm",
|
|
333
|
+
# Brand 1: Caldera | Collection 1: Utopia
|
|
334
|
+
(1, 1, 0): "Caldera Utopia",
|
|
335
|
+
(1, 1, 1): "Utopia Ravello International",
|
|
336
|
+
(1, 1, 2): "Utopia Niagara International",
|
|
337
|
+
(1, 1, 3): "Utopia Tahitian International",
|
|
338
|
+
(1, 1, 4): "Utopia Florence International",
|
|
339
|
+
(1, 1, 5): "Utopia Geneva International",
|
|
340
|
+
(1, 1, 6): "Utopia Cantabria International",
|
|
341
|
+
(1, 1, 7): "Utopia Ravello",
|
|
342
|
+
(1, 1, 8): "Utopia Niagara",
|
|
343
|
+
(1, 1, 9): "Utopia Tahitian",
|
|
344
|
+
(1, 1, 10): "Utopia Florence",
|
|
345
|
+
(1, 1, 11): "Utopia Geneva",
|
|
346
|
+
(1, 1, 12): "Utopia Cantabria",
|
|
347
|
+
# Brand 1: Caldera | Collection 3: Paradise
|
|
348
|
+
(1, 3, 0): "Caldera Paradise",
|
|
349
|
+
(1, 3, 1): "Paradise Kauai",
|
|
350
|
+
(1, 3, 2): "Paradise Kauai International",
|
|
351
|
+
(1, 3, 3): "Paradise Martinique",
|
|
352
|
+
(1, 3, 4): "Paradise Martinique International",
|
|
353
|
+
(1, 3, 5): "Paradise Makena",
|
|
354
|
+
(1, 3, 6): "Paradise Makena International",
|
|
355
|
+
(1, 3, 7): "Paradise Salina",
|
|
356
|
+
(1, 3, 8): "Paradise Salina International",
|
|
357
|
+
(1, 3, 9): "Paradise Reunion",
|
|
358
|
+
(1, 3, 10): "Paradise Reunion International",
|
|
359
|
+
(1, 3, 11): "Paradise Seychelles",
|
|
360
|
+
(1, 3, 12): "Paradise Seychelles International",
|
|
361
|
+
# Brand 1: Caldera | Collection 4: Vacanza
|
|
362
|
+
(1, 4, 0): "Vacanza Aventine",
|
|
363
|
+
(1, 4, 1): "Vacanza Tarino",
|
|
364
|
+
(1, 4, 2): "Vacanza Capitolo",
|
|
365
|
+
(1, 4, 3): "Vacanza Celio",
|
|
366
|
+
(1, 4, 4): "Vacanza Platino",
|
|
367
|
+
(1, 4, 5): "Vacanza Vanto",
|
|
368
|
+
(1, 4, 6): "Vacanza Marino",
|
|
369
|
+
(1, 4, 7): "Vacanza Tarino_can",
|
|
370
|
+
(1, 4, 8): "Vacanza Aventine",
|
|
371
|
+
(1, 4, 9): "Vacanza Tarino",
|
|
372
|
+
(1, 4, 10): "Vacanza Capitolo",
|
|
373
|
+
(1, 4, 11): "Vacanza Celio",
|
|
374
|
+
(1, 4, 12): "Vacanza Marino",
|
|
375
|
+
(1, 4, 13): "Vacanza Platino",
|
|
376
|
+
(1, 4, 14): "Vacanza Vanto",
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def resolve_spa_model(
|
|
381
|
+
brand_raw: str | int | None,
|
|
382
|
+
collection_raw: str | int | None,
|
|
383
|
+
model_raw: str | int | None,
|
|
384
|
+
) -> tuple[SpaBrand, str, str]:
|
|
385
|
+
"""Resolve raw API brand, collection, and model IDs to human-readable strings.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
----
|
|
389
|
+
brand_raw: Raw brand string or int from API (e.g. "0" or "1").
|
|
390
|
+
collection_raw: Raw collection string or int from API (e.g. "1").
|
|
391
|
+
model_raw: Raw model string or int from API (e.g. "4").
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
-------
|
|
395
|
+
Tuple of (SpaBrand enum, collection name, model name).
|
|
396
|
+
|
|
397
|
+
"""
|
|
398
|
+
brand = SpaBrand.build(brand_raw)
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
brand_id = int(str(brand_raw)) if brand_raw is not None else -1
|
|
402
|
+
collection_id = int(str(collection_raw)) if collection_raw is not None else -1
|
|
403
|
+
model_id = int(str(model_raw)) if model_raw is not None else -1
|
|
404
|
+
except ValueError:
|
|
405
|
+
return (brand, "Unknown", "Unknown")
|
|
406
|
+
|
|
407
|
+
collection = SPA_COLLECTION_MAP.get((brand_id, collection_id), "Unknown")
|
|
408
|
+
model_name = SPA_MODEL_MAP.get((brand_id, collection_id, model_id), "Unknown")
|
|
409
|
+
|
|
410
|
+
return (brand, collection, model_name)
|
hotspring/hotspring.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import contextlib
|
|
6
7
|
import json
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from typing import Self
|
|
@@ -11,6 +12,12 @@ import aiohttp
|
|
|
11
12
|
import backoff
|
|
12
13
|
from yarl import URL
|
|
13
14
|
|
|
15
|
+
from .const import (
|
|
16
|
+
HeatingMode,
|
|
17
|
+
JetSpeed,
|
|
18
|
+
LightColor,
|
|
19
|
+
LightWheelMode,
|
|
20
|
+
)
|
|
14
21
|
from .exceptions import (
|
|
15
22
|
HotSpringCommandError,
|
|
16
23
|
HotSpringConnectionError,
|
|
@@ -23,6 +30,7 @@ from .models import (
|
|
|
23
30
|
Diagnostics,
|
|
24
31
|
FreshWaterIQ,
|
|
25
32
|
Spa,
|
|
33
|
+
SpaInfo,
|
|
26
34
|
)
|
|
27
35
|
|
|
28
36
|
|
|
@@ -47,6 +55,7 @@ class HotSpring:
|
|
|
47
55
|
session: aiohttp.ClientSession | None = None
|
|
48
56
|
request_timeout: float = 10.0
|
|
49
57
|
_close_session: bool = False
|
|
58
|
+
_identity_loaded: bool = False
|
|
50
59
|
spa: Spa | None = None
|
|
51
60
|
|
|
52
61
|
@backoff.on_exception(
|
|
@@ -144,55 +153,102 @@ class HotSpring:
|
|
|
144
153
|
|
|
145
154
|
return response_data
|
|
146
155
|
|
|
147
|
-
async def
|
|
148
|
-
"""
|
|
156
|
+
async def _safe_request(self, uri: str) -> dict[str, object] | None:
|
|
157
|
+
"""Fetch an endpoint, returning None on error."""
|
|
158
|
+
with contextlib.suppress(HotSpringError):
|
|
159
|
+
return await self.request(uri)
|
|
160
|
+
return None
|
|
149
161
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
the primary 15-second polling cycle.
|
|
162
|
+
async def update(self, *, refresh_identity: bool = False) -> Spa:
|
|
163
|
+
"""Get all spa information.
|
|
153
164
|
|
|
154
|
-
|
|
165
|
+
On the initial call (or when `refresh_identity=True`), this method fetches
|
|
166
|
+
the main /status endpoint concurrently with /startup, /spaConnectStatus,
|
|
167
|
+
and /spamodel.
|
|
168
|
+
|
|
169
|
+
On subsequent routine polling cycles, it only queries the fast /status
|
|
170
|
+
endpoint, avoiding redundant radio (LoRA) queries for static identity data.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
----
|
|
174
|
+
refresh_identity: Force re-fetching static identity from /startup
|
|
175
|
+
and /spamodel.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
155
178
|
-------
|
|
156
179
|
The updated Spa data object.
|
|
157
180
|
|
|
158
|
-
Raises
|
|
181
|
+
Raises:
|
|
159
182
|
------
|
|
160
183
|
HotSpringError: If no data is returned from the spa.
|
|
161
184
|
|
|
162
185
|
"""
|
|
163
|
-
|
|
186
|
+
if not self._identity_loaded or refresh_identity:
|
|
187
|
+
status_res, startup_res, connect_res, model_res = await asyncio.gather(
|
|
188
|
+
self.request("/status"),
|
|
189
|
+
self._safe_request("/startup"),
|
|
190
|
+
self._safe_request("/spaConnectStatus"),
|
|
191
|
+
self._safe_request("/spamodel"),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if self.spa is None:
|
|
195
|
+
self.spa = Spa(status_res)
|
|
196
|
+
else:
|
|
197
|
+
self.spa.update_from_dict(status_res)
|
|
198
|
+
|
|
199
|
+
if startup_res:
|
|
200
|
+
self.spa.update_info(startup_res)
|
|
201
|
+
|
|
202
|
+
if model_res:
|
|
203
|
+
self.spa.update_info(model_res)
|
|
204
|
+
|
|
205
|
+
if connect_res:
|
|
206
|
+
self.spa.update_connection_status(connect_res)
|
|
207
|
+
|
|
208
|
+
self._identity_loaded = True
|
|
209
|
+
return self.spa
|
|
210
|
+
|
|
164
211
|
status_data = await self.request("/status")
|
|
165
212
|
|
|
166
|
-
if self.spa is None:
|
|
213
|
+
if self.spa is None: # Safety guard; spa is always set after cold sync
|
|
167
214
|
self.spa = Spa(status_data)
|
|
168
215
|
else:
|
|
169
216
|
self.spa.update_from_dict(status_data)
|
|
170
217
|
|
|
171
|
-
|
|
172
|
-
identity_data: dict[str, object] = {}
|
|
173
|
-
try:
|
|
174
|
-
startup_data = await self.request("/startup")
|
|
175
|
-
identity_data.update(startup_data)
|
|
176
|
-
except HotSpringError:
|
|
177
|
-
pass
|
|
218
|
+
return self.spa
|
|
178
219
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
220
|
+
async def update_identity(self) -> SpaInfo:
|
|
221
|
+
"""Fetch and update static spa identity info (/startup and /spamodel).
|
|
222
|
+
|
|
223
|
+
Returns
|
|
224
|
+
-------
|
|
225
|
+
The updated SpaInfo data.
|
|
226
|
+
|
|
227
|
+
Raises
|
|
228
|
+
------
|
|
229
|
+
HotSpringError: If the spa has not been initialized with update().
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
if self.spa is None:
|
|
233
|
+
msg = "Call update() before update_identity()"
|
|
234
|
+
raise HotSpringError(msg)
|
|
235
|
+
|
|
236
|
+
startup_res, model_res = await asyncio.gather(
|
|
237
|
+
self._safe_request("/startup"),
|
|
238
|
+
self._safe_request("/spamodel"),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
identity_data: dict[str, object] = {}
|
|
242
|
+
if startup_res:
|
|
243
|
+
identity_data.update(startup_res)
|
|
244
|
+
if model_res:
|
|
245
|
+
identity_data.update(model_res)
|
|
184
246
|
|
|
185
247
|
if identity_data:
|
|
186
248
|
self.spa.update_info(identity_data)
|
|
187
249
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
connect_data = await self.request("/spaConnectStatus")
|
|
191
|
-
self.spa.update_connection_status(connect_data)
|
|
192
|
-
except HotSpringError:
|
|
193
|
-
pass # Non-critical
|
|
194
|
-
|
|
195
|
-
return self.spa
|
|
250
|
+
self._identity_loaded = True
|
|
251
|
+
return self.spa.info
|
|
196
252
|
|
|
197
253
|
async def update_water_care(self) -> FreshWaterIQ:
|
|
198
254
|
"""Update FreshWater IQ water quality data.
|
|
@@ -292,7 +348,11 @@ class HotSpring:
|
|
|
292
348
|
raise HotSpringNotReadyError(msg)
|
|
293
349
|
|
|
294
350
|
try:
|
|
295
|
-
await self.request(
|
|
351
|
+
response_data = await self.request(
|
|
352
|
+
"/spaManager", method="POST", data=payload
|
|
353
|
+
)
|
|
354
|
+
if self.spa is not None:
|
|
355
|
+
self.spa.update_from_dict(response_data)
|
|
296
356
|
except HotSpringError as exception:
|
|
297
357
|
msg = f"Command failed: {payload}"
|
|
298
358
|
raise HotSpringCommandError(msg) from exception
|
|
@@ -310,57 +370,53 @@ class HotSpring:
|
|
|
310
370
|
{"heater": {"control": {"temperatureABS": str(temperature)}}}
|
|
311
371
|
)
|
|
312
372
|
|
|
313
|
-
async def set_heating_mode(self, mode: str) -> None:
|
|
373
|
+
async def set_heating_mode(self, mode: str | HeatingMode) -> None:
|
|
314
374
|
"""Set the heating mode.
|
|
315
375
|
|
|
316
376
|
Args:
|
|
317
377
|
----
|
|
318
|
-
mode: The heating mode value. Use HeatingMode enum values,
|
|
319
|
-
e.g. ``HeatingMode.HEAT_WITH_BOOST.value``.
|
|
378
|
+
mode: The heating mode value. Use HeatingMode enum values or string,
|
|
379
|
+
e.g. ``HeatingMode.HEAT_WITH_BOOST.value`` or ``"heatWithBoost"``.
|
|
320
380
|
|
|
321
381
|
"""
|
|
322
|
-
|
|
382
|
+
mode_val = mode.value if isinstance(mode, HeatingMode) else str(mode)
|
|
383
|
+
await self._send_command({"heater": {"control": {"heatingMode": mode_val}}})
|
|
323
384
|
|
|
324
|
-
async def set_jet(self, jet: int, speed: str) -> None:
|
|
385
|
+
async def set_jet(self, jet: int, speed: str | JetSpeed) -> None:
|
|
325
386
|
"""Set the speed of a jet pump.
|
|
326
387
|
|
|
327
388
|
Args:
|
|
328
389
|
----
|
|
329
390
|
jet: The jet number (1-based).
|
|
330
|
-
speed: The speed value. Use JetSpeed enum values,
|
|
331
|
-
e.g. ``JetSpeed.HIGH_SPEED.value``.
|
|
391
|
+
speed: The speed value. Use JetSpeed enum values or string,
|
|
392
|
+
e.g. ``JetSpeed.HIGH_SPEED.value`` or ``"highSpeed"``.
|
|
332
393
|
|
|
333
394
|
"""
|
|
334
|
-
|
|
395
|
+
speed_val = speed.value if isinstance(speed, JetSpeed) else str(speed)
|
|
396
|
+
await self._send_command({"JET": {f"JET{jet}": {"control": speed_val}}})
|
|
335
397
|
|
|
336
398
|
async def set_light_color(
|
|
337
399
|
self,
|
|
338
400
|
zone: int,
|
|
339
|
-
color: str,
|
|
340
|
-
intensity: int = 5,
|
|
341
|
-
light_wheel: str = "off",
|
|
401
|
+
color: str | LightColor,
|
|
342
402
|
) -> None:
|
|
343
403
|
"""Set the color of a light zone.
|
|
344
404
|
|
|
345
405
|
Args:
|
|
346
406
|
----
|
|
347
407
|
zone: The light zone number (1-based).
|
|
348
|
-
color: The color value. Use LightColor enum values,
|
|
349
|
-
e.g. ``LightColor.BLUE.value``.
|
|
350
|
-
intensity: The brightness intensity (0-5). Defaults to 5.
|
|
351
|
-
light_wheel: The light wheel mode. Use LightWheelMode enum values,
|
|
352
|
-
e.g. ``LightWheelMode.OFF.value``. Defaults to "off".
|
|
408
|
+
color: The color value. Use LightColor enum values or string,
|
|
409
|
+
e.g. ``LightColor.BLUE.value`` or ``"BLUE"``.
|
|
353
410
|
|
|
354
411
|
"""
|
|
412
|
+
color_val = color.value if isinstance(color, LightColor) else str(color)
|
|
355
413
|
await self._send_command(
|
|
356
414
|
{
|
|
357
415
|
"lights": {
|
|
358
416
|
"control": {
|
|
359
417
|
f"Zone{zone}": {
|
|
360
418
|
"control": {
|
|
361
|
-
"color":
|
|
362
|
-
"IntensityAbs": intensity,
|
|
363
|
-
"lightWheel": light_wheel,
|
|
419
|
+
"color": color_val.upper(),
|
|
364
420
|
}
|
|
365
421
|
}
|
|
366
422
|
}
|
|
@@ -390,23 +446,60 @@ class HotSpring:
|
|
|
390
446
|
}
|
|
391
447
|
)
|
|
392
448
|
|
|
393
|
-
async def set_light_brightness(self, zone: int,
|
|
394
|
-
"""Set the brightness of a light zone.
|
|
449
|
+
async def set_light_brightness(self, zone: int, brightness: int) -> None:
|
|
450
|
+
"""Set the brightness intensity of a light zone (0-5).
|
|
395
451
|
|
|
396
452
|
Args:
|
|
397
453
|
----
|
|
398
454
|
zone: The light zone number (1-based).
|
|
399
|
-
|
|
455
|
+
brightness: The brightness level (0 = off, 1 = lowest, 5 = maximum).
|
|
456
|
+
|
|
457
|
+
Raises:
|
|
458
|
+
------
|
|
459
|
+
ValueError: If brightness is not an integer between 0 and 5.
|
|
400
460
|
|
|
401
461
|
"""
|
|
402
|
-
|
|
462
|
+
if not 0 <= brightness <= 5:
|
|
463
|
+
msg = f"Brightness must be between 0 and 5, got {brightness}"
|
|
464
|
+
raise ValueError(msg)
|
|
465
|
+
|
|
403
466
|
await self._send_command(
|
|
404
467
|
{
|
|
405
468
|
"lights": {
|
|
406
469
|
"control": {
|
|
407
470
|
f"Zone{zone}": {
|
|
408
471
|
"control": {
|
|
409
|
-
"
|
|
472
|
+
"IntensityAbs": brightness,
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
async def set_light_wheel(
|
|
481
|
+
self,
|
|
482
|
+
zone: int,
|
|
483
|
+
mode: str | LightWheelMode = LightWheelMode.ON,
|
|
484
|
+
) -> None:
|
|
485
|
+
"""Set the light wheel (color cycle / rainbow loop) mode for a light zone.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
----
|
|
489
|
+
zone: The light zone number (1-based).
|
|
490
|
+
mode: The light wheel mode. Use LightWheelMode enum values or string,
|
|
491
|
+
e.g. ``LightWheelMode.ON.value``, ``"loopUp"``,
|
|
492
|
+
``"loopDown"``, or ``"off"``. Defaults to LightWheelMode.ON.
|
|
493
|
+
|
|
494
|
+
"""
|
|
495
|
+
mode_val = mode.value if isinstance(mode, LightWheelMode) else str(mode)
|
|
496
|
+
await self._send_command(
|
|
497
|
+
{
|
|
498
|
+
"lights": {
|
|
499
|
+
"control": {
|
|
500
|
+
f"Zone{zone}": {
|
|
501
|
+
"control": {
|
|
502
|
+
"lightWheel": mode_val,
|
|
410
503
|
}
|
|
411
504
|
}
|
|
412
505
|
}
|
|
@@ -430,7 +523,19 @@ class HotSpring:
|
|
|
430
523
|
green: Green value (0-255).
|
|
431
524
|
blue: Blue value (0-255).
|
|
432
525
|
|
|
526
|
+
Raises:
|
|
527
|
+
------
|
|
528
|
+
ValueError: If any RGB component is not between 0 and 255.
|
|
529
|
+
|
|
433
530
|
"""
|
|
531
|
+
for component in (red, green, blue):
|
|
532
|
+
if not 0 <= component <= 255:
|
|
533
|
+
msg = (
|
|
534
|
+
f"RGB values must be between 0 and 255, "
|
|
535
|
+
f"got ({red}, {green}, {blue})"
|
|
536
|
+
)
|
|
537
|
+
raise ValueError(msg)
|
|
538
|
+
|
|
434
539
|
await self._send_command(
|
|
435
540
|
{
|
|
436
541
|
"lights": {
|