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.
Files changed (25) hide show
  1. {python_pooldose-0.4.0/src/python_pooldose.egg-info → python_pooldose-0.4.1}/PKG-INFO +139 -5
  2. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/README.md +138 -4
  3. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/pyproject.toml +1 -1
  4. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/client.py +28 -0
  5. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/request_handler.py +2 -52
  6. python_pooldose-0.4.1/src/pooldose/request_status.py +26 -0
  7. {python_pooldose-0.4.0 → python_pooldose-0.4.1/src/python_pooldose.egg-info}/PKG-INFO +139 -5
  8. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/python_pooldose.egg-info/SOURCES.txt +1 -0
  9. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/tests/test_client.py +27 -2
  10. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/tests/test_request_handler.py +0 -11
  11. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/LICENSE +0 -0
  12. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/setup.cfg +0 -0
  13. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/__init__.py +0 -0
  14. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/mappings/__init__.py +0 -0
  15. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/mappings/mapping_info.py +0 -0
  16. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/mappings/model_PDPR1H1HAW100_FW539187.json +0 -0
  17. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/values/__init__.py +0 -0
  18. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/values/instant_values.py +0 -0
  19. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/pooldose/values/static_values.py +0 -0
  20. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/python_pooldose.egg-info/dependency_links.txt +0 -0
  21. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/python_pooldose.egg-info/requires.txt +0 -0
  22. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/src/python_pooldose.egg-info/top_level.txt +0 -0
  23. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/tests/test_instant_values.py +0 -0
  24. {python_pooldose-0.4.0 → python_pooldose-0.4.1}/tests/test_mapping_info.py +0 -0
  25. {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.0
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.request_handler import RequestStatus
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.request_handler import RequestStatus
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
- ### InstantValues
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.request_handler import RequestStatus
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.request_handler import RequestStatus
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
- ### InstantValues
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-pooldose"
3
- version = "0.4.0"
3
+ version = "0.4.1"
4
4
  description = "Unoffical async Python client for SEKO PoolDose devices"
5
5
  authors = [
6
6
  { name = "Lukas Maertin", email = "pypi@lukas-maertin.de" }
@@ -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.0
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.request_handler import RequestStatus
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.request_handler import RequestStatus
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
- ### InstantValues
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
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  src/pooldose/__init__.py
5
5
  src/pooldose/client.py
6
6
  src/pooldose/request_handler.py
7
+ src/pooldose/request_status.py
7
8
  src/pooldose/mappings/__init__.py
8
9
  src/pooldose/mappings/mapping_info.py
9
10
  src/pooldose/mappings/model_PDPR1H1HAW100_FW539187.json
@@ -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
- from pooldose.client import PooldoseClient
5
- from pooldose.request_handler import RequestStatus
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