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.
Files changed (34) hide show
  1. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/PKG-INFO +427 -163
  2. python_pooldose-0.5.1/README.md +772 -0
  3. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/pyproject.toml +1 -1
  4. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/client.py +28 -71
  5. python_pooldose-0.5.1/src/pooldose/constants.py +29 -0
  6. python_pooldose-0.5.1/src/pooldose/mappings/model_PDPR1H1HAR1V0_FW539224.json +110 -0
  7. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/mappings/model_PDPR1H1HAW100_FW539187.json +8 -0
  8. python_pooldose-0.5.1/src/pooldose/mock_client.py +256 -0
  9. python_pooldose-0.5.1/src/pooldose/values/instant_values.py +451 -0
  10. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/python_pooldose.egg-info/PKG-INFO +427 -163
  11. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/python_pooldose.egg-info/SOURCES.txt +3 -0
  12. python_pooldose-0.5.1/tests/test_client.py +270 -0
  13. python_pooldose-0.5.1/tests/test_instant_values.py +206 -0
  14. python_pooldose-0.5.1/tests/test_static_values.py +25 -0
  15. python_pooldose-0.4.6/README.md +0 -508
  16. python_pooldose-0.4.6/src/pooldose/values/instant_values.py +0 -270
  17. python_pooldose-0.4.6/tests/test_client.py +0 -67
  18. python_pooldose-0.4.6/tests/test_instant_values.py +0 -77
  19. python_pooldose-0.4.6/tests/test_static_values.py +0 -43
  20. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/LICENSE +0 -0
  21. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/setup.cfg +0 -0
  22. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/__init__.py +0 -0
  23. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/mappings/__init__.py +0 -0
  24. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/mappings/mapping_info.py +0 -0
  25. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/request_handler.py +0 -0
  26. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/request_status.py +0 -0
  27. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/values/__init__.py +0 -0
  28. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/pooldose/values/static_values.py +0 -0
  29. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/python_pooldose.egg-info/dependency_links.txt +0 -0
  30. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/python_pooldose.egg-info/requires.txt +0 -0
  31. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/src/python_pooldose.egg-info/top_level.txt +0 -0
  32. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/tests/test_mapping_info.py +0 -0
  33. {python_pooldose-0.4.6 → python_pooldose-0.5.1}/tests/test_request_handler.py +0 -0
  34. {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.4.6
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
- 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.
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
- - **Type-specific getters** for sensors, switches, numbers, selects
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
- ├── Fetch Device Info
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
- ├── Load Mapping JSON (based on MODEL_ID + FW_CODE)
43
- └── Query Available Types
44
- ├── Sensors
45
- ├── Binary Sensors
46
- ├── Numbers
47
- ├── Switches
48
- └── Selects
49
-
50
- 2. Get Instant Values
51
- └── Access Values via Dictionary Interface
52
- ├── instant_values['temperature']
53
- ├── instant_values.get('ph', default)
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
- Type Methods
100
- │ • set_number()
101
- │ • set_switch()
102
- │ • set_select()
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 new dictionary-based API."""
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
- if "ph_target" in instant_values.get_numbers():
254
- result = await instant_values.set_number("ph_target", 7.2)
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
- if "stop_pool_dosing" in instant_values.get_switches():
259
- result = await instant_values.set_switch("stop_pool_dosing", True)
260
- print(f"Set stop pool dosing: {result}")
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
- #### Type-specific Access
580
+ #### Working with Structured Data
581
+
312
582
  ```python
313
- # Get all values by type
314
- sensors = instant_values.get_sensors() # All sensor readings
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
- # Check available types dynamically
321
- available_types = instant_values.available_types()
322
- print("Available types:", list(available_types.keys()))
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 Hierarchy
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=10, include_sensitive_data=False, use_ssl=False, port=None, ssl_verify=True)
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: 10)
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
- - `connect()` - Connect to device and initialize all components
405
- - `static_values()` - Get static device information
406
- - `instant_values()` - Get current sensor readings and device state
407
- - `available_types()` - Get all available entity types
408
- - `available_sensors()` - Get available sensor configurations
409
- - `available_binary_sensors()` - Get available binary sensor configurations
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
- - `is_connected` - Check if client is connected to device
416
- - `device_info` - Dictionary containing device information
417
- - `host` - Device hostname or IP address
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
- # Writing (async)
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
- # Getters
470
- sensors = instant_values.get_sensors()
471
- binary_sensors = instant_values.get_binary_sensors()
472
- numbers = instant_values.get_numbers()
473
- switches = instant_values.get_switches()
474
- selects = instant_values.get_selects()
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
- # Setters (async, with validation)
477
- await instant_values.set_number("ph_target", 7.2)
478
- await instant_values.set_switch("stop_dosing", True)
479
- await instant_values.set_select("water_meter_unit", 1)
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 other JSON files in the `docs/` directory define the default English names for the data keys of the PoolDose devices. These mappings are used for display and documentation purposes.
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.4.6)
526
- - Device returns unit for pH values, which have physically no unit. Fixed by replacing such occurrences with None.
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