pycaldera 0.1.dev0__tar.gz → 0.1.2__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.2/CHANGELOG.rst +58 -0
- pycaldera-0.1.2/PKG-INFO +287 -0
- pycaldera-0.1.2/README.md +215 -0
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/pycaldera/__init__.py +10 -2
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/pycaldera/__meta__.py +2 -1
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/pycaldera/async_client.py +224 -43
- pycaldera-0.1.2/pycaldera/client.py +274 -0
- pycaldera-0.1.2/pycaldera/const.py +38 -0
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/pycaldera/models.py +80 -12
- pycaldera-0.1.2/pycaldera.egg-info/PKG-INFO +287 -0
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/pycaldera.egg-info/SOURCES.txt +5 -1
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/pycaldera.egg-info/requires.txt +6 -16
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/pyproject.toml +1 -1
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/requirements-docs.txt +1 -1
- pycaldera-0.1.2/requirements-test.txt +4 -0
- pycaldera-0.1.2/requirements.txt +2 -0
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/tests/test_async_client.py +181 -5
- pycaldera-0.1.2/tests/test_client.py +199 -0
- pycaldera-0.1.2/tests/test_models.py +100 -0
- pycaldera-0.1.dev0/CHANGELOG.rst +0 -18
- pycaldera-0.1.dev0/PKG-INFO +0 -198
- pycaldera-0.1.dev0/README.md +0 -117
- pycaldera-0.1.dev0/pycaldera.egg-info/PKG-INFO +0 -198
- pycaldera-0.1.dev0/requirements-test.txt +0 -3
- pycaldera-0.1.dev0/requirements.txt +0 -13
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/LICENSE +0 -0
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/MANIFEST.in +0 -0
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/pycaldera/exceptions.py +0 -0
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/pycaldera.egg-info/dependency_links.txt +0 -0
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/pycaldera.egg-info/top_level.txt +0 -0
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/requirements-dev.txt +0 -0
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/setup.cfg +0 -0
- {pycaldera-0.1.dev0 → pycaldera-0.1.2}/setup.py +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Changelog
|
|
2
|
+
=========
|
|
3
|
+
|
|
4
|
+
All notable changes to pycaldera 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.1 <https://github.com/mwatson2/pycaldera/tree/v0.1.1>`__
|
|
15
|
+
---------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
Release date: 2026-04-08.
|
|
18
|
+
|
|
19
|
+
Added
|
|
20
|
+
~~~~~
|
|
21
|
+
- ``SpaResponseDato.water_temperature`` and ``SpaResponseDato.set_temperature``
|
|
22
|
+
convenience properties that read the current and target temperatures from
|
|
23
|
+
the embedded ``isConnectedData.liveSettings.rows[0]`` payload. Both return
|
|
24
|
+
``None`` if no live-settings row is available.
|
|
25
|
+
- Unit tests for the synchronous ``CalderaClient`` covering ``get_spa_status``,
|
|
26
|
+
``get_live_settings``, ``set_pump``, ``set_temp_lock``, ``set_spa_lock`` and
|
|
27
|
+
the ``close()`` paths.
|
|
28
|
+
|
|
29
|
+
Fixed
|
|
30
|
+
~~~~~
|
|
31
|
+
- README quickstart examples referenced ``status.ctrl_head_water_temperature``,
|
|
32
|
+
which is not a field on the spa-status response. They now use the new
|
|
33
|
+
``status.water_temperature`` property and actually work.
|
|
34
|
+
- ``requirements.txt`` no longer pins development tooling (``black``,
|
|
35
|
+
``isort``, ``mypy``, ``pylint``, ``pytest`` and friends) as runtime
|
|
36
|
+
dependencies. Plain ``pip install pycaldera`` now installs only ``aiohttp``
|
|
37
|
+
and ``pydantic``.
|
|
38
|
+
- ``requirements-test.txt`` now contains the async test plugins
|
|
39
|
+
(``pytest-asyncio``, ``pytest-aiohttp``) that the test suite actually
|
|
40
|
+
requires, so the ``[test]`` extra is self-sufficient.
|
|
41
|
+
|
|
42
|
+
Added (release tooling)
|
|
43
|
+
~~~~~~~~~~~~~~~~~~~~~~~
|
|
44
|
+
- GitHub Actions ``release`` workflow that triggers on ``v*`` tags, runs the
|
|
45
|
+
test suite, builds sdist + wheel and publishes to PyPI via
|
|
46
|
+
`trusted publishing <https://docs.pypi.org/trusted-publishers/>`__
|
|
47
|
+
(no API tokens stored in the repository).
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
Version `0.1.0 <https://github.com/mwatson2/pycaldera/tree/v0.1.0>`__
|
|
51
|
+
---------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
Release date: 2026-03-13.
|
|
54
|
+
|
|
55
|
+
Initial release. Async and synchronous clients for the Caldera Connected Spa
|
|
56
|
+
cloud API, with support for authentication, status / live-settings retrieval,
|
|
57
|
+
temperature / pump / light / lock control, and an acknowledgment-polling
|
|
58
|
+
helper for temperature changes.
|
pycaldera-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pycaldera
|
|
3
|
+
Version: 0.1.2
|
|
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: pydantic>=2.0.0
|
|
22
|
+
Provides-Extra: docs
|
|
23
|
+
Requires-Dist: myst-parser; extra == "docs"
|
|
24
|
+
Requires-Dist: pypandoc>=1.15; extra == "docs"
|
|
25
|
+
Requires-Dist: readthedocs-sphinx-search; python_version >= "3.6" and extra == "docs"
|
|
26
|
+
Requires-Dist: sphinx<6,>=3.5.4; extra == "docs"
|
|
27
|
+
Requires-Dist: sphinx-autobuild; extra == "docs"
|
|
28
|
+
Requires-Dist: sphinx_book_theme; extra == "docs"
|
|
29
|
+
Requires-Dist: watchdog<1.0.0; python_version < "3.6" and extra == "docs"
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
32
|
+
Requires-Dist: pytest-aiohttp>=1.0.0; extra == "test"
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
34
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: isort>=5.12.0; extra == "dev"
|
|
38
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
39
|
+
Requires-Dist: pylint>=2.17.0; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
41
|
+
Requires-Dist: pytest-aiohttp>=1.0.0; extra == "dev"
|
|
42
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
43
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
44
|
+
Provides-Extra: all
|
|
45
|
+
Requires-Dist: black>=23.0.0; extra == "all"
|
|
46
|
+
Requires-Dist: isort>=5.12.0; extra == "all"
|
|
47
|
+
Requires-Dist: mypy>=1.0.0; extra == "all"
|
|
48
|
+
Requires-Dist: myst-parser; extra == "all"
|
|
49
|
+
Requires-Dist: pylint>=2.17.0; extra == "all"
|
|
50
|
+
Requires-Dist: pypandoc>=1.15; extra == "all"
|
|
51
|
+
Requires-Dist: pytest-aiohttp>=1.0.0; extra == "all"
|
|
52
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "all"
|
|
53
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "all"
|
|
54
|
+
Requires-Dist: pytest>=7.0.0; extra == "all"
|
|
55
|
+
Requires-Dist: readthedocs-sphinx-search; python_version >= "3.6" and extra == "all"
|
|
56
|
+
Requires-Dist: sphinx-autobuild; extra == "all"
|
|
57
|
+
Requires-Dist: sphinx<6,>=3.5.4; extra == "all"
|
|
58
|
+
Requires-Dist: sphinx_book_theme; extra == "all"
|
|
59
|
+
Requires-Dist: watchdog<1.0.0; python_version < "3.6" and extra == "all"
|
|
60
|
+
Dynamic: author
|
|
61
|
+
Dynamic: author-email
|
|
62
|
+
Dynamic: classifier
|
|
63
|
+
Dynamic: description
|
|
64
|
+
Dynamic: description-content-type
|
|
65
|
+
Dynamic: home-page
|
|
66
|
+
Dynamic: license
|
|
67
|
+
Dynamic: license-file
|
|
68
|
+
Dynamic: provides-extra
|
|
69
|
+
Dynamic: requires-dist
|
|
70
|
+
Dynamic: requires-python
|
|
71
|
+
Dynamic: summary
|
|
72
|
+
|
|
73
|
+
# pycaldera
|
|
74
|
+
|
|
75
|
+
Python client library for controlling Caldera spas via their cloud API.
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install pycaldera
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Usage
|
|
84
|
+
|
|
85
|
+
### Asynchronous API
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import asyncio
|
|
89
|
+
from pycaldera import AsyncCalderaClient, PUMP_OFF, PUMP_LOW, PUMP_HIGH
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def main():
|
|
93
|
+
async with AsyncCalderaClient("email@example.com", "password") as spa:
|
|
94
|
+
# Get current spa status
|
|
95
|
+
status = await spa.get_spa_status()
|
|
96
|
+
print(f"Current temperature: {status.water_temperature}°F")
|
|
97
|
+
|
|
98
|
+
# Get detailed live settings
|
|
99
|
+
settings = await spa.get_live_settings()
|
|
100
|
+
print(f"Target temperature: {settings.ctrl_head_set_temperature}°F")
|
|
101
|
+
|
|
102
|
+
# Control the spa
|
|
103
|
+
await spa.set_temperature(102) # Set temperature to 102°F
|
|
104
|
+
await spa.set_pump(1, PUMP_HIGH) # Set pump 1 to high speed
|
|
105
|
+
await spa.set_lights(True) # Turn on the lights
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
asyncio.run(main())
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Synchronous API
|
|
112
|
+
|
|
113
|
+
For simpler use cases, a synchronous wrapper is also available:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from pycaldera import CalderaClient, PUMP_OFF, PUMP_LOW, PUMP_HIGH
|
|
117
|
+
|
|
118
|
+
with CalderaClient("email@example.com", "password") as spa:
|
|
119
|
+
# Get current spa status
|
|
120
|
+
status = spa.get_spa_status()
|
|
121
|
+
print(f"Current temperature: {status.water_temperature}°F")
|
|
122
|
+
|
|
123
|
+
# Get detailed live settings
|
|
124
|
+
settings = spa.get_live_settings()
|
|
125
|
+
print(f"Target temperature: {settings.ctrl_head_set_temperature}°F")
|
|
126
|
+
|
|
127
|
+
# Control the spa
|
|
128
|
+
spa.set_temperature(102) # Set temperature to 102°F
|
|
129
|
+
spa.set_pump(1, PUMP_HIGH) # Set pump 1 to high speed
|
|
130
|
+
spa.set_lights(True) # Turn on the lights
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Both clients provide identical functionality, with the synchronous client simply wrapping the async one for convenience.
|
|
134
|
+
|
|
135
|
+
## API Reference
|
|
136
|
+
|
|
137
|
+
### AsyncCalderaClient
|
|
138
|
+
|
|
139
|
+
The main async client class for interacting with the spa. All operations must be performed within an async context manager:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
async with AsyncCalderaClient(
|
|
143
|
+
email="email@example.com",
|
|
144
|
+
password="password",
|
|
145
|
+
timeout=10.0, # Optional: request timeout in seconds
|
|
146
|
+
debug=False, # Optional: enable debug logging
|
|
147
|
+
) as spa:
|
|
148
|
+
# All spa operations must be inside this block
|
|
149
|
+
await spa.get_spa_status()
|
|
150
|
+
await spa.set_temperature(102)
|
|
151
|
+
# etc...
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### CalderaClient
|
|
155
|
+
|
|
156
|
+
A synchronous wrapper around AsyncCalderaClient that provides the same functionality without requiring async/await:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
with CalderaClient(
|
|
160
|
+
email="email@example.com",
|
|
161
|
+
password="password",
|
|
162
|
+
timeout=10.0, # Optional: request timeout in seconds
|
|
163
|
+
debug=False, # Optional: enable debug logging
|
|
164
|
+
) as spa:
|
|
165
|
+
# All spa operations can be called synchronously
|
|
166
|
+
spa.get_spa_status()
|
|
167
|
+
spa.set_temperature(102)
|
|
168
|
+
# etc...
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Error Handling
|
|
172
|
+
|
|
173
|
+
All operations can raise these base exceptions:
|
|
174
|
+
- `AuthenticationError`: When authentication fails or token expires
|
|
175
|
+
- `ConnectionError`: When network connection fails or API is unreachable
|
|
176
|
+
- `SpaControlError`: When the API returns an error response
|
|
177
|
+
|
|
178
|
+
### Temperature Control
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
async with spa as client:
|
|
182
|
+
# Set temperature (80-104°F or 26.5-40°C)
|
|
183
|
+
try:
|
|
184
|
+
# Basic temperature setting
|
|
185
|
+
await client.set_temperature(102) # Fahrenheit
|
|
186
|
+
await client.set_temperature(39, "C") # Celsius
|
|
187
|
+
|
|
188
|
+
# Wait for spa to acknowledge the temperature change
|
|
189
|
+
await client.set_temperature(102, wait_for_ack=True)
|
|
190
|
+
|
|
191
|
+
# Control polling behavior when waiting for acknowledgment
|
|
192
|
+
await client.set_temperature(
|
|
193
|
+
102,
|
|
194
|
+
wait_for_ack=True,
|
|
195
|
+
polling_interval=5.0, # Check every 5 seconds
|
|
196
|
+
polling_timeout=120.0, # Time out after 2 minutes
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Manually wait for temperature acknowledgment
|
|
200
|
+
settings = await client.wait_for_temperature_ack(
|
|
201
|
+
expected_temp=102, # Expected temperature in Fahrenheit
|
|
202
|
+
interval=5.0, # Check every 5 seconds
|
|
203
|
+
timeout=120.0, # Time out after 2 minutes
|
|
204
|
+
)
|
|
205
|
+
except InvalidParameterError:
|
|
206
|
+
# Raised when temperature is outside valid range
|
|
207
|
+
# (80-104°F or 26.5-40°C)
|
|
208
|
+
pass
|
|
209
|
+
except SpaControlError:
|
|
210
|
+
# Raised when polling times out waiting for acknowledgment
|
|
211
|
+
pass
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Pump Control
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
async with spa as client:
|
|
218
|
+
try:
|
|
219
|
+
await client.set_pump(1, PUMP_HIGH) # Set pump 1 to high speed
|
|
220
|
+
await client.set_pump(2, PUMP_LOW) # Set pump 2 to low speed
|
|
221
|
+
await client.set_pump(3, PUMP_OFF) # Turn off pump 3
|
|
222
|
+
except InvalidParameterError:
|
|
223
|
+
# Raised when:
|
|
224
|
+
# - pump_number is not 1, 2, or 3
|
|
225
|
+
# - speed is not PUMP_OFF (0), PUMP_LOW (1), or PUMP_HIGH (2)
|
|
226
|
+
pass
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Light Control
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
async with spa as client:
|
|
233
|
+
try:
|
|
234
|
+
await client.set_lights(True) # Turn lights on
|
|
235
|
+
await client.set_lights(False) # Turn lights off
|
|
236
|
+
except SpaControlError:
|
|
237
|
+
# Raised when light control fails
|
|
238
|
+
pass
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Status & Settings
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
async with spa as client:
|
|
245
|
+
try:
|
|
246
|
+
# Get basic spa status
|
|
247
|
+
status = await client.get_spa_status()
|
|
248
|
+
print(f"Online: {status.status == 'ONLINE'}")
|
|
249
|
+
|
|
250
|
+
# Get detailed live settings
|
|
251
|
+
settings = await client.get_live_settings()
|
|
252
|
+
print(f"Target temp: {settings.ctrl_head_set_temperature}°F")
|
|
253
|
+
except ConnectionError:
|
|
254
|
+
# Raised when spa is offline or unreachable
|
|
255
|
+
pass
|
|
256
|
+
except SpaControlError:
|
|
257
|
+
# Raised when API returns invalid data
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
## Development
|
|
261
|
+
|
|
262
|
+
1. Clone the repository
|
|
263
|
+
2. Create a virtual environment:
|
|
264
|
+
```bash
|
|
265
|
+
python -m venv venv
|
|
266
|
+
source venv/bin/activate # or `venv\Scripts\activate` on Windows
|
|
267
|
+
```
|
|
268
|
+
3. Install development dependencies:
|
|
269
|
+
```bash
|
|
270
|
+
pip install -r requirements-dev.txt
|
|
271
|
+
```
|
|
272
|
+
4. Install pre-commit hooks:
|
|
273
|
+
```bash
|
|
274
|
+
pre-commit install
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The pre-commit hooks will run automatically on git commit, checking:
|
|
278
|
+
- Code formatting (Black)
|
|
279
|
+
- Import sorting (isort)
|
|
280
|
+
- Type checking (mypy)
|
|
281
|
+
- Linting (pylint, ruff)
|
|
282
|
+
- YAML/TOML syntax
|
|
283
|
+
- Trailing whitespace and file endings
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
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.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.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.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"""Python client for Caldera Spa Connexion API."""
|
|
2
2
|
|
|
3
|
+
from .__meta__ import __version__
|
|
3
4
|
from .async_client import AsyncCalderaClient
|
|
5
|
+
from .client import CalderaClient
|
|
6
|
+
from .const import PUMP_HIGH, PUMP_LOW, PUMP_OFF
|
|
4
7
|
from .exceptions import (
|
|
5
8
|
AuthenticationError,
|
|
6
9
|
CalderaError,
|
|
@@ -8,15 +11,20 @@ from .exceptions import (
|
|
|
8
11
|
InvalidParameterError,
|
|
9
12
|
SpaControlError,
|
|
10
13
|
)
|
|
11
|
-
from .models import LiveSettings
|
|
14
|
+
from .models import LiveSettings, PumpInfo
|
|
12
15
|
|
|
13
16
|
__all__ = [
|
|
14
17
|
"AsyncCalderaClient",
|
|
18
|
+
"CalderaClient",
|
|
15
19
|
"LiveSettings",
|
|
20
|
+
"PumpInfo",
|
|
21
|
+
"PUMP_OFF",
|
|
22
|
+
"PUMP_LOW",
|
|
23
|
+
"PUMP_HIGH",
|
|
16
24
|
"CalderaError",
|
|
17
25
|
"AuthenticationError",
|
|
18
26
|
"ConnectionError",
|
|
19
27
|
"SpaControlError",
|
|
20
28
|
"InvalidParameterError",
|
|
29
|
+
"__version__",
|
|
21
30
|
]
|
|
22
|
-
__version__ = "0.1.0"
|
|
@@ -4,7 +4,8 @@ name = "pycaldera"
|
|
|
4
4
|
path = name.lower().replace("-", "_").replace(" ", "_")
|
|
5
5
|
# Your version number should follow https://python.org/dev/peps/pep-0440 and
|
|
6
6
|
# https://semver.org
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
__version__ = version
|
|
8
9
|
author = "Mark Watson"
|
|
9
10
|
author_email = "markwatson@cantab.net"
|
|
10
11
|
description = "Unofficial Python client for Caldera Spa API" # One-liner
|