pycaldera 0.1.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.
- pycaldera-0.1.0/CHANGELOG.rst +18 -0
- pycaldera-0.1.0/LICENSE +21 -0
- pycaldera-0.1.0/MANIFEST.in +10 -0
- pycaldera-0.1.0/PKG-INFO +296 -0
- pycaldera-0.1.0/README.md +215 -0
- pycaldera-0.1.0/pycaldera/__init__.py +29 -0
- pycaldera-0.1.0/pycaldera/__meta__.py +13 -0
- pycaldera-0.1.0/pycaldera/async_client.py +675 -0
- pycaldera-0.1.0/pycaldera/client.py +274 -0
- pycaldera-0.1.0/pycaldera/const.py +35 -0
- pycaldera-0.1.0/pycaldera/exceptions.py +31 -0
- pycaldera-0.1.0/pycaldera/models.py +261 -0
- pycaldera-0.1.0/pycaldera.egg-info/PKG-INFO +296 -0
- pycaldera-0.1.0/pycaldera.egg-info/SOURCES.txt +24 -0
- pycaldera-0.1.0/pycaldera.egg-info/dependency_links.txt +1 -0
- pycaldera-0.1.0/pycaldera.egg-info/requires.txt +62 -0
- pycaldera-0.1.0/pycaldera.egg-info/top_level.txt +1 -0
- pycaldera-0.1.0/pyproject.toml +15 -0
- pycaldera-0.1.0/requirements-dev.txt +8 -0
- pycaldera-0.1.0/requirements-docs.txt +7 -0
- pycaldera-0.1.0/requirements-test.txt +3 -0
- pycaldera-0.1.0/requirements.txt +13 -0
- pycaldera-0.1.0/setup.cfg +4 -0
- pycaldera-0.1.0/setup.py +156 -0
- pycaldera-0.1.0/tests/test_async_client.py +589 -0
- pycaldera-0.1.0/tests/test_client.py +123 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Changelog
|
|
2
|
+
=========
|
|
3
|
+
|
|
4
|
+
All notable changes to package_name will be documented here.
|
|
5
|
+
|
|
6
|
+
The format is based on `Keep a Changelog`_, and this project adheres to `Semantic Versioning`_.
|
|
7
|
+
|
|
8
|
+
.. _Keep a Changelog: https://keepachangelog.com/en/1.0.0/
|
|
9
|
+
.. _Semantic Versioning: https://semver.org/spec/v2.0.0.html
|
|
10
|
+
|
|
11
|
+
Categories for changes are: Added, Changed, Deprecated, Removed, Fixed, Security.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Version `0.1.0 <https://github.com/your_organisation/package_name/tree/0.1.0>`__
|
|
15
|
+
--------------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
Release date: YYYY-MM-DD.
|
|
18
|
+
Initial release (to be released).
|
pycaldera-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Mark Watson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pycaldera-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: pycaldera
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial Python client for Caldera Spa API
|
|
5
|
+
Home-page: https://github.com/mwatson2/pycaldera
|
|
6
|
+
Author: Mark Watson
|
|
7
|
+
Author-email: markwatson@cantab.net
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Natural Language :: English
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: aiohttp>=3.8.0
|
|
21
|
+
Requires-Dist: black>=23.0.0
|
|
22
|
+
Requires-Dist: isort>=5.12.0
|
|
23
|
+
Requires-Dist: mypy>=1.0.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0.0
|
|
25
|
+
Requires-Dist: pylint>=2.17.0
|
|
26
|
+
Requires-Dist: pytest>=7.0.0
|
|
27
|
+
Requires-Dist: pytest-aiohttp>=1.0.0
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.21.0
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0.0
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: pytest; extra == "test"
|
|
32
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
33
|
+
Requires-Dist: pytest-flake8; extra == "test"
|
|
34
|
+
Provides-Extra: docs
|
|
35
|
+
Requires-Dist: myst-parser; extra == "docs"
|
|
36
|
+
Requires-Dist: pypandoc>=1.15; extra == "docs"
|
|
37
|
+
Requires-Dist: readthedocs-sphinx-search; python_version >= "3.6" and extra == "docs"
|
|
38
|
+
Requires-Dist: sphinx<6,>=3.5.4; extra == "docs"
|
|
39
|
+
Requires-Dist: sphinx-autobuild; extra == "docs"
|
|
40
|
+
Requires-Dist: sphinx_book_theme; extra == "docs"
|
|
41
|
+
Requires-Dist: watchdog<1.0.0; python_version < "3.6" and extra == "docs"
|
|
42
|
+
Provides-Extra: dev
|
|
43
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
44
|
+
Requires-Dist: isort>=5.12.0; extra == "dev"
|
|
45
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
46
|
+
Requires-Dist: pylint>=2.17.0; extra == "dev"
|
|
47
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
48
|
+
Requires-Dist: pytest-aiohttp>=1.0.0; extra == "dev"
|
|
49
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
50
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
51
|
+
Provides-Extra: all
|
|
52
|
+
Requires-Dist: black>=23.0.0; extra == "all"
|
|
53
|
+
Requires-Dist: isort>=5.12.0; extra == "all"
|
|
54
|
+
Requires-Dist: mypy>=1.0.0; extra == "all"
|
|
55
|
+
Requires-Dist: myst-parser; extra == "all"
|
|
56
|
+
Requires-Dist: pylint>=2.17.0; extra == "all"
|
|
57
|
+
Requires-Dist: pypandoc>=1.15; extra == "all"
|
|
58
|
+
Requires-Dist: pytest; extra == "all"
|
|
59
|
+
Requires-Dist: pytest-aiohttp>=1.0.0; extra == "all"
|
|
60
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "all"
|
|
61
|
+
Requires-Dist: pytest-cov; extra == "all"
|
|
62
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "all"
|
|
63
|
+
Requires-Dist: pytest-flake8; extra == "all"
|
|
64
|
+
Requires-Dist: pytest>=7.0.0; extra == "all"
|
|
65
|
+
Requires-Dist: readthedocs-sphinx-search; python_version >= "3.6" and extra == "all"
|
|
66
|
+
Requires-Dist: sphinx-autobuild; extra == "all"
|
|
67
|
+
Requires-Dist: sphinx<6,>=3.5.4; extra == "all"
|
|
68
|
+
Requires-Dist: sphinx_book_theme; extra == "all"
|
|
69
|
+
Requires-Dist: watchdog<1.0.0; python_version < "3.6" and extra == "all"
|
|
70
|
+
Dynamic: author
|
|
71
|
+
Dynamic: author-email
|
|
72
|
+
Dynamic: classifier
|
|
73
|
+
Dynamic: description
|
|
74
|
+
Dynamic: description-content-type
|
|
75
|
+
Dynamic: home-page
|
|
76
|
+
Dynamic: license
|
|
77
|
+
Dynamic: provides-extra
|
|
78
|
+
Dynamic: requires-dist
|
|
79
|
+
Dynamic: requires-python
|
|
80
|
+
Dynamic: summary
|
|
81
|
+
|
|
82
|
+
# pycaldera
|
|
83
|
+
|
|
84
|
+
Python client library for controlling Caldera spas via their cloud API.
|
|
85
|
+
|
|
86
|
+
## Installation
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install pycaldera
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Usage
|
|
93
|
+
|
|
94
|
+
### Asynchronous API
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import asyncio
|
|
98
|
+
from pycaldera import AsyncCalderaClient, PUMP_OFF, PUMP_LOW, PUMP_HIGH
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def main():
|
|
102
|
+
async with AsyncCalderaClient("email@example.com", "password") as spa:
|
|
103
|
+
# Get current spa status
|
|
104
|
+
status = await spa.get_spa_status()
|
|
105
|
+
print(f"Current temperature: {status.ctrl_head_water_temperature}°F")
|
|
106
|
+
|
|
107
|
+
# Get detailed live settings
|
|
108
|
+
settings = await spa.get_live_settings()
|
|
109
|
+
print(f"Target temperature: {settings.ctrl_head_set_temperature}°F")
|
|
110
|
+
|
|
111
|
+
# Control the spa
|
|
112
|
+
await spa.set_temperature(102) # Set temperature to 102°F
|
|
113
|
+
await spa.set_pump(1, PUMP_HIGH) # Set pump 1 to high speed
|
|
114
|
+
await spa.set_lights(True) # Turn on the lights
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
asyncio.run(main())
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Synchronous API
|
|
121
|
+
|
|
122
|
+
For simpler use cases, a synchronous wrapper is also available:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from pycaldera import CalderaClient, PUMP_OFF, PUMP_LOW, PUMP_HIGH
|
|
126
|
+
|
|
127
|
+
with CalderaClient("email@example.com", "password") as spa:
|
|
128
|
+
# Get current spa status
|
|
129
|
+
status = spa.get_spa_status()
|
|
130
|
+
print(f"Current temperature: {status.ctrl_head_water_temperature}°F")
|
|
131
|
+
|
|
132
|
+
# Get detailed live settings
|
|
133
|
+
settings = spa.get_live_settings()
|
|
134
|
+
print(f"Target temperature: {settings.ctrl_head_set_temperature}°F")
|
|
135
|
+
|
|
136
|
+
# Control the spa
|
|
137
|
+
spa.set_temperature(102) # Set temperature to 102°F
|
|
138
|
+
spa.set_pump(1, PUMP_HIGH) # Set pump 1 to high speed
|
|
139
|
+
spa.set_lights(True) # Turn on the lights
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Both clients provide identical functionality, with the synchronous client simply wrapping the async one for convenience.
|
|
143
|
+
|
|
144
|
+
## API Reference
|
|
145
|
+
|
|
146
|
+
### AsyncCalderaClient
|
|
147
|
+
|
|
148
|
+
The main async client class for interacting with the spa. All operations must be performed within an async context manager:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
async with AsyncCalderaClient(
|
|
152
|
+
email="email@example.com",
|
|
153
|
+
password="password",
|
|
154
|
+
timeout=10.0, # Optional: request timeout in seconds
|
|
155
|
+
debug=False, # Optional: enable debug logging
|
|
156
|
+
) as spa:
|
|
157
|
+
# All spa operations must be inside this block
|
|
158
|
+
await spa.get_spa_status()
|
|
159
|
+
await spa.set_temperature(102)
|
|
160
|
+
# etc...
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### CalderaClient
|
|
164
|
+
|
|
165
|
+
A synchronous wrapper around AsyncCalderaClient that provides the same functionality without requiring async/await:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
with CalderaClient(
|
|
169
|
+
email="email@example.com",
|
|
170
|
+
password="password",
|
|
171
|
+
timeout=10.0, # Optional: request timeout in seconds
|
|
172
|
+
debug=False, # Optional: enable debug logging
|
|
173
|
+
) as spa:
|
|
174
|
+
# All spa operations can be called synchronously
|
|
175
|
+
spa.get_spa_status()
|
|
176
|
+
spa.set_temperature(102)
|
|
177
|
+
# etc...
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Error Handling
|
|
181
|
+
|
|
182
|
+
All operations can raise these base exceptions:
|
|
183
|
+
- `AuthenticationError`: When authentication fails or token expires
|
|
184
|
+
- `ConnectionError`: When network connection fails or API is unreachable
|
|
185
|
+
- `SpaControlError`: When the API returns an error response
|
|
186
|
+
|
|
187
|
+
### Temperature Control
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
async with spa as client:
|
|
191
|
+
# Set temperature (80-104°F or 26.5-40°C)
|
|
192
|
+
try:
|
|
193
|
+
# Basic temperature setting
|
|
194
|
+
await client.set_temperature(102) # Fahrenheit
|
|
195
|
+
await client.set_temperature(39, "C") # Celsius
|
|
196
|
+
|
|
197
|
+
# Wait for spa to acknowledge the temperature change
|
|
198
|
+
await client.set_temperature(102, wait_for_ack=True)
|
|
199
|
+
|
|
200
|
+
# Control polling behavior when waiting for acknowledgment
|
|
201
|
+
await client.set_temperature(
|
|
202
|
+
102,
|
|
203
|
+
wait_for_ack=True,
|
|
204
|
+
polling_interval=5.0, # Check every 5 seconds
|
|
205
|
+
polling_timeout=120.0, # Time out after 2 minutes
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Manually wait for temperature acknowledgment
|
|
209
|
+
settings = await client.wait_for_temperature_ack(
|
|
210
|
+
expected_temp=102, # Expected temperature in Fahrenheit
|
|
211
|
+
interval=5.0, # Check every 5 seconds
|
|
212
|
+
timeout=120.0, # Time out after 2 minutes
|
|
213
|
+
)
|
|
214
|
+
except InvalidParameterError:
|
|
215
|
+
# Raised when temperature is outside valid range
|
|
216
|
+
# (80-104°F or 26.5-40°C)
|
|
217
|
+
pass
|
|
218
|
+
except SpaControlError:
|
|
219
|
+
# Raised when polling times out waiting for acknowledgment
|
|
220
|
+
pass
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Pump Control
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
async with spa as client:
|
|
227
|
+
try:
|
|
228
|
+
await client.set_pump(1, PUMP_HIGH) # Set pump 1 to high speed
|
|
229
|
+
await client.set_pump(2, PUMP_LOW) # Set pump 2 to low speed
|
|
230
|
+
await client.set_pump(3, PUMP_OFF) # Turn off pump 3
|
|
231
|
+
except InvalidParameterError:
|
|
232
|
+
# Raised when:
|
|
233
|
+
# - pump_number is not 1, 2, or 3
|
|
234
|
+
# - speed is not PUMP_OFF (0), PUMP_LOW (1), or PUMP_HIGH (2)
|
|
235
|
+
pass
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Light Control
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
async with spa as client:
|
|
242
|
+
try:
|
|
243
|
+
await client.set_lights(True) # Turn lights on
|
|
244
|
+
await client.set_lights(False) # Turn lights off
|
|
245
|
+
except SpaControlError:
|
|
246
|
+
# Raised when light control fails
|
|
247
|
+
pass
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Status & Settings
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
async with spa as client:
|
|
254
|
+
try:
|
|
255
|
+
# Get basic spa status
|
|
256
|
+
status = await client.get_spa_status()
|
|
257
|
+
print(f"Online: {status.status == 'ONLINE'}")
|
|
258
|
+
|
|
259
|
+
# Get detailed live settings
|
|
260
|
+
settings = await client.get_live_settings()
|
|
261
|
+
print(f"Target temp: {settings.ctrl_head_set_temperature}°F")
|
|
262
|
+
except ConnectionError:
|
|
263
|
+
# Raised when spa is offline or unreachable
|
|
264
|
+
pass
|
|
265
|
+
except SpaControlError:
|
|
266
|
+
# Raised when API returns invalid data
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
## Development
|
|
270
|
+
|
|
271
|
+
1. Clone the repository
|
|
272
|
+
2. Create a virtual environment:
|
|
273
|
+
```bash
|
|
274
|
+
python -m venv venv
|
|
275
|
+
source venv/bin/activate # or `venv\Scripts\activate` on Windows
|
|
276
|
+
```
|
|
277
|
+
3. Install development dependencies:
|
|
278
|
+
```bash
|
|
279
|
+
pip install -r requirements-dev.txt
|
|
280
|
+
```
|
|
281
|
+
4. Install pre-commit hooks:
|
|
282
|
+
```bash
|
|
283
|
+
pre-commit install
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The pre-commit hooks will run automatically on git commit, checking:
|
|
287
|
+
- Code formatting (Black)
|
|
288
|
+
- Import sorting (isort)
|
|
289
|
+
- Type checking (mypy)
|
|
290
|
+
- Linting (pylint, ruff)
|
|
291
|
+
- YAML/TOML syntax
|
|
292
|
+
- Trailing whitespace and file endings
|
|
293
|
+
|
|
294
|
+
## License
|
|
295
|
+
|
|
296
|
+
MIT License - see LICENSE file for details.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# pycaldera
|
|
2
|
+
|
|
3
|
+
Python client library for controlling Caldera spas via their cloud API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install pycaldera
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Asynchronous API
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import asyncio
|
|
17
|
+
from pycaldera import AsyncCalderaClient, PUMP_OFF, PUMP_LOW, PUMP_HIGH
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def main():
|
|
21
|
+
async with AsyncCalderaClient("email@example.com", "password") as spa:
|
|
22
|
+
# Get current spa status
|
|
23
|
+
status = await spa.get_spa_status()
|
|
24
|
+
print(f"Current temperature: {status.ctrl_head_water_temperature}°F")
|
|
25
|
+
|
|
26
|
+
# Get detailed live settings
|
|
27
|
+
settings = await spa.get_live_settings()
|
|
28
|
+
print(f"Target temperature: {settings.ctrl_head_set_temperature}°F")
|
|
29
|
+
|
|
30
|
+
# Control the spa
|
|
31
|
+
await spa.set_temperature(102) # Set temperature to 102°F
|
|
32
|
+
await spa.set_pump(1, PUMP_HIGH) # Set pump 1 to high speed
|
|
33
|
+
await spa.set_lights(True) # Turn on the lights
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
asyncio.run(main())
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Synchronous API
|
|
40
|
+
|
|
41
|
+
For simpler use cases, a synchronous wrapper is also available:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from pycaldera import CalderaClient, PUMP_OFF, PUMP_LOW, PUMP_HIGH
|
|
45
|
+
|
|
46
|
+
with CalderaClient("email@example.com", "password") as spa:
|
|
47
|
+
# Get current spa status
|
|
48
|
+
status = spa.get_spa_status()
|
|
49
|
+
print(f"Current temperature: {status.ctrl_head_water_temperature}°F")
|
|
50
|
+
|
|
51
|
+
# Get detailed live settings
|
|
52
|
+
settings = spa.get_live_settings()
|
|
53
|
+
print(f"Target temperature: {settings.ctrl_head_set_temperature}°F")
|
|
54
|
+
|
|
55
|
+
# Control the spa
|
|
56
|
+
spa.set_temperature(102) # Set temperature to 102°F
|
|
57
|
+
spa.set_pump(1, PUMP_HIGH) # Set pump 1 to high speed
|
|
58
|
+
spa.set_lights(True) # Turn on the lights
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Both clients provide identical functionality, with the synchronous client simply wrapping the async one for convenience.
|
|
62
|
+
|
|
63
|
+
## API Reference
|
|
64
|
+
|
|
65
|
+
### AsyncCalderaClient
|
|
66
|
+
|
|
67
|
+
The main async client class for interacting with the spa. All operations must be performed within an async context manager:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
async with AsyncCalderaClient(
|
|
71
|
+
email="email@example.com",
|
|
72
|
+
password="password",
|
|
73
|
+
timeout=10.0, # Optional: request timeout in seconds
|
|
74
|
+
debug=False, # Optional: enable debug logging
|
|
75
|
+
) as spa:
|
|
76
|
+
# All spa operations must be inside this block
|
|
77
|
+
await spa.get_spa_status()
|
|
78
|
+
await spa.set_temperature(102)
|
|
79
|
+
# etc...
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### CalderaClient
|
|
83
|
+
|
|
84
|
+
A synchronous wrapper around AsyncCalderaClient that provides the same functionality without requiring async/await:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
with CalderaClient(
|
|
88
|
+
email="email@example.com",
|
|
89
|
+
password="password",
|
|
90
|
+
timeout=10.0, # Optional: request timeout in seconds
|
|
91
|
+
debug=False, # Optional: enable debug logging
|
|
92
|
+
) as spa:
|
|
93
|
+
# All spa operations can be called synchronously
|
|
94
|
+
spa.get_spa_status()
|
|
95
|
+
spa.set_temperature(102)
|
|
96
|
+
# etc...
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Error Handling
|
|
100
|
+
|
|
101
|
+
All operations can raise these base exceptions:
|
|
102
|
+
- `AuthenticationError`: When authentication fails or token expires
|
|
103
|
+
- `ConnectionError`: When network connection fails or API is unreachable
|
|
104
|
+
- `SpaControlError`: When the API returns an error response
|
|
105
|
+
|
|
106
|
+
### Temperature Control
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
async with spa as client:
|
|
110
|
+
# Set temperature (80-104°F or 26.5-40°C)
|
|
111
|
+
try:
|
|
112
|
+
# Basic temperature setting
|
|
113
|
+
await client.set_temperature(102) # Fahrenheit
|
|
114
|
+
await client.set_temperature(39, "C") # Celsius
|
|
115
|
+
|
|
116
|
+
# Wait for spa to acknowledge the temperature change
|
|
117
|
+
await client.set_temperature(102, wait_for_ack=True)
|
|
118
|
+
|
|
119
|
+
# Control polling behavior when waiting for acknowledgment
|
|
120
|
+
await client.set_temperature(
|
|
121
|
+
102,
|
|
122
|
+
wait_for_ack=True,
|
|
123
|
+
polling_interval=5.0, # Check every 5 seconds
|
|
124
|
+
polling_timeout=120.0, # Time out after 2 minutes
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Manually wait for temperature acknowledgment
|
|
128
|
+
settings = await client.wait_for_temperature_ack(
|
|
129
|
+
expected_temp=102, # Expected temperature in Fahrenheit
|
|
130
|
+
interval=5.0, # Check every 5 seconds
|
|
131
|
+
timeout=120.0, # Time out after 2 minutes
|
|
132
|
+
)
|
|
133
|
+
except InvalidParameterError:
|
|
134
|
+
# Raised when temperature is outside valid range
|
|
135
|
+
# (80-104°F or 26.5-40°C)
|
|
136
|
+
pass
|
|
137
|
+
except SpaControlError:
|
|
138
|
+
# Raised when polling times out waiting for acknowledgment
|
|
139
|
+
pass
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Pump Control
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
async with spa as client:
|
|
146
|
+
try:
|
|
147
|
+
await client.set_pump(1, PUMP_HIGH) # Set pump 1 to high speed
|
|
148
|
+
await client.set_pump(2, PUMP_LOW) # Set pump 2 to low speed
|
|
149
|
+
await client.set_pump(3, PUMP_OFF) # Turn off pump 3
|
|
150
|
+
except InvalidParameterError:
|
|
151
|
+
# Raised when:
|
|
152
|
+
# - pump_number is not 1, 2, or 3
|
|
153
|
+
# - speed is not PUMP_OFF (0), PUMP_LOW (1), or PUMP_HIGH (2)
|
|
154
|
+
pass
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Light Control
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
async with spa as client:
|
|
161
|
+
try:
|
|
162
|
+
await client.set_lights(True) # Turn lights on
|
|
163
|
+
await client.set_lights(False) # Turn lights off
|
|
164
|
+
except SpaControlError:
|
|
165
|
+
# Raised when light control fails
|
|
166
|
+
pass
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Status & Settings
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
async with spa as client:
|
|
173
|
+
try:
|
|
174
|
+
# Get basic spa status
|
|
175
|
+
status = await client.get_spa_status()
|
|
176
|
+
print(f"Online: {status.status == 'ONLINE'}")
|
|
177
|
+
|
|
178
|
+
# Get detailed live settings
|
|
179
|
+
settings = await client.get_live_settings()
|
|
180
|
+
print(f"Target temp: {settings.ctrl_head_set_temperature}°F")
|
|
181
|
+
except ConnectionError:
|
|
182
|
+
# Raised when spa is offline or unreachable
|
|
183
|
+
pass
|
|
184
|
+
except SpaControlError:
|
|
185
|
+
# Raised when API returns invalid data
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
## Development
|
|
189
|
+
|
|
190
|
+
1. Clone the repository
|
|
191
|
+
2. Create a virtual environment:
|
|
192
|
+
```bash
|
|
193
|
+
python -m venv venv
|
|
194
|
+
source venv/bin/activate # or `venv\Scripts\activate` on Windows
|
|
195
|
+
```
|
|
196
|
+
3. Install development dependencies:
|
|
197
|
+
```bash
|
|
198
|
+
pip install -r requirements-dev.txt
|
|
199
|
+
```
|
|
200
|
+
4. Install pre-commit hooks:
|
|
201
|
+
```bash
|
|
202
|
+
pre-commit install
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The pre-commit hooks will run automatically on git commit, checking:
|
|
206
|
+
- Code formatting (Black)
|
|
207
|
+
- Import sorting (isort)
|
|
208
|
+
- Type checking (mypy)
|
|
209
|
+
- Linting (pylint, ruff)
|
|
210
|
+
- YAML/TOML syntax
|
|
211
|
+
- Trailing whitespace and file endings
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT License - see LICENSE file for details.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Python client for Caldera Spa Connexion API."""
|
|
2
|
+
|
|
3
|
+
from .__meta__ import __version__
|
|
4
|
+
from .async_client import AsyncCalderaClient
|
|
5
|
+
from .client import CalderaClient
|
|
6
|
+
from .const import PUMP_HIGH, PUMP_LOW, PUMP_OFF
|
|
7
|
+
from .exceptions import (
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
CalderaError,
|
|
10
|
+
ConnectionError,
|
|
11
|
+
InvalidParameterError,
|
|
12
|
+
SpaControlError,
|
|
13
|
+
)
|
|
14
|
+
from .models import LiveSettings
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AsyncCalderaClient",
|
|
18
|
+
"CalderaClient",
|
|
19
|
+
"LiveSettings",
|
|
20
|
+
"PUMP_OFF",
|
|
21
|
+
"PUMP_LOW",
|
|
22
|
+
"PUMP_HIGH",
|
|
23
|
+
"CalderaError",
|
|
24
|
+
"AuthenticationError",
|
|
25
|
+
"ConnectionError",
|
|
26
|
+
"SpaControlError",
|
|
27
|
+
"InvalidParameterError",
|
|
28
|
+
"__version__",
|
|
29
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# `name` is the name of the package as used for `pip install package`
|
|
2
|
+
name = "pycaldera"
|
|
3
|
+
# `path` is the name of the package for `import package`
|
|
4
|
+
path = name.lower().replace("-", "_").replace(" ", "_")
|
|
5
|
+
# Your version number should follow https://python.org/dev/peps/pep-0440 and
|
|
6
|
+
# https://semver.org
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
__version__ = version
|
|
9
|
+
author = "Mark Watson"
|
|
10
|
+
author_email = "markwatson@cantab.net"
|
|
11
|
+
description = "Unofficial Python client for Caldera Spa API" # One-liner
|
|
12
|
+
url = "https://github.com/mwatson2/pycaldera" # Add your GitHub repo URL
|
|
13
|
+
license = "MIT" # See https://choosealicense.com
|