python-pooldose 0.4.6__tar.gz → 0.5.1__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_pooldose-0.4.6 → python_pooldose-0.5.1}/PKG-INFO +427 -163
- python_pooldose-0.5.1/README.md +772 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/pyproject.toml +1 -1
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/client.py +28 -71
- python_pooldose-0.5.1/src/pooldose/constants.py +29 -0
- python_pooldose-0.5.1/src/pooldose/mappings/model_PDPR1H1HAR1V0_FW539224.json +110 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/mappings/model_PDPR1H1HAW100_FW539187.json +8 -0
- python_pooldose-0.5.1/src/pooldose/mock_client.py +256 -0
- python_pooldose-0.5.1/src/pooldose/values/instant_values.py +451 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/python_pooldose.egg-info/PKG-INFO +427 -163
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/python_pooldose.egg-info/SOURCES.txt +3 -0
- python_pooldose-0.5.1/tests/test_client.py +270 -0
- python_pooldose-0.5.1/tests/test_instant_values.py +206 -0
- python_pooldose-0.5.1/tests/test_static_values.py +25 -0
- python_pooldose-0.4.6/README.md +0 -508
- python_pooldose-0.4.6/src/pooldose/values/instant_values.py +0 -270
- python_pooldose-0.4.6/tests/test_client.py +0 -67
- python_pooldose-0.4.6/tests/test_instant_values.py +0 -77
- python_pooldose-0.4.6/tests/test_static_values.py +0 -43
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/LICENSE +0 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/setup.cfg +0 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/__init__.py +0 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/mappings/__init__.py +0 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/mappings/mapping_info.py +0 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/request_handler.py +0 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/request_status.py +0 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/values/__init__.py +0 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/values/static_values.py +0 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/python_pooldose.egg-info/dependency_links.txt +0 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/python_pooldose.egg-info/requires.txt +0 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/python_pooldose.egg-info/top_level.txt +0 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/tests/test_mapping_info.py +0 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/tests/test_request_handler.py +0 -0
- {python_pooldose-0.4.6 → python_pooldose-0.5.1}/tests/test_ssl_support.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-pooldose
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Unoffical async Python client for SEKO PoolDose devices
|
|
5
5
|
Author-email: Lukas Maertin <pypi@lukas-maertin.de>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -17,16 +17,20 @@ Requires-Dist: pytest-asyncio; extra == "dev"
|
|
|
17
17
|
Dynamic: license-file
|
|
18
18
|
|
|
19
19
|
# python-pooldose
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
Unofficial async Python client for [SEKO](https://www.seko.com/) Pooldosing systems. SEKO is a manufacturer of various monitoring and control devices for pools and spas.
|
|
22
|
+
|
|
21
23
|
This client uses an undocumented local HTTP API. It provides live readings for pool sensors such as temperature, pH, ORP/Redox, as well as status information and control over the dosing logic.
|
|
22
24
|
|
|
23
25
|
## Features
|
|
26
|
+
|
|
24
27
|
- **Async/await support** for non-blocking operations
|
|
25
28
|
- **Dynamic sensor discovery** based on device model and firmware
|
|
26
29
|
- **Dictionary-style access** to instant values
|
|
27
|
-
- **
|
|
30
|
+
- **Structured data API** with type-based organization
|
|
28
31
|
- **Secure by default** - WiFi passwords excluded unless explicitly requested
|
|
29
32
|
- **Comprehensive error handling** with detailed logging
|
|
33
|
+
- **SSL/HTTPS support** for secure communication
|
|
30
34
|
|
|
31
35
|
## API Overview
|
|
32
36
|
|
|
@@ -34,26 +38,23 @@ This client uses an undocumented local HTTP API. It provides live readings for p
|
|
|
34
38
|
|
|
35
39
|
```
|
|
36
40
|
1. Create PooldoseClient
|
|
37
|
-
├──
|
|
38
|
-
│ ├── Debug Config
|
|
41
|
+
├── Connect to Device
|
|
42
|
+
│ ├── Fetch Device Info (Debug Config)
|
|
39
43
|
│ ├── WiFi Station Info (optional)
|
|
40
44
|
│ ├── Access Point Info (optional)
|
|
41
45
|
│ └── Network Info
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
└──
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
└── 'sensor_name' in instant_values
|
|
55
|
-
|
|
56
|
-
3. Set Values via Type Methods
|
|
46
|
+
└── Load Mapping JSON (based on MODEL_ID + FW_CODE)
|
|
47
|
+
|
|
48
|
+
2. Get Static Values
|
|
49
|
+
└── Device information and configuration
|
|
50
|
+
|
|
51
|
+
3. Get Instant Values
|
|
52
|
+
├── Dictionary-style access: instant_values['temperature']
|
|
53
|
+
├── Get with default: instant_values.get('ph', default)
|
|
54
|
+
├── Check existence: 'sensor_name' in instant_values
|
|
55
|
+
└── Structured access: instant_values_structured()
|
|
56
|
+
|
|
57
|
+
4. Set Values via Type Methods
|
|
57
58
|
├── set_number()
|
|
58
59
|
├── set_switch()
|
|
59
60
|
└── set_select()
|
|
@@ -81,29 +82,23 @@ This client uses an undocumented local HTTP API. It provides live readings for p
|
|
|
81
82
|
└─────────────────┘ └─────────────────┘
|
|
82
83
|
│
|
|
83
84
|
▼
|
|
84
|
-
┌─────────────────┐
|
|
85
|
-
│ Type Discovery │
|
|
86
|
-
│ • Sensors │
|
|
87
|
-
│ • Switches │
|
|
88
|
-
│ • Numbers │
|
|
89
|
-
│ • Selects │
|
|
90
|
-
└─────────────────┘
|
|
91
|
-
│
|
|
92
|
-
▼
|
|
93
85
|
┌─────────────────┐ ┌─────────────────┐
|
|
94
86
|
│ InstantValues │────│ Dictionary API │
|
|
95
87
|
└─────────────────┘ └─────────────────┘
|
|
96
88
|
│
|
|
97
89
|
▼
|
|
98
90
|
┌─────────────────┐
|
|
99
|
-
│
|
|
100
|
-
│ •
|
|
101
|
-
│ •
|
|
102
|
-
│ •
|
|
91
|
+
│ Structured API │
|
|
92
|
+
│ • sensor{} │
|
|
93
|
+
│ • number{} │
|
|
94
|
+
│ • switch{} │
|
|
95
|
+
│ • binary_sensor{}│
|
|
96
|
+
│ • select{} │
|
|
103
97
|
└─────────────────┘
|
|
104
98
|
```
|
|
105
99
|
|
|
106
100
|
## Prerequisites
|
|
101
|
+
|
|
107
102
|
1. Install and set-up the PoolDose devices according to the user manual.
|
|
108
103
|
1. In particular, connect the device to your WiFi network.
|
|
109
104
|
2. Identify the IP address or hostname of the device.
|
|
@@ -174,9 +169,246 @@ client = PooldoseClient("192.168.1.100", use_ssl=True, port=8443, ssl_verify=Fal
|
|
|
174
169
|
pip install python-pooldose
|
|
175
170
|
```
|
|
176
171
|
|
|
172
|
+
## Examples
|
|
173
|
+
|
|
174
|
+
The `examples/` directory contains demonstration scripts that show how to use the python-pooldose library:
|
|
175
|
+
|
|
176
|
+
### 1. Real Device Demo (`examples/demo.py`)
|
|
177
|
+
|
|
178
|
+
Demonstrates connecting to a real PoolDose device and accessing all types of data:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Edit the HOST variable in the file first
|
|
182
|
+
python examples/demo.py
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Features:**
|
|
186
|
+
|
|
187
|
+
- Connects to actual hardware
|
|
188
|
+
- Shows device information and static values
|
|
189
|
+
- Displays all sensor readings, alarms, setpoints, and settings
|
|
190
|
+
- Demonstrates error handling
|
|
191
|
+
|
|
192
|
+
### 2. Mock Client Demo (`examples/demo_mock.py`)
|
|
193
|
+
|
|
194
|
+
Shows how to use the mock client with JSON files for development and testing:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# Run with sample data
|
|
198
|
+
python examples/demo_mock.py references/testdaten/suplere/instantvalues.json
|
|
199
|
+
|
|
200
|
+
# Use custom JSON file
|
|
201
|
+
python examples/demo_mock.py path/to/your/data.json
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Features:**
|
|
205
|
+
|
|
206
|
+
- No hardware required
|
|
207
|
+
- Uses real device data from JSON files
|
|
208
|
+
- Same API as real client
|
|
209
|
+
- Perfect for development and CI/CD
|
|
210
|
+
|
|
211
|
+
### Benefits of the Examples
|
|
212
|
+
|
|
213
|
+
- **Learning**: Step-by-step progression from simple to advanced usage
|
|
214
|
+
- **Development**: Mock client allows development without hardware
|
|
215
|
+
- **Testing**: JSON-based testing for CI/CD pipelines
|
|
216
|
+
- **Reference**: Real-world code patterns and best practices
|
|
217
|
+
|
|
218
|
+
## Mock Client System
|
|
219
|
+
|
|
220
|
+
The **MockPooldoseClient** system allows using JSON files instead of real Pooldose hardware for testing and development. This is particularly useful for:
|
|
221
|
+
|
|
222
|
+
- **Development without hardware**
|
|
223
|
+
- **Unit tests**
|
|
224
|
+
- **Data analysis with real device data**
|
|
225
|
+
- **CI/CD pipeline tests**
|
|
226
|
+
|
|
227
|
+
### Mock Client Quick Start
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
import asyncio
|
|
231
|
+
from pathlib import Path
|
|
232
|
+
from pooldose.mock_client import MockPooldoseClient
|
|
233
|
+
|
|
234
|
+
async def simple_test():
|
|
235
|
+
# Load data file
|
|
236
|
+
json_file = Path("path/to/your/data.json")
|
|
237
|
+
|
|
238
|
+
# Create mock client
|
|
239
|
+
client = MockPooldoseClient(json_file_path=json_file)
|
|
240
|
+
|
|
241
|
+
# Connect (loads mapping data)
|
|
242
|
+
status = await client.connect()
|
|
243
|
+
if status.name != "SUCCESS":
|
|
244
|
+
print(f"Connection failed: {status}")
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
# Get sensor values
|
|
248
|
+
status, instant_values = await client.instant_values()
|
|
249
|
+
if status.name == "SUCCESS" and instant_values:
|
|
250
|
+
print(f"Temperature: {instant_values['temperature']}")
|
|
251
|
+
print(f"pH Value: {instant_values['ph']}")
|
|
252
|
+
print(f"ORP: {instant_values['orp']}")
|
|
253
|
+
|
|
254
|
+
# Get structured data
|
|
255
|
+
status, data = await client.instant_values_structured()
|
|
256
|
+
if status.name == "SUCCESS":
|
|
257
|
+
sensors = data.get('sensor', {})
|
|
258
|
+
for name, info in sensors.items():
|
|
259
|
+
value = info.get('value', 'N/A')
|
|
260
|
+
unit = info.get('unit', '')
|
|
261
|
+
print(f"{name}: {value} {unit}")
|
|
262
|
+
|
|
263
|
+
# Run demo
|
|
264
|
+
asyncio.run(simple_test())
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Mock Client Command Line Usage
|
|
268
|
+
|
|
269
|
+
You can run the demo script with custom JSON files:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
# Run with sample data
|
|
273
|
+
python examples/demo_mock.py references/testdaten/suplere/instantvalues.json
|
|
274
|
+
|
|
275
|
+
# Use custom JSON file
|
|
276
|
+
python examples/demo_mock.py path/to/your/data.json
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### JSON Data Format
|
|
280
|
+
|
|
281
|
+
The JSON file must have the following structure:
|
|
282
|
+
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
"devicedata": {
|
|
286
|
+
"SERIALNUMBER_DEVICE": {
|
|
287
|
+
"MODEL_FW_w_key1": {
|
|
288
|
+
"current": 25.5,
|
|
289
|
+
"magnitude": ["°C"]
|
|
290
|
+
},
|
|
291
|
+
"MODEL_FW_w_key2": {
|
|
292
|
+
"current": 7.2,
|
|
293
|
+
"magnitude": ["pH"]
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Mock Client API Methods
|
|
301
|
+
|
|
302
|
+
#### Initialization
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
client = MockPooldoseClient(
|
|
306
|
+
json_file_path="path/to/data.json",
|
|
307
|
+
timeout=30, # Ignored (compatibility)
|
|
308
|
+
include_sensitive_data=True # Include WiFi keys etc.
|
|
309
|
+
)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
#### Connection
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
status = await client.connect() # Loads mapping configuration
|
|
316
|
+
is_connected = client.is_connected # Check status
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### Data Retrieval
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
# Static device information
|
|
323
|
+
status, static_values = client.static_values()
|
|
324
|
+
|
|
325
|
+
# Live sensor values
|
|
326
|
+
status, instant_values = await client.instant_values()
|
|
327
|
+
|
|
328
|
+
# Structured data (grouped by types)
|
|
329
|
+
status, structured_data = await client.instant_values_structured()
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
#### Utility Methods
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
# Get raw data
|
|
336
|
+
raw_data = client.get_raw_data()
|
|
337
|
+
device_data = client.get_device_data()
|
|
338
|
+
|
|
339
|
+
# Reload JSON file
|
|
340
|
+
success = client.reload_data()
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Available Sample Files
|
|
344
|
+
|
|
345
|
+
The following sample JSON files are available in the repository:
|
|
346
|
+
|
|
347
|
+
- `references/testdaten/suplere/instantvalues.json` - PDPR1H1HAR1V0_FW539224 device
|
|
348
|
+
- `references/testdaten/instantvalues_poolforum_1.json` - Additional sample data
|
|
349
|
+
|
|
350
|
+
### Mock Client Use Cases
|
|
351
|
+
|
|
352
|
+
#### Unit Tests
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
def test_temperature_reading():
|
|
356
|
+
client = MockPooldoseClient("sample_data.json")
|
|
357
|
+
asyncio.run(client.connect())
|
|
358
|
+
|
|
359
|
+
status, values = asyncio.run(client.instant_values())
|
|
360
|
+
assert status.name == "SUCCESS"
|
|
361
|
+
assert values['temperature'][0] == 23.0 # Expected value
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
#### Data Analysis
|
|
365
|
+
|
|
366
|
+
```python
|
|
367
|
+
# Analyze all sensor values
|
|
368
|
+
client = MockPooldoseClient("production_data.json")
|
|
369
|
+
await client.connect()
|
|
370
|
+
|
|
371
|
+
status, data = await client.instant_values_structured()
|
|
372
|
+
sensors = data.get('sensor', {})
|
|
373
|
+
|
|
374
|
+
for sensor_name, sensor_data in sensors.items():
|
|
375
|
+
value = sensor_data.get('value')
|
|
376
|
+
unit = sensor_data.get('unit', '')
|
|
377
|
+
print(f"{sensor_name}: {value} {unit}")
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### Integration Tests
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
async def test_full_integration():
|
|
384
|
+
client = MockPooldoseClient("integration_sample_data.json")
|
|
385
|
+
|
|
386
|
+
# Test connection
|
|
387
|
+
assert await client.connect() == RequestStatus.SUCCESS
|
|
388
|
+
|
|
389
|
+
# Test static values
|
|
390
|
+
status, static = client.static_values()
|
|
391
|
+
assert status == RequestStatus.SUCCESS
|
|
392
|
+
assert static.sensor_name is not None
|
|
393
|
+
|
|
394
|
+
# Test live values
|
|
395
|
+
status, instant = await client.instant_values()
|
|
396
|
+
assert status == RequestStatus.SUCCESS
|
|
397
|
+
assert 'temperature' in instant
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Benefits of the Mock System
|
|
401
|
+
|
|
402
|
+
- **Fast**: No network latency
|
|
403
|
+
- **Reliable**: No hardware dependencies
|
|
404
|
+
- **Flexible**: Different scenarios testable
|
|
405
|
+
- **Realistic**: Real device data structures
|
|
406
|
+
- **Compatible**: Same API as real client
|
|
407
|
+
|
|
177
408
|
## Example Usage
|
|
178
409
|
|
|
179
410
|
### Basic Example
|
|
411
|
+
|
|
180
412
|
```python
|
|
181
413
|
import asyncio
|
|
182
414
|
import json
|
|
@@ -187,7 +419,7 @@ HOST = "192.168.1.100" # Change this to your device's host or IP address
|
|
|
187
419
|
TIMEOUT = 30
|
|
188
420
|
|
|
189
421
|
async def main() -> None:
|
|
190
|
-
"""Demonstrate PooldoseClient usage with
|
|
422
|
+
"""Demonstrate PooldoseClient usage with dictionary-based API."""
|
|
191
423
|
|
|
192
424
|
# Create client instance (excludes WiFi passwords by default)
|
|
193
425
|
client = PooldoseClient(host=HOST, timeout=TIMEOUT)
|
|
@@ -204,18 +436,6 @@ async def main() -> None:
|
|
|
204
436
|
print(f"Connected to {HOST}")
|
|
205
437
|
print("Device Info:", json.dumps(client.device_info, indent=2))
|
|
206
438
|
|
|
207
|
-
# --- Query available types dynamically ---
|
|
208
|
-
print("\nAvailable types:")
|
|
209
|
-
for typ, keys in client.available_types().items():
|
|
210
|
-
print(f" {typ}: {keys}")
|
|
211
|
-
|
|
212
|
-
# --- Query available sensors ---
|
|
213
|
-
print("\nAvailable sensors:")
|
|
214
|
-
for name, sensor in client.available_sensors().items():
|
|
215
|
-
print(f" {name}: key={sensor.key}, type={sensor.type}")
|
|
216
|
-
if sensor.conversion is not None:
|
|
217
|
-
print(f" conversion: {sensor.conversion}")
|
|
218
|
-
|
|
219
439
|
# --- Get static values ---
|
|
220
440
|
status, static_values = client.static_values()
|
|
221
441
|
if status == RequestStatus.SUCCESS:
|
|
@@ -223,21 +443,12 @@ async def main() -> None:
|
|
|
223
443
|
print(f"Serial Number: {static_values.sensor_serial_number}")
|
|
224
444
|
print(f"Firmware Version: {static_values.sensor_fw_version}")
|
|
225
445
|
|
|
226
|
-
# --- Get instant values ---
|
|
446
|
+
# --- Get instant values (dictionary-style) ---
|
|
227
447
|
status, instant_values = await client.instant_values()
|
|
228
448
|
if status != RequestStatus.SUCCESS:
|
|
229
449
|
print(f"Error getting instant values: {status}")
|
|
230
450
|
return
|
|
231
451
|
|
|
232
|
-
# --- Dictionary-style access ---
|
|
233
|
-
|
|
234
|
-
# Get all sensors at once
|
|
235
|
-
print("\nAll sensor values:")
|
|
236
|
-
sensors = instant_values.get_sensors()
|
|
237
|
-
for key, value in sensors.items():
|
|
238
|
-
if isinstance(value, tuple) and len(value) >= 2:
|
|
239
|
-
print(f" {key}: {value[0]} {value[1]}")
|
|
240
|
-
|
|
241
452
|
# Dictionary-style individual access
|
|
242
453
|
if "temperature" in instant_values:
|
|
243
454
|
temp = instant_values["temperature"]
|
|
@@ -247,17 +458,73 @@ async def main() -> None:
|
|
|
247
458
|
ph_value = instant_values.get("ph", "Not available")
|
|
248
459
|
print(f"pH: {ph_value}")
|
|
249
460
|
|
|
461
|
+
# --- Get structured instant values ---
|
|
462
|
+
status, structured_data = await client.instant_values_structured()
|
|
463
|
+
if status != RequestStatus.SUCCESS:
|
|
464
|
+
print(f"Error getting structured values: {status}")
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
# Access sensors
|
|
468
|
+
sensors = structured_data.get("sensor", {})
|
|
469
|
+
print("\nSensor Values:")
|
|
470
|
+
for key, sensor_data in sensors.items():
|
|
471
|
+
value = sensor_data.get("value")
|
|
472
|
+
unit = sensor_data.get("unit")
|
|
473
|
+
if unit:
|
|
474
|
+
print(f" {key}: {value} {unit}")
|
|
475
|
+
else:
|
|
476
|
+
print(f" {key}: {value}")
|
|
477
|
+
|
|
478
|
+
# Access numbers (setpoints)
|
|
479
|
+
numbers = structured_data.get("number", {})
|
|
480
|
+
print("\nSetpoints:")
|
|
481
|
+
for key, number_data in numbers.items():
|
|
482
|
+
value = number_data.get("value")
|
|
483
|
+
unit = number_data.get("unit")
|
|
484
|
+
min_val = number_data.get("min")
|
|
485
|
+
max_val = number_data.get("max")
|
|
486
|
+
|
|
487
|
+
if unit:
|
|
488
|
+
print(f" {key}: {value} {unit} (Range: {min_val}-{max_val})")
|
|
489
|
+
else:
|
|
490
|
+
print(f" {key}: {value} (Range: {min_val}-{max_val})")
|
|
491
|
+
|
|
492
|
+
# Access switches
|
|
493
|
+
switches = structured_data.get("switch", {})
|
|
494
|
+
print("\nSwitches:")
|
|
495
|
+
for key, switch_data in switches.items():
|
|
496
|
+
value = switch_data.get("value")
|
|
497
|
+
status_text = "ON" if value else "OFF"
|
|
498
|
+
print(f" {key}: {status_text}")
|
|
499
|
+
|
|
500
|
+
# Access binary sensors (alarms/status)
|
|
501
|
+
binary_sensors = structured_data.get("binary_sensor", {})
|
|
502
|
+
print("\nAlarms & Status:")
|
|
503
|
+
for key, sensor_data in binary_sensors.items():
|
|
504
|
+
value = sensor_data.get("value")
|
|
505
|
+
status_text = "ACTIVE" if value else "OK"
|
|
506
|
+
print(f" {key}: {status_text}")
|
|
507
|
+
|
|
508
|
+
# Access selects (configuration options)
|
|
509
|
+
selects = structured_data.get("select", {})
|
|
510
|
+
print("\nSettings:")
|
|
511
|
+
for key, select_data in selects.items():
|
|
512
|
+
value = select_data.get("value")
|
|
513
|
+
print(f" {key}: {value}")
|
|
514
|
+
|
|
250
515
|
# --- Setting values ---
|
|
251
516
|
|
|
252
|
-
# Set number values
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
print(f"Set pH target to 7.2: {result}")
|
|
517
|
+
# Set number values (via InstantValues)
|
|
518
|
+
result = await instant_values.set_number("target_ph", 7.2)
|
|
519
|
+
print(f"Set pH target to 7.2: {result}")
|
|
256
520
|
|
|
257
521
|
# Set switch values
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
522
|
+
result = await instant_values.set_switch("stop_dosing", True)
|
|
523
|
+
print(f"Set stop dosing: {result}")
|
|
524
|
+
|
|
525
|
+
# Set select values
|
|
526
|
+
result = await instant_values.set_select("water_meter_unit", "L/h")
|
|
527
|
+
print(f"Set water meter unit: {result}")
|
|
261
528
|
|
|
262
529
|
if __name__ == "__main__":
|
|
263
530
|
asyncio.run(main())
|
|
@@ -266,6 +533,7 @@ if __name__ == "__main__":
|
|
|
266
533
|
### Advanced Usage
|
|
267
534
|
|
|
268
535
|
#### Connection Management
|
|
536
|
+
|
|
269
537
|
```python
|
|
270
538
|
from pooldose.client import PooldoseClient
|
|
271
539
|
from pooldose.request_status import RequestStatus
|
|
@@ -290,6 +558,7 @@ else:
|
|
|
290
558
|
```
|
|
291
559
|
|
|
292
560
|
#### Error Handling
|
|
561
|
+
|
|
293
562
|
```python
|
|
294
563
|
from pooldose.client import PooldoseClient
|
|
295
564
|
|
|
@@ -308,21 +577,38 @@ else:
|
|
|
308
577
|
print(f"Other error: {status}")
|
|
309
578
|
```
|
|
310
579
|
|
|
311
|
-
####
|
|
580
|
+
#### Working with Structured Data
|
|
581
|
+
|
|
312
582
|
```python
|
|
313
|
-
# Get all
|
|
314
|
-
|
|
315
|
-
binary_sensors = instant_values.get_binary_sensors() # All boolean states
|
|
316
|
-
numbers = instant_values.get_numbers() # All configurable numbers
|
|
317
|
-
switches = instant_values.get_switches() # All switch states
|
|
318
|
-
selects = instant_values.get_selects() # All select options
|
|
583
|
+
# Get all data types at once
|
|
584
|
+
status, structured_data = await client.instant_values_structured()
|
|
319
585
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
586
|
+
if status == RequestStatus.SUCCESS:
|
|
587
|
+
# Check what types are available
|
|
588
|
+
available_types = list(structured_data.keys())
|
|
589
|
+
print("Available types:", available_types)
|
|
590
|
+
|
|
591
|
+
# Process each type
|
|
592
|
+
for data_type, items in structured_data.items():
|
|
593
|
+
print(f"\n{data_type.title()} ({len(items)} items):")
|
|
594
|
+
for key, data in items.items():
|
|
595
|
+
if data_type in ["sensor", "number"]:
|
|
596
|
+
value = data.get("value")
|
|
597
|
+
unit = data.get("unit")
|
|
598
|
+
if unit:
|
|
599
|
+
print(f" {key}: {value} {unit}")
|
|
600
|
+
else:
|
|
601
|
+
print(f" {key}: {value}")
|
|
602
|
+
elif data_type in ["switch", "binary_sensor"]:
|
|
603
|
+
value = data.get("value")
|
|
604
|
+
print(f" {key}: {'ON' if value else 'OFF'}")
|
|
605
|
+
elif data_type == "select":
|
|
606
|
+
value = data.get("value")
|
|
607
|
+
print(f" {key}: {value}")
|
|
323
608
|
```
|
|
324
609
|
|
|
325
610
|
#### Working with Mappings
|
|
611
|
+
|
|
326
612
|
```
|
|
327
613
|
Mapping Discovery Process:
|
|
328
614
|
┌─────────────────┐
|
|
@@ -348,74 +634,42 @@ Mapping Discovery Process:
|
|
|
348
634
|
│ │ Switches │ │ ──────► stop_dosing, pump_detection, ...
|
|
349
635
|
│ │ Numbers │ │ ──────► ph_target, orp_target, ...
|
|
350
636
|
│ │ Selects │ │ ──────► water_meter_unit, ...
|
|
637
|
+
│ │ Binary Sens │ │ ──────► alarm_ph, alarm_orp, ...
|
|
351
638
|
│ └─────────────┘ │
|
|
352
639
|
└─────────────────┘
|
|
353
640
|
```
|
|
354
641
|
|
|
355
|
-
```python
|
|
356
|
-
# Query what's available for your specific device
|
|
357
|
-
print("\nAvailable sensors:")
|
|
358
|
-
for name, sensor in client.available_sensors().items():
|
|
359
|
-
print(f" {name}: key={sensor.key}")
|
|
360
|
-
if sensor.conversion:
|
|
361
|
-
print(f" conversion: {sensor.conversion}")
|
|
362
|
-
|
|
363
|
-
print("\nAvailable numbers (settable):")
|
|
364
|
-
for name, number in client.available_numbers().items():
|
|
365
|
-
print(f" {name}: key={number.key}")
|
|
366
|
-
|
|
367
|
-
print("\nAvailable switches:")
|
|
368
|
-
for name, switch in client.available_switches().items():
|
|
369
|
-
print(f" {name}: key={switch.key}")
|
|
370
|
-
```
|
|
371
|
-
|
|
372
642
|
## API Reference
|
|
373
643
|
|
|
374
|
-
### PooldoseClient Class
|
|
375
|
-
```
|
|
376
|
-
PooldoseClient
|
|
377
|
-
├── Device Info
|
|
378
|
-
│ ├── static_values() ──────► StaticValues
|
|
379
|
-
│ └── device_info{} ─────────► dict
|
|
380
|
-
├── Type Discovery
|
|
381
|
-
│ ├── available_types() ────► dict[str, list[str]]
|
|
382
|
-
│ ├── available_sensors() ──► dict[str, SensorMapping]
|
|
383
|
-
│ ├── available_numbers() ──► dict[str, NumberMapping]
|
|
384
|
-
│ ├── available_switches() ─► dict[str, SwitchMapping]
|
|
385
|
-
│ └── available_selects() ──► dict[str, SelectMapping]
|
|
386
|
-
└── Live Data
|
|
387
|
-
└── instant_values() ─────► InstantValues
|
|
388
|
-
```
|
|
644
|
+
### PooldoseClient Class
|
|
389
645
|
|
|
390
646
|
#### Constructor
|
|
647
|
+
|
|
391
648
|
```python
|
|
392
|
-
PooldoseClient(host, timeout=
|
|
649
|
+
PooldoseClient(host, timeout=30, include_sensitive_data=False, use_ssl=False, port=None, ssl_verify=True)
|
|
393
650
|
```
|
|
394
651
|
|
|
395
652
|
**Parameters:**
|
|
653
|
+
|
|
396
654
|
- `host` (str): The hostname or IP address of the device
|
|
397
|
-
- `timeout` (int): Request timeout in seconds (default:
|
|
655
|
+
- `timeout` (int): Request timeout in seconds (default: 30)
|
|
398
656
|
- `include_sensitive_data` (bool): Whether to include sensitive data like WiFi passwords (default: False)
|
|
399
657
|
- `use_ssl` (bool): Whether to use HTTPS instead of HTTP (default: False)
|
|
400
658
|
- `port` (Optional[int]): Custom port for connections. Defaults to 80 for HTTP, 443 for HTTPS (default: None)
|
|
401
659
|
- `ssl_verify` (bool): Whether to verify SSL certificates when using HTTPS (default: True)
|
|
402
660
|
|
|
403
661
|
#### Methods
|
|
404
|
-
|
|
405
|
-
- `
|
|
406
|
-
- `
|
|
407
|
-
- `
|
|
408
|
-
- `
|
|
409
|
-
- `
|
|
410
|
-
- `available_numbers()` - Get available number configurations
|
|
411
|
-
- `available_switches()` - Get available switch configurations
|
|
412
|
-
- `available_selects()` - Get available select configurations
|
|
662
|
+
|
|
663
|
+
- `async connect()` → `RequestStatus` - Connect to device and initialize all components
|
|
664
|
+
- `static_values()` → `tuple[RequestStatus, StaticValues | None]` - Get static device information
|
|
665
|
+
- `async instant_values()` → `tuple[RequestStatus, InstantValues | None]` - Get current sensor readings and device state
|
|
666
|
+
- `async instant_values_structured()` → `tuple[RequestStatus, dict[str, Any]]` - Get structured data organized by type
|
|
667
|
+
- `check_apiversion_supported()` → `tuple[RequestStatus, dict]` - Check API version compatibility
|
|
413
668
|
|
|
414
669
|
#### Properties
|
|
415
|
-
|
|
416
|
-
- `
|
|
417
|
-
- `
|
|
418
|
-
- `timeout` - Request timeout in seconds
|
|
670
|
+
|
|
671
|
+
- `is_connected: bool` - Check if client is connected to device
|
|
672
|
+
- `device_info: dict` - Dictionary containing device information
|
|
419
673
|
|
|
420
674
|
### RequestStatus
|
|
421
675
|
|
|
@@ -425,68 +679,73 @@ All client methods return `RequestStatus` enum values:
|
|
|
425
679
|
from pooldose.request_status import RequestStatus
|
|
426
680
|
|
|
427
681
|
RequestStatus.SUCCESS # Operation successful
|
|
428
|
-
RequestStatus.CONNECTION_ERROR # Network connection failed
|
|
429
682
|
RequestStatus.HOST_UNREACHABLE # Device not reachable
|
|
430
683
|
RequestStatus.PARAMS_FETCH_FAILED # Failed to fetch device parameters
|
|
431
684
|
RequestStatus.API_VERSION_UNSUPPORTED # API version not supported
|
|
432
685
|
RequestStatus.NO_DATA # No data received
|
|
686
|
+
RequestStatus.LAST_DATA # Last valid data used
|
|
687
|
+
RequestStatus.CLIENT_ERROR_SET # Error setting client value
|
|
433
688
|
RequestStatus.UNKNOWN_ERROR # Other error occurred
|
|
434
689
|
```
|
|
435
690
|
|
|
436
691
|
### InstantValues Interface
|
|
437
|
-
```
|
|
438
|
-
InstantValues
|
|
439
|
-
├── Dictionary Interface
|
|
440
|
-
│ ├── [key] ─────────────────► __getitem__
|
|
441
|
-
│ ├── get(key, default) ────► get method
|
|
442
|
-
│ ├── key in values ────────► __contains__
|
|
443
|
-
│ └── [key] = value ────────► __setitem__ (async)
|
|
444
|
-
├── Type Getters
|
|
445
|
-
│ ├── get_sensors() ────────► dict[str, tuple]
|
|
446
|
-
│ ├── get_binary_sensors() ─► dict[str, bool]
|
|
447
|
-
│ ├── get_numbers() ────────► dict[str, tuple]
|
|
448
|
-
│ ├── get_switches() ───────► dict[str, bool]
|
|
449
|
-
│ └── get_selects() ────────► dict[str, int]
|
|
450
|
-
└── Type Setters (async)
|
|
451
|
-
├── set_number(key, value) ──► bool
|
|
452
|
-
├── set_switch(key, value) ──► bool
|
|
453
|
-
└── set_select(key, value) ──► bool
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
#### Dictionary Interface
|
|
457
|
-
```python
|
|
458
|
-
# Reading
|
|
459
|
-
value = instant_values["sensor_name"]
|
|
460
|
-
value = instant_values.get("sensor_name", default)
|
|
461
|
-
exists = "sensor_name" in instant_values
|
|
462
692
|
|
|
463
|
-
|
|
464
|
-
await instant_values.__setitem__("switch_name", True)
|
|
465
|
-
```
|
|
693
|
+
The `InstantValues` class provides dictionary-style access to sensor data:
|
|
466
694
|
|
|
467
|
-
#### Type-specific Methods
|
|
468
695
|
```python
|
|
469
|
-
#
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
696
|
+
# Dictionary Interface
|
|
697
|
+
value = instant_values["sensor_name"] # Direct access
|
|
698
|
+
value = instant_values.get("sensor_name", default) # Get with default
|
|
699
|
+
exists = "sensor_name" in instant_values # Check existence
|
|
700
|
+
|
|
701
|
+
# Setting values (async, with validation)
|
|
702
|
+
await instant_values.set_number("ph_target", 7.2) # Set number value
|
|
703
|
+
await instant_values.set_switch("stop_dosing", True) # Set switch value
|
|
704
|
+
await instant_values.set_select("unit", "L/h") # Set select value
|
|
705
|
+
```
|
|
475
706
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
707
|
+
### Structured Data Format
|
|
708
|
+
|
|
709
|
+
The `instant_values_structured()` method returns data organized by type:
|
|
710
|
+
|
|
711
|
+
```python
|
|
712
|
+
{
|
|
713
|
+
"sensor": {
|
|
714
|
+
"temperature": {"value": 25.5, "unit": "°C"},
|
|
715
|
+
"ph": {"value": 7.2, "unit": None}
|
|
716
|
+
},
|
|
717
|
+
"number": {
|
|
718
|
+
"target_ph": {"value": 7.0, "unit": None, "min": 6.0, "max": 8.0, "step": 0.1}
|
|
719
|
+
},
|
|
720
|
+
"switch": {
|
|
721
|
+
"stop_dosing": {"value": False}
|
|
722
|
+
},
|
|
723
|
+
"binary_sensor": {
|
|
724
|
+
"alarm_ph": {"value": False}
|
|
725
|
+
},
|
|
726
|
+
"select": {
|
|
727
|
+
"water_meter_unit": {"value": "L/h"}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
480
730
|
```
|
|
481
731
|
|
|
732
|
+
#### Data Types
|
|
733
|
+
|
|
734
|
+
- **sensor**: Read-only sensor values with optional units
|
|
735
|
+
- **number**: Configurable numeric values with min/max/step constraints
|
|
736
|
+
- **switch**: Boolean on/off controls
|
|
737
|
+
- **binary_sensor**: Read-only boolean status indicators
|
|
738
|
+
- **select**: Configurable selection options
|
|
739
|
+
|
|
482
740
|
## Supported Devices
|
|
483
741
|
|
|
484
742
|
This client has been tested with:
|
|
743
|
+
|
|
485
744
|
- **PoolDose Double/Dual WiFi** (Model: PDPR1H1HAW100, FW: 539187)
|
|
486
745
|
|
|
487
746
|
Other SEKO PoolDose models may work but are untested. The client uses JSON mapping files to adapt to different device models and firmware versions (see e.g. `src/pooldose/mappings/model_PDPR1H1HAW100_FW539187.json`).
|
|
488
747
|
|
|
489
|
-
> **Note:** The
|
|
748
|
+
> **Note:** The JSON files in the mappings directory define the device-specific data keys and their human-readable names for different PoolDose models and firmware versions.
|
|
490
749
|
|
|
491
750
|
## Security
|
|
492
751
|
|
|
@@ -501,6 +760,7 @@ status = await client.connect()
|
|
|
501
760
|
```
|
|
502
761
|
|
|
503
762
|
### Security Model
|
|
763
|
+
|
|
504
764
|
```
|
|
505
765
|
Data Classification:
|
|
506
766
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
@@ -522,5 +782,9 @@ Data Classification:
|
|
|
522
782
|
|
|
523
783
|
For detailed release notes and version history, please see [CHANGELOG.md](CHANGELOG.md).
|
|
524
784
|
|
|
525
|
-
### Latest Release (0.
|
|
526
|
-
|
|
785
|
+
### Latest Release (0.5.1)
|
|
786
|
+
|
|
787
|
+
- **Examples**: Demo scripts for real and mock clients (`examples/` directory)
|
|
788
|
+
- **Device Support**: Added mapping for model `PDPR1H1HAR1V0_FW539224`
|
|
789
|
+
- **Mock Client**: JSON-based testing framework for development without hardware
|
|
790
|
+
- **Fixed**: Removed deprecated references and improved consistency
|