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.
- {python_hotspring-1.2.0 → python_hotspring-2.0.0}/PKG-INFO +1 -1
- {python_hotspring-1.2.0 → python_hotspring-2.0.0}/pyproject.toml +1 -1
- {python_hotspring-1.2.0 → python_hotspring-2.0.0}/src/hotspring/__init__.py +2 -0
- python_hotspring-2.0.0/src/hotspring/const.py +410 -0
- {python_hotspring-1.2.0 → python_hotspring-2.0.0}/src/hotspring/hotspring.py +159 -54
- python_hotspring-2.0.0/src/hotspring/models.py +1309 -0
- python_hotspring-1.2.0/src/hotspring/const.py +0 -237
- python_hotspring-1.2.0/src/hotspring/models.py +0 -867
- {python_hotspring-1.2.0 → python_hotspring-2.0.0}/LICENSE +0 -0
- {python_hotspring-1.2.0 → python_hotspring-2.0.0}/README.md +0 -0
- {python_hotspring-1.2.0 → python_hotspring-2.0.0}/src/hotspring/exceptions.py +0 -0
- {python_hotspring-1.2.0 → python_hotspring-2.0.0}/src/hotspring/py.typed +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)
|