python-hotspring 1.2.0__tar.gz → 2.0.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: python-hotspring
3
- Version: 1.2.0
3
+ Version: 2.0.0
4
4
  Summary: Asynchronous Python client for Hot Spring Connected Spa Kit 2.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -22,7 +22,7 @@ packages = [
22
22
  ]
23
23
  readme = "README.md"
24
24
  repository = "https://github.com/Moustachauve/python-hotspring"
25
- version = "1.2.0"
25
+ version = "2.0.0"
26
26
 
27
27
  [tool.poetry.dependencies]
28
28
  aiohttp = ">=3.0.0"
@@ -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",
@@ -0,0 +1,410 @@
1
+ """Constants and enumerations for Hot Spring Connected Spa Kit 2."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+
7
+
8
+ class HeatingMode(Enum):
9
+ """Heating mode for the spa heater.
10
+
11
+ Controls how the spa manages water temperature regulation.
12
+ """
13
+
14
+ UNKNOWN = "unknown"
15
+ INVALID = "invalid"
16
+ HEAT_SAVER = "heatSaver"
17
+ HEAT_WITH_BOOST = "heatWithBoost"
18
+ CHILL = "chill"
19
+ AUTO_WITH_BOOST = "autoWithBoost"
20
+ AUTO_SAVER = "autoSaver"
21
+
22
+ @classmethod
23
+ def build(cls, value: str | None) -> HeatingMode:
24
+ """Parse a raw API string into a HeatingMode.
25
+
26
+ Args:
27
+ ----
28
+ value: The raw heating mode string from the API, or None.
29
+
30
+ Returns:
31
+ -------
32
+ The matching HeatingMode, or HeatingMode.UNKNOWN for
33
+ unrecognized values.
34
+
35
+ """
36
+ if value is None:
37
+ return cls.UNKNOWN
38
+ return _HEATING_MODE_MAP.get(value, cls.UNKNOWN)
39
+
40
+
41
+ _HEATING_MODE_MAP: dict[str, HeatingMode] = {m.value: m for m in HeatingMode}
42
+
43
+
44
+ class JetSpeed(Enum):
45
+ """Speed setting for a spa jet pump.
46
+
47
+ Jets can be single-speed or multi-speed depending on the spa model.
48
+ """
49
+
50
+ UNKNOWN = "unknown"
51
+ OFF = "off"
52
+ LOW_SPEED = "lowSpeed"
53
+ HIGH_SPEED = "highSpeed"
54
+ SINGLE_SPEED = "singleSpeed"
55
+
56
+ @classmethod
57
+ def build(cls, value: str | None) -> JetSpeed:
58
+ """Parse a raw API string into a JetSpeed.
59
+
60
+ Args:
61
+ ----
62
+ value: The raw jet speed string from the API, or None.
63
+
64
+ Returns:
65
+ -------
66
+ The matching JetSpeed, or JetSpeed.UNKNOWN for
67
+ unrecognized values.
68
+
69
+ """
70
+ if value is None:
71
+ return cls.UNKNOWN
72
+ return _JET_SPEED_MAP.get(value, cls.UNKNOWN)
73
+
74
+
75
+ _JET_SPEED_MAP: dict[str, JetSpeed] = {s.value: s for s in JetSpeed}
76
+
77
+
78
+ class LightColor(Enum):
79
+ """Color setting for a spa light zone.
80
+
81
+ Represents the available color options for multi-zone LED lighting.
82
+ """
83
+
84
+ UNKNOWN = "unknown"
85
+ CUSTOM = "CUSTOM"
86
+ RED = "RED"
87
+ BLUE = "BLUE"
88
+ GREEN = "GREEN"
89
+ YELLOW = "YELLOW"
90
+ WHITE = "WHITE"
91
+ AQUA = "AQUA"
92
+ MAGENTA = "MAGENTA"
93
+
94
+ @classmethod
95
+ def build(cls, value: str | None) -> LightColor:
96
+ """Parse a raw API string into a LightColor.
97
+
98
+ Case-insensitive matching (real API returns e.g. "BLUE", "custom").
99
+
100
+ Args:
101
+ ----
102
+ value: The raw color string from the API, or None.
103
+
104
+ Returns:
105
+ -------
106
+ The matching LightColor, or LightColor.UNKNOWN for
107
+ unrecognized values.
108
+
109
+ """
110
+ if value is None:
111
+ return cls.UNKNOWN
112
+ return _LIGHT_COLOR_MAP.get(value.upper(), cls.UNKNOWN)
113
+
114
+
115
+ _LIGHT_COLOR_MAP: dict[str, LightColor] = {c.value.upper(): c for c in LightColor}
116
+
117
+
118
+ class LightWheelMode(Enum):
119
+ """Mode for the color light wheel loop."""
120
+
121
+ UNKNOWN = "unknown"
122
+ OFF = "off"
123
+ ON = "on"
124
+ LOOP_UP = "loopUp"
125
+ LOOP_DOWN = "loopDown"
126
+
127
+ @classmethod
128
+ def build(cls, value: str | None) -> LightWheelMode:
129
+ """Parse a raw API string into a LightWheelMode.
130
+
131
+ Args:
132
+ ----
133
+ value: The raw light wheel string from the API, or None.
134
+
135
+ Returns:
136
+ -------
137
+ The matching LightWheelMode, or LightWheelMode.UNKNOWN for
138
+ unrecognized values.
139
+
140
+ """
141
+ if value is None:
142
+ return cls.UNKNOWN
143
+ return _LIGHT_WHEEL_MAP.get(value, cls.UNKNOWN)
144
+
145
+
146
+ _LIGHT_WHEEL_MAP: dict[str, LightWheelMode] = {w.value: w for w in LightWheelMode}
147
+
148
+
149
+ class BrightnessLevel(Enum):
150
+ """Brightness level for the spa logo light.
151
+
152
+ The logo light supports a limited set of discrete brightness levels.
153
+ """
154
+
155
+ UNKNOWN = "unknown"
156
+ LEVEL_1 = "brightness_level_1"
157
+ LEVEL_2 = "brightness_level_2"
158
+ LEVEL_3 = "brightness_level_3"
159
+
160
+ @classmethod
161
+ def build(cls, value: str | None) -> BrightnessLevel:
162
+ """Parse a raw API string into a BrightnessLevel.
163
+
164
+ Args:
165
+ ----
166
+ value: The raw brightness string from the API, or None.
167
+
168
+ Returns:
169
+ -------
170
+ The matching BrightnessLevel, or BrightnessLevel.UNKNOWN for
171
+ unrecognized values.
172
+
173
+ """
174
+ if value is None:
175
+ return cls.UNKNOWN
176
+ return _BRIGHTNESS_MAP.get(value, cls.UNKNOWN)
177
+
178
+
179
+ _BRIGHTNESS_MAP: dict[str, BrightnessLevel] = {b.value: b for b in BrightnessLevel}
180
+
181
+
182
+ class TemperatureUnit(Enum):
183
+ """Unit of temperature measurement used by the spa."""
184
+
185
+ UNKNOWN = "unknown"
186
+ FAHRENHEIT = "DegF"
187
+ CELSIUS = "DegC"
188
+
189
+ @classmethod
190
+ def build(cls, value: str | None) -> TemperatureUnit:
191
+ """Parse a raw API string into a TemperatureUnit.
192
+
193
+ Args:
194
+ ----
195
+ value: The raw temperature unit string from the API, or None.
196
+
197
+ Returns:
198
+ -------
199
+ The matching TemperatureUnit, or TemperatureUnit.UNKNOWN for
200
+ unrecognized values.
201
+
202
+ """
203
+ if value is None:
204
+ return cls.UNKNOWN
205
+ return _TEMP_UNIT_MAP.get(value, cls.UNKNOWN)
206
+
207
+
208
+ _TEMP_UNIT_MAP: dict[str, TemperatureUnit] = {t.value: t for t in TemperatureUnit}
209
+
210
+
211
+ class SpaFailureState(Enum):
212
+ """Failure state of the spa as reported by diagnostics."""
213
+
214
+ UNKNOWN = "unknown"
215
+ OK = "Spa_Ok"
216
+
217
+ @classmethod
218
+ def build(cls, value: str | None) -> SpaFailureState:
219
+ """Parse a raw API string into a SpaFailureState.
220
+
221
+ Args:
222
+ ----
223
+ value: The raw failure state string from the API, or None.
224
+
225
+ Returns:
226
+ -------
227
+ The matching SpaFailureState, or SpaFailureState.UNKNOWN for
228
+ unrecognized values.
229
+
230
+ """
231
+ if value is None:
232
+ return cls.UNKNOWN
233
+ return _FAILURE_STATE_MAP.get(value, cls.UNKNOWN)
234
+
235
+
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)