python-pooldose 0.4.0__tar.gz → 0.4.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.0/src/python_pooldose.egg-info → python_pooldose-0.4.1}/PKG-INFO +139 -5
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/README.md +138 -4
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/pyproject.toml +1 -1
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/client.py +28 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/request_handler.py +2 -52
- python_pooldose-0.4.1/src/pooldose/request_status.py +26 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1/src/python_pooldose.egg-info}/PKG-INFO +139 -5
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/python_pooldose.egg-info/SOURCES.txt +1 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/tests/test_client.py +27 -2
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/tests/test_request_handler.py +0 -11
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/LICENSE +0 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/setup.cfg +0 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/__init__.py +0 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/mappings/__init__.py +0 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/mappings/mapping_info.py +0 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/mappings/model_PDPR1H1HAW100_FW539187.json +0 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/values/__init__.py +0 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/values/instant_values.py +0 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/values/static_values.py +0 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/python_pooldose.egg-info/dependency_links.txt +0 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/python_pooldose.egg-info/requires.txt +0 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/python_pooldose.egg-info/top_level.txt +0 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/tests/test_instant_values.py +0 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/tests/test_mapping_info.py +0 -0
- {python_pooldose-0.4.0 → python_pooldose-0.4.1}/tests/test_static_values.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-pooldose
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -125,7 +125,7 @@ pip install python-pooldose
|
|
|
125
125
|
import asyncio
|
|
126
126
|
import json
|
|
127
127
|
from pooldose.client import PooldoseClient
|
|
128
|
-
from pooldose.
|
|
128
|
+
from pooldose.client import RequestStatus
|
|
129
129
|
|
|
130
130
|
HOST = "192.168.1.100" # Change this to your device's host or IP address
|
|
131
131
|
TIMEOUT = 30
|
|
@@ -211,6 +211,8 @@ if __name__ == "__main__":
|
|
|
211
211
|
|
|
212
212
|
#### Connection Management
|
|
213
213
|
```python
|
|
214
|
+
from pooldose.client import PooldoseClient, RequestStatus
|
|
215
|
+
|
|
214
216
|
# Recommended: Separate initialization and connection
|
|
215
217
|
client = PooldoseClient("192.168.1.100", timeout=30)
|
|
216
218
|
status = await client.connect()
|
|
@@ -224,7 +226,7 @@ else:
|
|
|
224
226
|
|
|
225
227
|
#### Error Handling
|
|
226
228
|
```python
|
|
227
|
-
from pooldose.
|
|
229
|
+
from pooldose.client import PooldoseClient, RequestStatus
|
|
228
230
|
|
|
229
231
|
client = PooldoseClient("192.168.1.100")
|
|
230
232
|
status = await client.connect()
|
|
@@ -241,9 +243,84 @@ else:
|
|
|
241
243
|
print(f"Other error: {status}")
|
|
242
244
|
```
|
|
243
245
|
|
|
246
|
+
#### Type-specific Access
|
|
247
|
+
```python
|
|
248
|
+
# Get all values by type
|
|
249
|
+
sensors = instant_values.get_sensors() # All sensor readings
|
|
250
|
+
binary_sensors = instant_values.get_binary_sensors() # All boolean states
|
|
251
|
+
numbers = instant_values.get_numbers() # All configurable numbers
|
|
252
|
+
switches = instant_values.get_switches() # All switch states
|
|
253
|
+
selects = instant_values.get_selects() # All select options
|
|
254
|
+
|
|
255
|
+
# Check available types dynamically
|
|
256
|
+
available_types = instant_values.available_types()
|
|
257
|
+
print("Available types:", list(available_types.keys()))
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### Working with Mappings
|
|
261
|
+
```
|
|
262
|
+
Mapping Discovery Process:
|
|
263
|
+
┌─────────────────┐
|
|
264
|
+
│ Device Connect │
|
|
265
|
+
└─────────────────┘
|
|
266
|
+
│
|
|
267
|
+
▼
|
|
268
|
+
┌─────────────────┐
|
|
269
|
+
│ Get MODEL_ID │ ──────► PDPR1H1HAW100
|
|
270
|
+
│ Get FW_CODE │ ──────► 539187
|
|
271
|
+
└─────────────────┘
|
|
272
|
+
│
|
|
273
|
+
▼
|
|
274
|
+
┌─────────────────┐
|
|
275
|
+
│ Load JSON File │ ──────► model_PDPR1H1HAW100_FW539187.json
|
|
276
|
+
└─────────────────┘
|
|
277
|
+
│
|
|
278
|
+
▼
|
|
279
|
+
┌─────────────────┐
|
|
280
|
+
│ Type Discovery │
|
|
281
|
+
│ ┌─────────────┐ │
|
|
282
|
+
│ │ Sensors │ │ ──────► temperature, ph, orp, ...
|
|
283
|
+
│ │ Switches │ │ ──────► stop_dosing, pump_detection, ...
|
|
284
|
+
│ │ Numbers │ │ ──────► ph_target, orp_target, ...
|
|
285
|
+
│ │ Selects │ │ ──────► water_meter_unit, ...
|
|
286
|
+
│ └─────────────┘ │
|
|
287
|
+
└─────────────────┘
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
# Query what's available for your specific device
|
|
292
|
+
print("\nAvailable sensors:")
|
|
293
|
+
for name, sensor in client.available_sensors().items():
|
|
294
|
+
print(f" {name}: key={sensor.key}")
|
|
295
|
+
if sensor.conversion:
|
|
296
|
+
print(f" conversion: {sensor.conversion}")
|
|
297
|
+
|
|
298
|
+
print("\nAvailable numbers (settable):")
|
|
299
|
+
for name, number in client.available_numbers().items():
|
|
300
|
+
print(f" {name}: key={number.key}")
|
|
301
|
+
|
|
302
|
+
print("\nAvailable switches:")
|
|
303
|
+
for name, switch in client.available_switches().items():
|
|
304
|
+
print(f" {name}: key={switch.key}")
|
|
305
|
+
```
|
|
306
|
+
|
|
244
307
|
## API Reference
|
|
245
308
|
|
|
246
|
-
### PooldoseClient
|
|
309
|
+
### PooldoseClient Class Hierarchy
|
|
310
|
+
```
|
|
311
|
+
PooldoseClient
|
|
312
|
+
├── Device Info
|
|
313
|
+
│ ├── static_values() ──────► StaticValues
|
|
314
|
+
│ └── device_info{} ─────────► dict
|
|
315
|
+
├── Type Discovery
|
|
316
|
+
│ ├── available_types() ────► dict[str, list[str]]
|
|
317
|
+
│ ├── available_sensors() ──► dict[str, SensorMapping]
|
|
318
|
+
│ ├── available_numbers() ──► dict[str, NumberMapping]
|
|
319
|
+
│ ├── available_switches() ─► dict[str, SwitchMapping]
|
|
320
|
+
│ └── available_selects() ──► dict[str, SelectMapping]
|
|
321
|
+
└── Live Data
|
|
322
|
+
└── instant_values() ─────► InstantValues
|
|
323
|
+
```
|
|
247
324
|
|
|
248
325
|
#### Constructor
|
|
249
326
|
```python
|
|
@@ -272,7 +349,41 @@ PooldoseClient(host, timeout=10, include_sensitive_data=False)
|
|
|
272
349
|
- `host` - Device hostname or IP address
|
|
273
350
|
- `timeout` - Request timeout in seconds
|
|
274
351
|
|
|
275
|
-
###
|
|
352
|
+
### RequestStatus
|
|
353
|
+
|
|
354
|
+
All client methods return `RequestStatus` enum values:
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
from pooldose.client import RequestStatus
|
|
358
|
+
|
|
359
|
+
RequestStatus.SUCCESS # Operation successful
|
|
360
|
+
RequestStatus.CONNECTION_ERROR # Network connection failed
|
|
361
|
+
RequestStatus.HOST_UNREACHABLE # Device not reachable
|
|
362
|
+
RequestStatus.PARAMS_FETCH_FAILED # Failed to fetch device parameters
|
|
363
|
+
RequestStatus.API_VERSION_UNSUPPORTED # API version not supported
|
|
364
|
+
RequestStatus.NO_DATA # No data received
|
|
365
|
+
RequestStatus.UNKNOWN_ERROR # Other error occurred
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### InstantValues Interface
|
|
369
|
+
```
|
|
370
|
+
InstantValues
|
|
371
|
+
├── Dictionary Interface
|
|
372
|
+
│ ├── [key] ─────────────────► __getitem__
|
|
373
|
+
│ ├── get(key, default) ────► get method
|
|
374
|
+
│ ├── key in values ────────► __contains__
|
|
375
|
+
│ └── [key] = value ────────► __setitem__ (async)
|
|
376
|
+
├── Type Getters
|
|
377
|
+
│ ├── get_sensors() ────────► dict[str, tuple]
|
|
378
|
+
│ ├── get_binary_sensors() ─► dict[str, bool]
|
|
379
|
+
│ ├── get_numbers() ────────► dict[str, tuple]
|
|
380
|
+
│ ├── get_switches() ───────► dict[str, bool]
|
|
381
|
+
│ └── get_selects() ────────► dict[str, int]
|
|
382
|
+
└── Type Setters (async)
|
|
383
|
+
├── set_number(key, value) ──► bool
|
|
384
|
+
├── set_switch(key, value) ──► bool
|
|
385
|
+
└── set_select(key, value) ──► bool
|
|
386
|
+
```
|
|
276
387
|
|
|
277
388
|
#### Dictionary Interface
|
|
278
389
|
```python
|
|
@@ -321,8 +432,31 @@ client = PooldoseClient(
|
|
|
321
432
|
status = await client.connect()
|
|
322
433
|
```
|
|
323
434
|
|
|
435
|
+
### Security Model
|
|
436
|
+
```
|
|
437
|
+
Data Classification:
|
|
438
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
439
|
+
│ Public Data │ │ Sensitive Data │ │ Never Exposed │
|
|
440
|
+
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
|
|
441
|
+
│ • Device Name │ │ • WiFi Password │ │ • Admin Creds │
|
|
442
|
+
│ • Model ID │ │ • AP Password │ │ • Internal Keys │
|
|
443
|
+
│ • Serial Number │ │ │ │ │
|
|
444
|
+
│ • Sensor Values │ │ │ │ │
|
|
445
|
+
│ • IP Address │ │ │ │ │
|
|
446
|
+
│ • MAC Address │ │ │ │ │
|
|
447
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
448
|
+
│ │ │
|
|
449
|
+
▼ ▼ ▼
|
|
450
|
+
Always Included include_sensitive_data=True Never Included
|
|
451
|
+
```
|
|
452
|
+
|
|
324
453
|
## Changelog
|
|
325
454
|
|
|
455
|
+
### [0.4.1] - 2025-07-17
|
|
456
|
+
- **BREAKING**: Moved all RequestStatus into client module - import from `pooldose.client` instead of `pooldose.request_handler`
|
|
457
|
+
- Moved all connect checks into client (incl. API Version check) to avoid public access to requesthandler
|
|
458
|
+
- Clean up code and improved encapsulation
|
|
459
|
+
|
|
326
460
|
### [0.4.0] - 2025-07-11
|
|
327
461
|
- **BREAKING**: Removed `create()` factory method
|
|
328
462
|
- **BREAKING**: Changed client initialization pattern to separate `__init__` and async `connect()` methods
|
|
@@ -107,7 +107,7 @@ pip install python-pooldose
|
|
|
107
107
|
import asyncio
|
|
108
108
|
import json
|
|
109
109
|
from pooldose.client import PooldoseClient
|
|
110
|
-
from pooldose.
|
|
110
|
+
from pooldose.client import RequestStatus
|
|
111
111
|
|
|
112
112
|
HOST = "192.168.1.100" # Change this to your device's host or IP address
|
|
113
113
|
TIMEOUT = 30
|
|
@@ -193,6 +193,8 @@ if __name__ == "__main__":
|
|
|
193
193
|
|
|
194
194
|
#### Connection Management
|
|
195
195
|
```python
|
|
196
|
+
from pooldose.client import PooldoseClient, RequestStatus
|
|
197
|
+
|
|
196
198
|
# Recommended: Separate initialization and connection
|
|
197
199
|
client = PooldoseClient("192.168.1.100", timeout=30)
|
|
198
200
|
status = await client.connect()
|
|
@@ -206,7 +208,7 @@ else:
|
|
|
206
208
|
|
|
207
209
|
#### Error Handling
|
|
208
210
|
```python
|
|
209
|
-
from pooldose.
|
|
211
|
+
from pooldose.client import PooldoseClient, RequestStatus
|
|
210
212
|
|
|
211
213
|
client = PooldoseClient("192.168.1.100")
|
|
212
214
|
status = await client.connect()
|
|
@@ -223,9 +225,84 @@ else:
|
|
|
223
225
|
print(f"Other error: {status}")
|
|
224
226
|
```
|
|
225
227
|
|
|
228
|
+
#### Type-specific Access
|
|
229
|
+
```python
|
|
230
|
+
# Get all values by type
|
|
231
|
+
sensors = instant_values.get_sensors() # All sensor readings
|
|
232
|
+
binary_sensors = instant_values.get_binary_sensors() # All boolean states
|
|
233
|
+
numbers = instant_values.get_numbers() # All configurable numbers
|
|
234
|
+
switches = instant_values.get_switches() # All switch states
|
|
235
|
+
selects = instant_values.get_selects() # All select options
|
|
236
|
+
|
|
237
|
+
# Check available types dynamically
|
|
238
|
+
available_types = instant_values.available_types()
|
|
239
|
+
print("Available types:", list(available_types.keys()))
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
#### Working with Mappings
|
|
243
|
+
```
|
|
244
|
+
Mapping Discovery Process:
|
|
245
|
+
┌─────────────────┐
|
|
246
|
+
│ Device Connect │
|
|
247
|
+
└─────────────────┘
|
|
248
|
+
│
|
|
249
|
+
▼
|
|
250
|
+
┌─────────────────┐
|
|
251
|
+
│ Get MODEL_ID │ ──────► PDPR1H1HAW100
|
|
252
|
+
│ Get FW_CODE │ ──────► 539187
|
|
253
|
+
└─────────────────┘
|
|
254
|
+
│
|
|
255
|
+
▼
|
|
256
|
+
┌─────────────────┐
|
|
257
|
+
│ Load JSON File │ ──────► model_PDPR1H1HAW100_FW539187.json
|
|
258
|
+
└─────────────────┘
|
|
259
|
+
│
|
|
260
|
+
▼
|
|
261
|
+
┌─────────────────┐
|
|
262
|
+
│ Type Discovery │
|
|
263
|
+
│ ┌─────────────┐ │
|
|
264
|
+
│ │ Sensors │ │ ──────► temperature, ph, orp, ...
|
|
265
|
+
│ │ Switches │ │ ──────► stop_dosing, pump_detection, ...
|
|
266
|
+
│ │ Numbers │ │ ──────► ph_target, orp_target, ...
|
|
267
|
+
│ │ Selects │ │ ──────► water_meter_unit, ...
|
|
268
|
+
│ └─────────────┘ │
|
|
269
|
+
└─────────────────┘
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
# Query what's available for your specific device
|
|
274
|
+
print("\nAvailable sensors:")
|
|
275
|
+
for name, sensor in client.available_sensors().items():
|
|
276
|
+
print(f" {name}: key={sensor.key}")
|
|
277
|
+
if sensor.conversion:
|
|
278
|
+
print(f" conversion: {sensor.conversion}")
|
|
279
|
+
|
|
280
|
+
print("\nAvailable numbers (settable):")
|
|
281
|
+
for name, number in client.available_numbers().items():
|
|
282
|
+
print(f" {name}: key={number.key}")
|
|
283
|
+
|
|
284
|
+
print("\nAvailable switches:")
|
|
285
|
+
for name, switch in client.available_switches().items():
|
|
286
|
+
print(f" {name}: key={switch.key}")
|
|
287
|
+
```
|
|
288
|
+
|
|
226
289
|
## API Reference
|
|
227
290
|
|
|
228
|
-
### PooldoseClient
|
|
291
|
+
### PooldoseClient Class Hierarchy
|
|
292
|
+
```
|
|
293
|
+
PooldoseClient
|
|
294
|
+
├── Device Info
|
|
295
|
+
│ ├── static_values() ──────► StaticValues
|
|
296
|
+
│ └── device_info{} ─────────► dict
|
|
297
|
+
├── Type Discovery
|
|
298
|
+
│ ├── available_types() ────► dict[str, list[str]]
|
|
299
|
+
│ ├── available_sensors() ──► dict[str, SensorMapping]
|
|
300
|
+
│ ├── available_numbers() ──► dict[str, NumberMapping]
|
|
301
|
+
│ ├── available_switches() ─► dict[str, SwitchMapping]
|
|
302
|
+
│ └── available_selects() ──► dict[str, SelectMapping]
|
|
303
|
+
└── Live Data
|
|
304
|
+
└── instant_values() ─────► InstantValues
|
|
305
|
+
```
|
|
229
306
|
|
|
230
307
|
#### Constructor
|
|
231
308
|
```python
|
|
@@ -254,7 +331,41 @@ PooldoseClient(host, timeout=10, include_sensitive_data=False)
|
|
|
254
331
|
- `host` - Device hostname or IP address
|
|
255
332
|
- `timeout` - Request timeout in seconds
|
|
256
333
|
|
|
257
|
-
###
|
|
334
|
+
### RequestStatus
|
|
335
|
+
|
|
336
|
+
All client methods return `RequestStatus` enum values:
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
from pooldose.client import RequestStatus
|
|
340
|
+
|
|
341
|
+
RequestStatus.SUCCESS # Operation successful
|
|
342
|
+
RequestStatus.CONNECTION_ERROR # Network connection failed
|
|
343
|
+
RequestStatus.HOST_UNREACHABLE # Device not reachable
|
|
344
|
+
RequestStatus.PARAMS_FETCH_FAILED # Failed to fetch device parameters
|
|
345
|
+
RequestStatus.API_VERSION_UNSUPPORTED # API version not supported
|
|
346
|
+
RequestStatus.NO_DATA # No data received
|
|
347
|
+
RequestStatus.UNKNOWN_ERROR # Other error occurred
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### InstantValues Interface
|
|
351
|
+
```
|
|
352
|
+
InstantValues
|
|
353
|
+
├── Dictionary Interface
|
|
354
|
+
│ ├── [key] ─────────────────► __getitem__
|
|
355
|
+
│ ├── get(key, default) ────► get method
|
|
356
|
+
│ ├── key in values ────────► __contains__
|
|
357
|
+
│ └── [key] = value ────────► __setitem__ (async)
|
|
358
|
+
├── Type Getters
|
|
359
|
+
│ ├── get_sensors() ────────► dict[str, tuple]
|
|
360
|
+
│ ├── get_binary_sensors() ─► dict[str, bool]
|
|
361
|
+
│ ├── get_numbers() ────────► dict[str, tuple]
|
|
362
|
+
│ ├── get_switches() ───────► dict[str, bool]
|
|
363
|
+
│ └── get_selects() ────────► dict[str, int]
|
|
364
|
+
└── Type Setters (async)
|
|
365
|
+
├── set_number(key, value) ──► bool
|
|
366
|
+
├── set_switch(key, value) ──► bool
|
|
367
|
+
└── set_select(key, value) ──► bool
|
|
368
|
+
```
|
|
258
369
|
|
|
259
370
|
#### Dictionary Interface
|
|
260
371
|
```python
|
|
@@ -303,8 +414,31 @@ client = PooldoseClient(
|
|
|
303
414
|
status = await client.connect()
|
|
304
415
|
```
|
|
305
416
|
|
|
417
|
+
### Security Model
|
|
418
|
+
```
|
|
419
|
+
Data Classification:
|
|
420
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
421
|
+
│ Public Data │ │ Sensitive Data │ │ Never Exposed │
|
|
422
|
+
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
|
|
423
|
+
│ • Device Name │ │ • WiFi Password │ │ • Admin Creds │
|
|
424
|
+
│ • Model ID │ │ • AP Password │ │ • Internal Keys │
|
|
425
|
+
│ • Serial Number │ │ │ │ │
|
|
426
|
+
│ • Sensor Values │ │ │ │ │
|
|
427
|
+
│ • IP Address │ │ │ │ │
|
|
428
|
+
│ • MAC Address │ │ │ │ │
|
|
429
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
430
|
+
│ │ │
|
|
431
|
+
▼ ▼ ▼
|
|
432
|
+
Always Included include_sensitive_data=True Never Included
|
|
433
|
+
```
|
|
434
|
+
|
|
306
435
|
## Changelog
|
|
307
436
|
|
|
437
|
+
### [0.4.1] - 2025-07-17
|
|
438
|
+
- **BREAKING**: Moved all RequestStatus into client module - import from `pooldose.client` instead of `pooldose.request_handler`
|
|
439
|
+
- Moved all connect checks into client (incl. API Version check) to avoid public access to requesthandler
|
|
440
|
+
- Clean up code and improved encapsulation
|
|
441
|
+
|
|
308
442
|
### [0.4.0] - 2025-07-11
|
|
309
443
|
- **BREAKING**: Removed `create()` factory method
|
|
310
444
|
- **BREAKING**: Changed client initialization pattern to separate `__init__` and async `connect()` methods
|
|
@@ -21,6 +21,8 @@ from pooldose.mappings.mapping_info import (
|
|
|
21
21
|
|
|
22
22
|
_LOGGER = logging.getLogger(__name__)
|
|
23
23
|
|
|
24
|
+
API_VERSION_SUPPORTED = "v1/"
|
|
25
|
+
|
|
24
26
|
class PooldoseClient:
|
|
25
27
|
"""
|
|
26
28
|
Async client for SEKO Pooldose API.
|
|
@@ -92,6 +94,32 @@ class PooldoseClient:
|
|
|
92
94
|
_LOGGER.debug("Initialized Pooldose client with device info: %s", self.device_info)
|
|
93
95
|
return RequestStatus.SUCCESS
|
|
94
96
|
|
|
97
|
+
def check_apiversion_supported(self) -> tuple[RequestStatus, dict]:
|
|
98
|
+
"""
|
|
99
|
+
Check if the loaded API version matches the supported version.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
tuple: (RequestStatus, dict)
|
|
103
|
+
- dict contains:
|
|
104
|
+
"api_version_is": the current API version (or None if not set)
|
|
105
|
+
"api_version_should": the supported API version
|
|
106
|
+
- RequestStatus.SUCCESS if supported,
|
|
107
|
+
- RequestStatus.API_VERSION_UNSUPPORTED if not supported,
|
|
108
|
+
- RequestStatus.NO_DATA if not set.
|
|
109
|
+
"""
|
|
110
|
+
result = {
|
|
111
|
+
"api_version_is": self._request_handler.api_version,
|
|
112
|
+
"api_version_should": API_VERSION_SUPPORTED,
|
|
113
|
+
}
|
|
114
|
+
if not self._request_handler.api_version:
|
|
115
|
+
_LOGGER.warning("API version not set, cannot check support")
|
|
116
|
+
return RequestStatus.NO_DATA, result
|
|
117
|
+
if self._request_handler.api_version != API_VERSION_SUPPORTED:
|
|
118
|
+
_LOGGER.warning("Unsupported API version: %s, expected %s", self._request_handler.api_version, API_VERSION_SUPPORTED)
|
|
119
|
+
return RequestStatus.API_VERSION_UNSUPPORTED, result
|
|
120
|
+
|
|
121
|
+
return RequestStatus.SUCCESS, result
|
|
122
|
+
|
|
95
123
|
async def _load_device_info(self) -> RequestStatus:
|
|
96
124
|
"""
|
|
97
125
|
Load device information from the request handler.
|
|
@@ -5,39 +5,15 @@ import json
|
|
|
5
5
|
import re
|
|
6
6
|
import socket
|
|
7
7
|
from typing import Any
|
|
8
|
-
from enum import Enum
|
|
9
8
|
import asyncio
|
|
10
9
|
import aiohttp
|
|
11
10
|
|
|
11
|
+
from pooldose.request_status import RequestStatus
|
|
12
|
+
|
|
12
13
|
# pylint: disable=line-too-long,no-else-return
|
|
13
14
|
|
|
14
15
|
_LOGGER = logging.getLogger(__name__)
|
|
15
16
|
|
|
16
|
-
API_VERSION_SUPPORTED = "v1/"
|
|
17
|
-
|
|
18
|
-
class RequestStatus(Enum):
|
|
19
|
-
"""
|
|
20
|
-
Enum for standardized return codes of API and client methods.
|
|
21
|
-
|
|
22
|
-
Each status represents a specific result or error case:
|
|
23
|
-
- SUCCESS: Operation was successful.
|
|
24
|
-
- HOST_UNREACHABLE: The host could not be reached (e.g. network error).
|
|
25
|
-
- PARAMS_FETCH_FAILED: params.js could not be fetched or parsed.
|
|
26
|
-
- API_VERSION_UNSUPPORTED: The API version is not supported.
|
|
27
|
-
- NO_DATA: No data was returned or found.
|
|
28
|
-
- LAST_DATA: No new data was found, last valid data was returned.
|
|
29
|
-
- CLIENT_ERROR_SET: Error while setting a value on the client/device.
|
|
30
|
-
- UNKNOWN_ERROR: An unspecified or unexpected error occurred.
|
|
31
|
-
"""
|
|
32
|
-
SUCCESS = "success"
|
|
33
|
-
HOST_UNREACHABLE = "host_unreachable"
|
|
34
|
-
PARAMS_FETCH_FAILED = "params_fetch_failed"
|
|
35
|
-
API_VERSION_UNSUPPORTED = "api_version_unsupported"
|
|
36
|
-
NO_DATA = "no_data"
|
|
37
|
-
LAST_DATA = "last_data"
|
|
38
|
-
CLIENT_ERROR_SET = "client_error_set"
|
|
39
|
-
UNKNOWN_ERROR = "unknown_error"
|
|
40
|
-
|
|
41
17
|
class RequestHandler:
|
|
42
18
|
"""
|
|
43
19
|
Handles all HTTP requests to the Pooldose API.
|
|
@@ -126,32 +102,6 @@ class RequestHandler:
|
|
|
126
102
|
_LOGGER.warning("Error fetching core params: %s", err)
|
|
127
103
|
return None
|
|
128
104
|
|
|
129
|
-
def check_apiversion_supported(self) -> tuple[RequestStatus, dict]:
|
|
130
|
-
"""
|
|
131
|
-
Check if the loaded API version matches the supported version.
|
|
132
|
-
|
|
133
|
-
Returns:
|
|
134
|
-
tuple: (RequestStatus, dict)
|
|
135
|
-
- dict contains:
|
|
136
|
-
"api_version_is": the current API version (or None if not set)
|
|
137
|
-
"api_version_should": the supported API version
|
|
138
|
-
- RequestStatus.SUCCESS if supported,
|
|
139
|
-
- RequestStatus.API_VERSION_UNSUPPORTED if not supported,
|
|
140
|
-
- RequestStatus.NO_DATA if not set.
|
|
141
|
-
"""
|
|
142
|
-
result = {
|
|
143
|
-
"api_version_is": self.api_version,
|
|
144
|
-
"api_version_should": API_VERSION_SUPPORTED,
|
|
145
|
-
}
|
|
146
|
-
if not self.api_version:
|
|
147
|
-
_LOGGER.warning("API version not set, cannot check support")
|
|
148
|
-
return RequestStatus.NO_DATA, result
|
|
149
|
-
if self.api_version != API_VERSION_SUPPORTED:
|
|
150
|
-
_LOGGER.warning("Unsupported API version: %s, expected %s", self.api_version, API_VERSION_SUPPORTED)
|
|
151
|
-
return RequestStatus.API_VERSION_UNSUPPORTED, result
|
|
152
|
-
else:
|
|
153
|
-
return RequestStatus.SUCCESS, result
|
|
154
|
-
|
|
155
105
|
async def get_debug_config(self):
|
|
156
106
|
"""
|
|
157
107
|
Asynchronously fetches the debug configuration from the server.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Request Status for async API client for SEKO Pooldose."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
class RequestStatus(Enum):
|
|
6
|
+
"""
|
|
7
|
+
Enum for standardized return codes of API and client methods.
|
|
8
|
+
|
|
9
|
+
Each status represents a specific result or error case:
|
|
10
|
+
- SUCCESS: Operation was successful.
|
|
11
|
+
- HOST_UNREACHABLE: The host could not be reached (e.g. network error).
|
|
12
|
+
- PARAMS_FETCH_FAILED: params.js could not be fetched or parsed.
|
|
13
|
+
- API_VERSION_UNSUPPORTED: The API version is not supported.
|
|
14
|
+
- NO_DATA: No data was returned or found.
|
|
15
|
+
- LAST_DATA: No new data was found, last valid data was returned.
|
|
16
|
+
- CLIENT_ERROR_SET: Error while setting a value on the client/device.
|
|
17
|
+
- UNKNOWN_ERROR: An unspecified or unexpected error occurred.
|
|
18
|
+
"""
|
|
19
|
+
SUCCESS = "success"
|
|
20
|
+
HOST_UNREACHABLE = "host_unreachable"
|
|
21
|
+
PARAMS_FETCH_FAILED = "params_fetch_failed"
|
|
22
|
+
API_VERSION_UNSUPPORTED = "api_version_unsupported"
|
|
23
|
+
NO_DATA = "no_data"
|
|
24
|
+
LAST_DATA = "last_data"
|
|
25
|
+
CLIENT_ERROR_SET = "client_error_set"
|
|
26
|
+
UNKNOWN_ERROR = "unknown_error"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-pooldose
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -125,7 +125,7 @@ pip install python-pooldose
|
|
|
125
125
|
import asyncio
|
|
126
126
|
import json
|
|
127
127
|
from pooldose.client import PooldoseClient
|
|
128
|
-
from pooldose.
|
|
128
|
+
from pooldose.client import RequestStatus
|
|
129
129
|
|
|
130
130
|
HOST = "192.168.1.100" # Change this to your device's host or IP address
|
|
131
131
|
TIMEOUT = 30
|
|
@@ -211,6 +211,8 @@ if __name__ == "__main__":
|
|
|
211
211
|
|
|
212
212
|
#### Connection Management
|
|
213
213
|
```python
|
|
214
|
+
from pooldose.client import PooldoseClient, RequestStatus
|
|
215
|
+
|
|
214
216
|
# Recommended: Separate initialization and connection
|
|
215
217
|
client = PooldoseClient("192.168.1.100", timeout=30)
|
|
216
218
|
status = await client.connect()
|
|
@@ -224,7 +226,7 @@ else:
|
|
|
224
226
|
|
|
225
227
|
#### Error Handling
|
|
226
228
|
```python
|
|
227
|
-
from pooldose.
|
|
229
|
+
from pooldose.client import PooldoseClient, RequestStatus
|
|
228
230
|
|
|
229
231
|
client = PooldoseClient("192.168.1.100")
|
|
230
232
|
status = await client.connect()
|
|
@@ -241,9 +243,84 @@ else:
|
|
|
241
243
|
print(f"Other error: {status}")
|
|
242
244
|
```
|
|
243
245
|
|
|
246
|
+
#### Type-specific Access
|
|
247
|
+
```python
|
|
248
|
+
# Get all values by type
|
|
249
|
+
sensors = instant_values.get_sensors() # All sensor readings
|
|
250
|
+
binary_sensors = instant_values.get_binary_sensors() # All boolean states
|
|
251
|
+
numbers = instant_values.get_numbers() # All configurable numbers
|
|
252
|
+
switches = instant_values.get_switches() # All switch states
|
|
253
|
+
selects = instant_values.get_selects() # All select options
|
|
254
|
+
|
|
255
|
+
# Check available types dynamically
|
|
256
|
+
available_types = instant_values.available_types()
|
|
257
|
+
print("Available types:", list(available_types.keys()))
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### Working with Mappings
|
|
261
|
+
```
|
|
262
|
+
Mapping Discovery Process:
|
|
263
|
+
┌─────────────────┐
|
|
264
|
+
│ Device Connect │
|
|
265
|
+
└─────────────────┘
|
|
266
|
+
│
|
|
267
|
+
▼
|
|
268
|
+
┌─────────────────┐
|
|
269
|
+
│ Get MODEL_ID │ ──────► PDPR1H1HAW100
|
|
270
|
+
│ Get FW_CODE │ ──────► 539187
|
|
271
|
+
└─────────────────┘
|
|
272
|
+
│
|
|
273
|
+
▼
|
|
274
|
+
┌─────────────────┐
|
|
275
|
+
│ Load JSON File │ ──────► model_PDPR1H1HAW100_FW539187.json
|
|
276
|
+
└─────────────────┘
|
|
277
|
+
│
|
|
278
|
+
▼
|
|
279
|
+
┌─────────────────┐
|
|
280
|
+
│ Type Discovery │
|
|
281
|
+
│ ┌─────────────┐ │
|
|
282
|
+
│ │ Sensors │ │ ──────► temperature, ph, orp, ...
|
|
283
|
+
│ │ Switches │ │ ──────► stop_dosing, pump_detection, ...
|
|
284
|
+
│ │ Numbers │ │ ──────► ph_target, orp_target, ...
|
|
285
|
+
│ │ Selects │ │ ──────► water_meter_unit, ...
|
|
286
|
+
│ └─────────────┘ │
|
|
287
|
+
└─────────────────┘
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
# Query what's available for your specific device
|
|
292
|
+
print("\nAvailable sensors:")
|
|
293
|
+
for name, sensor in client.available_sensors().items():
|
|
294
|
+
print(f" {name}: key={sensor.key}")
|
|
295
|
+
if sensor.conversion:
|
|
296
|
+
print(f" conversion: {sensor.conversion}")
|
|
297
|
+
|
|
298
|
+
print("\nAvailable numbers (settable):")
|
|
299
|
+
for name, number in client.available_numbers().items():
|
|
300
|
+
print(f" {name}: key={number.key}")
|
|
301
|
+
|
|
302
|
+
print("\nAvailable switches:")
|
|
303
|
+
for name, switch in client.available_switches().items():
|
|
304
|
+
print(f" {name}: key={switch.key}")
|
|
305
|
+
```
|
|
306
|
+
|
|
244
307
|
## API Reference
|
|
245
308
|
|
|
246
|
-
### PooldoseClient
|
|
309
|
+
### PooldoseClient Class Hierarchy
|
|
310
|
+
```
|
|
311
|
+
PooldoseClient
|
|
312
|
+
├── Device Info
|
|
313
|
+
│ ├── static_values() ──────► StaticValues
|
|
314
|
+
│ └── device_info{} ─────────► dict
|
|
315
|
+
├── Type Discovery
|
|
316
|
+
│ ├── available_types() ────► dict[str, list[str]]
|
|
317
|
+
│ ├── available_sensors() ──► dict[str, SensorMapping]
|
|
318
|
+
│ ├── available_numbers() ──► dict[str, NumberMapping]
|
|
319
|
+
│ ├── available_switches() ─► dict[str, SwitchMapping]
|
|
320
|
+
│ └── available_selects() ──► dict[str, SelectMapping]
|
|
321
|
+
└── Live Data
|
|
322
|
+
└── instant_values() ─────► InstantValues
|
|
323
|
+
```
|
|
247
324
|
|
|
248
325
|
#### Constructor
|
|
249
326
|
```python
|
|
@@ -272,7 +349,41 @@ PooldoseClient(host, timeout=10, include_sensitive_data=False)
|
|
|
272
349
|
- `host` - Device hostname or IP address
|
|
273
350
|
- `timeout` - Request timeout in seconds
|
|
274
351
|
|
|
275
|
-
###
|
|
352
|
+
### RequestStatus
|
|
353
|
+
|
|
354
|
+
All client methods return `RequestStatus` enum values:
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
from pooldose.client import RequestStatus
|
|
358
|
+
|
|
359
|
+
RequestStatus.SUCCESS # Operation successful
|
|
360
|
+
RequestStatus.CONNECTION_ERROR # Network connection failed
|
|
361
|
+
RequestStatus.HOST_UNREACHABLE # Device not reachable
|
|
362
|
+
RequestStatus.PARAMS_FETCH_FAILED # Failed to fetch device parameters
|
|
363
|
+
RequestStatus.API_VERSION_UNSUPPORTED # API version not supported
|
|
364
|
+
RequestStatus.NO_DATA # No data received
|
|
365
|
+
RequestStatus.UNKNOWN_ERROR # Other error occurred
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### InstantValues Interface
|
|
369
|
+
```
|
|
370
|
+
InstantValues
|
|
371
|
+
├── Dictionary Interface
|
|
372
|
+
│ ├── [key] ─────────────────► __getitem__
|
|
373
|
+
│ ├── get(key, default) ────► get method
|
|
374
|
+
│ ├── key in values ────────► __contains__
|
|
375
|
+
│ └── [key] = value ────────► __setitem__ (async)
|
|
376
|
+
├── Type Getters
|
|
377
|
+
│ ├── get_sensors() ────────► dict[str, tuple]
|
|
378
|
+
│ ├── get_binary_sensors() ─► dict[str, bool]
|
|
379
|
+
│ ├── get_numbers() ────────► dict[str, tuple]
|
|
380
|
+
│ ├── get_switches() ───────► dict[str, bool]
|
|
381
|
+
│ └── get_selects() ────────► dict[str, int]
|
|
382
|
+
└── Type Setters (async)
|
|
383
|
+
├── set_number(key, value) ──► bool
|
|
384
|
+
├── set_switch(key, value) ──► bool
|
|
385
|
+
└── set_select(key, value) ──► bool
|
|
386
|
+
```
|
|
276
387
|
|
|
277
388
|
#### Dictionary Interface
|
|
278
389
|
```python
|
|
@@ -321,8 +432,31 @@ client = PooldoseClient(
|
|
|
321
432
|
status = await client.connect()
|
|
322
433
|
```
|
|
323
434
|
|
|
435
|
+
### Security Model
|
|
436
|
+
```
|
|
437
|
+
Data Classification:
|
|
438
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
439
|
+
│ Public Data │ │ Sensitive Data │ │ Never Exposed │
|
|
440
|
+
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
|
|
441
|
+
│ • Device Name │ │ • WiFi Password │ │ • Admin Creds │
|
|
442
|
+
│ • Model ID │ │ • AP Password │ │ • Internal Keys │
|
|
443
|
+
│ • Serial Number │ │ │ │ │
|
|
444
|
+
│ • Sensor Values │ │ │ │ │
|
|
445
|
+
│ • IP Address │ │ │ │ │
|
|
446
|
+
│ • MAC Address │ │ │ │ │
|
|
447
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
448
|
+
│ │ │
|
|
449
|
+
▼ ▼ ▼
|
|
450
|
+
Always Included include_sensitive_data=True Never Included
|
|
451
|
+
```
|
|
452
|
+
|
|
324
453
|
## Changelog
|
|
325
454
|
|
|
455
|
+
### [0.4.1] - 2025-07-17
|
|
456
|
+
- **BREAKING**: Moved all RequestStatus into client module - import from `pooldose.client` instead of `pooldose.request_handler`
|
|
457
|
+
- Moved all connect checks into client (incl. API Version check) to avoid public access to requesthandler
|
|
458
|
+
- Clean up code and improved encapsulation
|
|
459
|
+
|
|
326
460
|
### [0.4.0] - 2025-07-11
|
|
327
461
|
- **BREAKING**: Removed `create()` factory method
|
|
328
462
|
- **BREAKING**: Changed client initialization pattern to separate `__init__` and async `connect()` methods
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"""Tests for Client for Async API client for SEKO Pooldose."""
|
|
2
2
|
|
|
3
|
+
from unittest.mock import Mock
|
|
4
|
+
|
|
3
5
|
import pytest
|
|
4
|
-
|
|
5
|
-
from pooldose.
|
|
6
|
+
|
|
7
|
+
from pooldose.client import PooldoseClient, RequestStatus
|
|
8
|
+
from pooldose.request_handler import RequestHandler
|
|
6
9
|
from pooldose.mappings.mapping_info import MappingInfo
|
|
7
10
|
|
|
8
11
|
@pytest.mark.asyncio
|
|
@@ -39,3 +42,25 @@ async def test_get_model_mapping_file_not_found():
|
|
|
39
42
|
mapping_info = await MappingInfo.load("DOESNOTEXIST", "000000")
|
|
40
43
|
assert mapping_info.status != RequestStatus.SUCCESS
|
|
41
44
|
assert mapping_info.mapping is None
|
|
45
|
+
|
|
46
|
+
@pytest.mark.asyncio
|
|
47
|
+
async def test_check_apiversion_supported():
|
|
48
|
+
"""Test API version check logic."""
|
|
49
|
+
client = PooldoseClient("localhost")
|
|
50
|
+
|
|
51
|
+
# Mock the request handler instead of trying to connect
|
|
52
|
+
mock_handler = Mock(spec=RequestHandler)
|
|
53
|
+
# pylint: disable=protected-access
|
|
54
|
+
client._request_handler = mock_handler
|
|
55
|
+
|
|
56
|
+
# Test supported API version
|
|
57
|
+
mock_handler.api_version = "v1/"
|
|
58
|
+
assert client.check_apiversion_supported()[0] == RequestStatus.SUCCESS
|
|
59
|
+
|
|
60
|
+
# Test unsupported API version
|
|
61
|
+
mock_handler.api_version = "v2/"
|
|
62
|
+
assert client.check_apiversion_supported()[0] == RequestStatus.API_VERSION_UNSUPPORTED
|
|
63
|
+
|
|
64
|
+
# Test missing API version
|
|
65
|
+
mock_handler.api_version = None
|
|
66
|
+
assert client.check_apiversion_supported()[0] == RequestStatus.NO_DATA
|
|
@@ -12,14 +12,3 @@ async def test_host_unreachable(monkeypatch):
|
|
|
12
12
|
handler = RequestHandler("256.256.256.256", timeout=1)
|
|
13
13
|
status = await handler.connect()
|
|
14
14
|
assert status == RequestStatus.HOST_UNREACHABLE
|
|
15
|
-
|
|
16
|
-
@pytest.mark.asyncio
|
|
17
|
-
async def test_check_apiversion_supported():
|
|
18
|
-
"""Test API version check logic."""
|
|
19
|
-
handler = RequestHandler("localhost")
|
|
20
|
-
handler.api_version = "v1/"
|
|
21
|
-
assert handler.check_apiversion_supported()[0] == RequestStatus.SUCCESS
|
|
22
|
-
handler.api_version = "v2/"
|
|
23
|
-
assert handler.check_apiversion_supported()[0] == RequestStatus.API_VERSION_UNSUPPORTED
|
|
24
|
-
handler.api_version = None
|
|
25
|
-
assert handler.check_apiversion_supported()[0] == RequestStatus.NO_DATA
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/python_pooldose.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|