python-pooldose 0.3.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.3.1/LICENSE +21 -0
- python_pooldose-0.3.1/PKG-INFO +324 -0
- python_pooldose-0.3.1/README.md +306 -0
- python_pooldose-0.3.1/pyproject.toml +38 -0
- python_pooldose-0.3.1/setup.cfg +4 -0
- python_pooldose-0.3.1/src/pooldose/__init__.py +4 -0
- python_pooldose-0.3.1/src/pooldose/client.py +228 -0
- python_pooldose-0.3.1/src/pooldose/mappings/__init__.py +1 -0
- python_pooldose-0.3.1/src/pooldose/mappings/mapping_info.py +222 -0
- python_pooldose-0.3.1/src/pooldose/mappings/model_PDPR1H1HAW100_FW539187.json +153 -0
- python_pooldose-0.3.1/src/pooldose/request_handler.py +395 -0
- python_pooldose-0.3.1/src/pooldose/values/__init__.py +1 -0
- python_pooldose-0.3.1/src/pooldose/values/instant_values.py +270 -0
- python_pooldose-0.3.1/src/pooldose/values/static_values.py +180 -0
- python_pooldose-0.3.1/src/python_pooldose.egg-info/PKG-INFO +324 -0
- python_pooldose-0.3.1/src/python_pooldose.egg-info/SOURCES.txt +22 -0
- python_pooldose-0.3.1/src/python_pooldose.egg-info/dependency_links.txt +1 -0
- python_pooldose-0.3.1/src/python_pooldose.egg-info/requires.txt +6 -0
- python_pooldose-0.3.1/src/python_pooldose.egg-info/top_level.txt +1 -0
- python_pooldose-0.3.1/tests/test_client.py +41 -0
- python_pooldose-0.3.1/tests/test_instant_values.py +77 -0
- python_pooldose-0.3.1/tests/test_mapping_info.py +61 -0
- python_pooldose-0.3.1/tests/test_request_handler.py +25 -0
- python_pooldose-0.3.1/tests/test_static_values.py +43 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Lukas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-pooldose
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Unoffical async Python client for SEKO PoolDose devices
|
|
5
|
+
Author-email: Lukas Maertin <pypi@lukas-maertin.de>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/lmaertin/python-pooldose
|
|
8
|
+
Project-URL: Repository, https://github.com/lmaertin/python-pooldose
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: aiohttp
|
|
13
|
+
Requires-Dist: aiofiles
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
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.
|
|
21
|
+
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
|
+
|
|
23
|
+
## Features
|
|
24
|
+
- **Async/await support** for non-blocking operations
|
|
25
|
+
- **Dynamic sensor discovery** based on device model and firmware
|
|
26
|
+
- **Dictionary-style access** to instant values
|
|
27
|
+
- **Type-specific getters** for sensors, switches, numbers, selects
|
|
28
|
+
- **Secure by default** - WiFi passwords excluded unless explicitly requested
|
|
29
|
+
- **Comprehensive error handling** with detailed logging
|
|
30
|
+
|
|
31
|
+
## API Overview
|
|
32
|
+
|
|
33
|
+
### Program Flow
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
1. Create PooldoseClient
|
|
37
|
+
├── Fetch Device Info
|
|
38
|
+
│ ├── Debug Config
|
|
39
|
+
│ ├── WiFi Station Info (optional)
|
|
40
|
+
│ ├── Access Point Info (optional)
|
|
41
|
+
│ └── 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
|
|
57
|
+
├── set_number()
|
|
58
|
+
├── set_switch()
|
|
59
|
+
└── set_select()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### API Architecture
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
66
|
+
│ PooldoseClient │────│ RequestHandler │────│ HTTP Device │
|
|
67
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
68
|
+
│ │
|
|
69
|
+
│ ▼
|
|
70
|
+
│ ┌─────────────────┐
|
|
71
|
+
│ │ API Endpoints │
|
|
72
|
+
│ │ • get_debug │
|
|
73
|
+
│ │ • get_wifi │
|
|
74
|
+
│ │ • get_values │
|
|
75
|
+
│ │ • set_value │
|
|
76
|
+
│ └─────────────────┘
|
|
77
|
+
│
|
|
78
|
+
▼
|
|
79
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
80
|
+
│ MappingInfo │────│ JSON Files │
|
|
81
|
+
└─────────────────┘ └─────────────────┘
|
|
82
|
+
│
|
|
83
|
+
▼
|
|
84
|
+
┌─────────────────┐
|
|
85
|
+
│ Type Discovery │
|
|
86
|
+
│ • Sensors │
|
|
87
|
+
│ • Switches │
|
|
88
|
+
│ • Numbers │
|
|
89
|
+
│ • Selects │
|
|
90
|
+
└─────────────────┘
|
|
91
|
+
│
|
|
92
|
+
▼
|
|
93
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
94
|
+
│ InstantValues │────│ Dictionary API │
|
|
95
|
+
└─────────────────┘ └─────────────────┘
|
|
96
|
+
│
|
|
97
|
+
▼
|
|
98
|
+
┌─────────────────┐
|
|
99
|
+
│ Type Methods │
|
|
100
|
+
│ • set_number() │
|
|
101
|
+
│ • set_switch() │
|
|
102
|
+
│ • set_select() │
|
|
103
|
+
└─────────────────┘
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Prerequisites
|
|
107
|
+
1. Install and set-up the PoolDose devices according to the user manual.
|
|
108
|
+
1. In particular, connect the device to your WiFi network.
|
|
109
|
+
2. Identify the IP address or hostname of the device.
|
|
110
|
+
2. Browse to the IP address or hostname (default port: 80).
|
|
111
|
+
1. Try to log in to the web interface with the default password (0000).
|
|
112
|
+
2. Check availability of data in the web interface.
|
|
113
|
+
3. Optionally: Block the device from internet access to ensure cloudless-only operation.
|
|
114
|
+
|
|
115
|
+
## Installation
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pip install python-pooldose
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Example Usage
|
|
122
|
+
|
|
123
|
+
### Basic Example
|
|
124
|
+
```python
|
|
125
|
+
import asyncio
|
|
126
|
+
import json
|
|
127
|
+
from pooldose.client import PooldoseClient
|
|
128
|
+
from pooldose.request_handler import RequestStatus
|
|
129
|
+
|
|
130
|
+
HOST = "192.168.1.100" # Change this to your device's host or IP address
|
|
131
|
+
TIMEOUT = 30
|
|
132
|
+
|
|
133
|
+
async def main() -> None:
|
|
134
|
+
"""Demonstrate PooldoseClient usage with new dictionary-based API."""
|
|
135
|
+
|
|
136
|
+
# Create client (excludes WiFi passwords by default)
|
|
137
|
+
status, client = await PooldoseClient.create(host=HOST, timeout=TIMEOUT)
|
|
138
|
+
|
|
139
|
+
# Optional: Include sensitive data like WiFi passwords
|
|
140
|
+
# status, client = await PooldoseClient.create(host=HOST, timeout=TIMEOUT, include_sensitive_data=True)
|
|
141
|
+
|
|
142
|
+
if status != RequestStatus.SUCCESS:
|
|
143
|
+
print(f"Error creating client: {status}")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
print(f"Connected to {HOST}")
|
|
147
|
+
print("Device Info:", json.dumps(client.device_info, indent=2))
|
|
148
|
+
|
|
149
|
+
# --- Query available types dynamically ---
|
|
150
|
+
print("\nAvailable types:")
|
|
151
|
+
for typ, keys in client.available_types().items():
|
|
152
|
+
print(f" {typ}: {keys}")
|
|
153
|
+
|
|
154
|
+
# --- Query available sensors ---
|
|
155
|
+
print("\nAvailable sensors:")
|
|
156
|
+
for name, sensor in client.available_sensors().items():
|
|
157
|
+
print(f" {name}: key={sensor.key}, type={sensor.type}")
|
|
158
|
+
if sensor.conversion is not None:
|
|
159
|
+
print(f" conversion: {sensor.conversion}")
|
|
160
|
+
|
|
161
|
+
# --- Get static values ---
|
|
162
|
+
status, static_values = client.static_values()
|
|
163
|
+
if status == RequestStatus.SUCCESS:
|
|
164
|
+
print(f"Device Name: {static_values.sensor_name}")
|
|
165
|
+
print(f"Serial Number: {static_values.sensor_serial_number}")
|
|
166
|
+
print(f"Firmware Version: {static_values.sensor_fw_version}")
|
|
167
|
+
|
|
168
|
+
# --- Get instant values ---
|
|
169
|
+
status, instant_values = await client.instant_values()
|
|
170
|
+
if status != RequestStatus.SUCCESS:
|
|
171
|
+
print(f"Error getting instant values: {status}")
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
# --- Dictionary-style access ---
|
|
175
|
+
|
|
176
|
+
# Get all sensors at once
|
|
177
|
+
print("\nAll sensor values:")
|
|
178
|
+
sensors = instant_values.get_sensors()
|
|
179
|
+
for key, value in sensors.items():
|
|
180
|
+
if isinstance(value, tuple) and len(value) >= 2:
|
|
181
|
+
print(f" {key}: {value[0]} {value[1]}")
|
|
182
|
+
|
|
183
|
+
# Dictionary-style individual access
|
|
184
|
+
if "temperature" in instant_values:
|
|
185
|
+
temp = instant_values["temperature"]
|
|
186
|
+
print(f"Temperature: {temp[0]} {temp[1]}")
|
|
187
|
+
|
|
188
|
+
# Get with default
|
|
189
|
+
ph_value = instant_values.get("ph", "Not available")
|
|
190
|
+
print(f"pH: {ph_value}")
|
|
191
|
+
|
|
192
|
+
# --- Setting values ---
|
|
193
|
+
|
|
194
|
+
# Set number values
|
|
195
|
+
if "ph_target" in instant_values.get_numbers():
|
|
196
|
+
result = await instant_values.set_number("ph_target", 7.2)
|
|
197
|
+
print(f"Set pH target to 7.2: {result}")
|
|
198
|
+
|
|
199
|
+
# Set switch values
|
|
200
|
+
if "stop_pool_dosing" in instant_values.get_switches():
|
|
201
|
+
result = await instant_values.set_switch("stop_pool_dosing", True)
|
|
202
|
+
print(f"Set stop pool dosing: {result}")
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
asyncio.run(main())
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Advanced Usage
|
|
209
|
+
|
|
210
|
+
#### Type-specific Access
|
|
211
|
+
```python
|
|
212
|
+
# Get all values by type
|
|
213
|
+
sensors = instant_values.get_sensors() # All sensor readings
|
|
214
|
+
binary_sensors = instant_values.get_binary_sensors() # All boolean states
|
|
215
|
+
numbers = instant_values.get_numbers() # All configurable numbers
|
|
216
|
+
switches = instant_values.get_switches() # All switch states
|
|
217
|
+
selects = instant_values.get_selects() # All select options
|
|
218
|
+
|
|
219
|
+
# Check available types dynamically
|
|
220
|
+
available_types = instant_values.available_types()
|
|
221
|
+
print("Available types:", list(available_types.keys()))
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### Error Handling
|
|
225
|
+
```python
|
|
226
|
+
from pooldose.request_handler import RequestStatus
|
|
227
|
+
|
|
228
|
+
status, client = await PooldoseClient.create("192.168.1.100")
|
|
229
|
+
if status == RequestStatus.SUCCESS:
|
|
230
|
+
print("Connected successfully")
|
|
231
|
+
elif status == RequestStatus.CONNECTION_ERROR:
|
|
232
|
+
print("Could not connect to device")
|
|
233
|
+
elif status == RequestStatus.API_VERSION_UNSUPPORTED:
|
|
234
|
+
print("Unsupported API version")
|
|
235
|
+
else:
|
|
236
|
+
print(f"Other error: {status}")
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## API Reference
|
|
240
|
+
|
|
241
|
+
### PooldoseClient
|
|
242
|
+
|
|
243
|
+
#### Methods
|
|
244
|
+
- `create(host, timeout=10, include_sensitive_data=False)` - Factory method to create and initialize client
|
|
245
|
+
- `static_values()` - Get static device information
|
|
246
|
+
- `instant_values()` - Get current sensor readings and device state
|
|
247
|
+
- `available_types()` - Get all available entity types
|
|
248
|
+
- `available_sensors()` - Get available sensor configurations
|
|
249
|
+
- `available_binary_sensors()` - Get available binary sensor configurations
|
|
250
|
+
- `available_numbers()` - Get available number configurations
|
|
251
|
+
- `available_switches()` - Get available switch configurations
|
|
252
|
+
- `available_selects()` - Get available select configurations
|
|
253
|
+
|
|
254
|
+
### InstantValues
|
|
255
|
+
|
|
256
|
+
#### Dictionary Interface
|
|
257
|
+
```python
|
|
258
|
+
# Reading
|
|
259
|
+
value = instant_values["sensor_name"]
|
|
260
|
+
value = instant_values.get("sensor_name", default)
|
|
261
|
+
exists = "sensor_name" in instant_values
|
|
262
|
+
|
|
263
|
+
# Writing (async)
|
|
264
|
+
await instant_values.__setitem__("switch_name", True)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
#### Type-specific Methods
|
|
268
|
+
```python
|
|
269
|
+
# Getters
|
|
270
|
+
sensors = instant_values.get_sensors()
|
|
271
|
+
binary_sensors = instant_values.get_binary_sensors()
|
|
272
|
+
numbers = instant_values.get_numbers()
|
|
273
|
+
switches = instant_values.get_switches()
|
|
274
|
+
selects = instant_values.get_selects()
|
|
275
|
+
|
|
276
|
+
# Setters (async, with validation)
|
|
277
|
+
await instant_values.set_number("ph_target", 7.2)
|
|
278
|
+
await instant_values.set_switch("stop_dosing", True)
|
|
279
|
+
await instant_values.set_select("water_meter_unit", 1)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Supported Devices
|
|
283
|
+
|
|
284
|
+
This client has been tested with:
|
|
285
|
+
- **PoolDose Double/Dual WiFi** (Model: PDPR1H1HAW100, FW: 539187)
|
|
286
|
+
|
|
287
|
+
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`).
|
|
288
|
+
|
|
289
|
+
> **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.
|
|
290
|
+
|
|
291
|
+
## Security
|
|
292
|
+
|
|
293
|
+
By default, the client excludes sensitive information like WiFi passwords from device info. To include sensitive data:
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
status, client = await PooldoseClient.create(
|
|
297
|
+
host="192.168.1.100",
|
|
298
|
+
include_sensitive_data=True
|
|
299
|
+
)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Changelog
|
|
303
|
+
|
|
304
|
+
### [0.3.1] - 2025-07-04
|
|
305
|
+
- First official release, published on PyPi
|
|
306
|
+
- Install with ```pip install python-pooldose```
|
|
307
|
+
|
|
308
|
+
### [0.3.0] - 2025-07-02
|
|
309
|
+
- **BREAKING**: Changed from dataclass properties to dictionary-based access for instant values
|
|
310
|
+
- Added dynamic sensor discovery based on device mapping files
|
|
311
|
+
- Added type-specific getter methods (get_sensors, get_switches, etc.)
|
|
312
|
+
- Added type-specific setter methods with validation (set_number, set_switch, etc.)
|
|
313
|
+
- Added dictionary-style access (__getitem__, __setitem__, get, __contains__)
|
|
314
|
+
- Added configurable sensitive data handling (excludes WiFi passwords by default)
|
|
315
|
+
- Improved async file loading to prevent event loop blocking
|
|
316
|
+
- Enhanced error handling and logging
|
|
317
|
+
- Added comprehensive type annotations
|
|
318
|
+
|
|
319
|
+
### [0.2.0] - 2024-06-25
|
|
320
|
+
- Added query feature to list all available sensors and actuators
|
|
321
|
+
|
|
322
|
+
### [0.1.5] - 2024-06-24
|
|
323
|
+
- First working prototype for PoolDose Double/Dual WiFi supported
|
|
324
|
+
- All sensors and actuators for PoolDose Double/Dual WiFi supported
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# python-pooldose
|
|
2
|
+
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.
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- **Async/await support** for non-blocking operations
|
|
7
|
+
- **Dynamic sensor discovery** based on device model and firmware
|
|
8
|
+
- **Dictionary-style access** to instant values
|
|
9
|
+
- **Type-specific getters** for sensors, switches, numbers, selects
|
|
10
|
+
- **Secure by default** - WiFi passwords excluded unless explicitly requested
|
|
11
|
+
- **Comprehensive error handling** with detailed logging
|
|
12
|
+
|
|
13
|
+
## API Overview
|
|
14
|
+
|
|
15
|
+
### Program Flow
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
1. Create PooldoseClient
|
|
19
|
+
├── Fetch Device Info
|
|
20
|
+
│ ├── Debug Config
|
|
21
|
+
│ ├── WiFi Station Info (optional)
|
|
22
|
+
│ ├── Access Point Info (optional)
|
|
23
|
+
│ └── Network Info
|
|
24
|
+
├── Load Mapping JSON (based on MODEL_ID + FW_CODE)
|
|
25
|
+
└── Query Available Types
|
|
26
|
+
├── Sensors
|
|
27
|
+
├── Binary Sensors
|
|
28
|
+
├── Numbers
|
|
29
|
+
├── Switches
|
|
30
|
+
└── Selects
|
|
31
|
+
|
|
32
|
+
2. Get Instant Values
|
|
33
|
+
└── Access Values via Dictionary Interface
|
|
34
|
+
├── instant_values['temperature']
|
|
35
|
+
├── instant_values.get('ph', default)
|
|
36
|
+
└── 'sensor_name' in instant_values
|
|
37
|
+
|
|
38
|
+
3. Set Values via Type Methods
|
|
39
|
+
├── set_number()
|
|
40
|
+
├── set_switch()
|
|
41
|
+
└── set_select()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### API Architecture
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
48
|
+
│ PooldoseClient │────│ RequestHandler │────│ HTTP Device │
|
|
49
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
50
|
+
│ │
|
|
51
|
+
│ ▼
|
|
52
|
+
│ ┌─────────────────┐
|
|
53
|
+
│ │ API Endpoints │
|
|
54
|
+
│ │ • get_debug │
|
|
55
|
+
│ │ • get_wifi │
|
|
56
|
+
│ │ • get_values │
|
|
57
|
+
│ │ • set_value │
|
|
58
|
+
│ └─────────────────┘
|
|
59
|
+
│
|
|
60
|
+
▼
|
|
61
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
62
|
+
│ MappingInfo │────│ JSON Files │
|
|
63
|
+
└─────────────────┘ └─────────────────┘
|
|
64
|
+
│
|
|
65
|
+
▼
|
|
66
|
+
┌─────────────────┐
|
|
67
|
+
│ Type Discovery │
|
|
68
|
+
│ • Sensors │
|
|
69
|
+
│ • Switches │
|
|
70
|
+
│ • Numbers │
|
|
71
|
+
│ • Selects │
|
|
72
|
+
└─────────────────┘
|
|
73
|
+
│
|
|
74
|
+
▼
|
|
75
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
76
|
+
│ InstantValues │────│ Dictionary API │
|
|
77
|
+
└─────────────────┘ └─────────────────┘
|
|
78
|
+
│
|
|
79
|
+
▼
|
|
80
|
+
┌─────────────────┐
|
|
81
|
+
│ Type Methods │
|
|
82
|
+
│ • set_number() │
|
|
83
|
+
│ • set_switch() │
|
|
84
|
+
│ • set_select() │
|
|
85
|
+
└─────────────────┘
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Prerequisites
|
|
89
|
+
1. Install and set-up the PoolDose devices according to the user manual.
|
|
90
|
+
1. In particular, connect the device to your WiFi network.
|
|
91
|
+
2. Identify the IP address or hostname of the device.
|
|
92
|
+
2. Browse to the IP address or hostname (default port: 80).
|
|
93
|
+
1. Try to log in to the web interface with the default password (0000).
|
|
94
|
+
2. Check availability of data in the web interface.
|
|
95
|
+
3. Optionally: Block the device from internet access to ensure cloudless-only operation.
|
|
96
|
+
|
|
97
|
+
## Installation
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install python-pooldose
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Example Usage
|
|
104
|
+
|
|
105
|
+
### Basic Example
|
|
106
|
+
```python
|
|
107
|
+
import asyncio
|
|
108
|
+
import json
|
|
109
|
+
from pooldose.client import PooldoseClient
|
|
110
|
+
from pooldose.request_handler import RequestStatus
|
|
111
|
+
|
|
112
|
+
HOST = "192.168.1.100" # Change this to your device's host or IP address
|
|
113
|
+
TIMEOUT = 30
|
|
114
|
+
|
|
115
|
+
async def main() -> None:
|
|
116
|
+
"""Demonstrate PooldoseClient usage with new dictionary-based API."""
|
|
117
|
+
|
|
118
|
+
# Create client (excludes WiFi passwords by default)
|
|
119
|
+
status, client = await PooldoseClient.create(host=HOST, timeout=TIMEOUT)
|
|
120
|
+
|
|
121
|
+
# Optional: Include sensitive data like WiFi passwords
|
|
122
|
+
# status, client = await PooldoseClient.create(host=HOST, timeout=TIMEOUT, include_sensitive_data=True)
|
|
123
|
+
|
|
124
|
+
if status != RequestStatus.SUCCESS:
|
|
125
|
+
print(f"Error creating client: {status}")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
print(f"Connected to {HOST}")
|
|
129
|
+
print("Device Info:", json.dumps(client.device_info, indent=2))
|
|
130
|
+
|
|
131
|
+
# --- Query available types dynamically ---
|
|
132
|
+
print("\nAvailable types:")
|
|
133
|
+
for typ, keys in client.available_types().items():
|
|
134
|
+
print(f" {typ}: {keys}")
|
|
135
|
+
|
|
136
|
+
# --- Query available sensors ---
|
|
137
|
+
print("\nAvailable sensors:")
|
|
138
|
+
for name, sensor in client.available_sensors().items():
|
|
139
|
+
print(f" {name}: key={sensor.key}, type={sensor.type}")
|
|
140
|
+
if sensor.conversion is not None:
|
|
141
|
+
print(f" conversion: {sensor.conversion}")
|
|
142
|
+
|
|
143
|
+
# --- Get static values ---
|
|
144
|
+
status, static_values = client.static_values()
|
|
145
|
+
if status == RequestStatus.SUCCESS:
|
|
146
|
+
print(f"Device Name: {static_values.sensor_name}")
|
|
147
|
+
print(f"Serial Number: {static_values.sensor_serial_number}")
|
|
148
|
+
print(f"Firmware Version: {static_values.sensor_fw_version}")
|
|
149
|
+
|
|
150
|
+
# --- Get instant values ---
|
|
151
|
+
status, instant_values = await client.instant_values()
|
|
152
|
+
if status != RequestStatus.SUCCESS:
|
|
153
|
+
print(f"Error getting instant values: {status}")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# --- Dictionary-style access ---
|
|
157
|
+
|
|
158
|
+
# Get all sensors at once
|
|
159
|
+
print("\nAll sensor values:")
|
|
160
|
+
sensors = instant_values.get_sensors()
|
|
161
|
+
for key, value in sensors.items():
|
|
162
|
+
if isinstance(value, tuple) and len(value) >= 2:
|
|
163
|
+
print(f" {key}: {value[0]} {value[1]}")
|
|
164
|
+
|
|
165
|
+
# Dictionary-style individual access
|
|
166
|
+
if "temperature" in instant_values:
|
|
167
|
+
temp = instant_values["temperature"]
|
|
168
|
+
print(f"Temperature: {temp[0]} {temp[1]}")
|
|
169
|
+
|
|
170
|
+
# Get with default
|
|
171
|
+
ph_value = instant_values.get("ph", "Not available")
|
|
172
|
+
print(f"pH: {ph_value}")
|
|
173
|
+
|
|
174
|
+
# --- Setting values ---
|
|
175
|
+
|
|
176
|
+
# Set number values
|
|
177
|
+
if "ph_target" in instant_values.get_numbers():
|
|
178
|
+
result = await instant_values.set_number("ph_target", 7.2)
|
|
179
|
+
print(f"Set pH target to 7.2: {result}")
|
|
180
|
+
|
|
181
|
+
# Set switch values
|
|
182
|
+
if "stop_pool_dosing" in instant_values.get_switches():
|
|
183
|
+
result = await instant_values.set_switch("stop_pool_dosing", True)
|
|
184
|
+
print(f"Set stop pool dosing: {result}")
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
asyncio.run(main())
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Advanced Usage
|
|
191
|
+
|
|
192
|
+
#### Type-specific Access
|
|
193
|
+
```python
|
|
194
|
+
# Get all values by type
|
|
195
|
+
sensors = instant_values.get_sensors() # All sensor readings
|
|
196
|
+
binary_sensors = instant_values.get_binary_sensors() # All boolean states
|
|
197
|
+
numbers = instant_values.get_numbers() # All configurable numbers
|
|
198
|
+
switches = instant_values.get_switches() # All switch states
|
|
199
|
+
selects = instant_values.get_selects() # All select options
|
|
200
|
+
|
|
201
|
+
# Check available types dynamically
|
|
202
|
+
available_types = instant_values.available_types()
|
|
203
|
+
print("Available types:", list(available_types.keys()))
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### Error Handling
|
|
207
|
+
```python
|
|
208
|
+
from pooldose.request_handler import RequestStatus
|
|
209
|
+
|
|
210
|
+
status, client = await PooldoseClient.create("192.168.1.100")
|
|
211
|
+
if status == RequestStatus.SUCCESS:
|
|
212
|
+
print("Connected successfully")
|
|
213
|
+
elif status == RequestStatus.CONNECTION_ERROR:
|
|
214
|
+
print("Could not connect to device")
|
|
215
|
+
elif status == RequestStatus.API_VERSION_UNSUPPORTED:
|
|
216
|
+
print("Unsupported API version")
|
|
217
|
+
else:
|
|
218
|
+
print(f"Other error: {status}")
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## API Reference
|
|
222
|
+
|
|
223
|
+
### PooldoseClient
|
|
224
|
+
|
|
225
|
+
#### Methods
|
|
226
|
+
- `create(host, timeout=10, include_sensitive_data=False)` - Factory method to create and initialize client
|
|
227
|
+
- `static_values()` - Get static device information
|
|
228
|
+
- `instant_values()` - Get current sensor readings and device state
|
|
229
|
+
- `available_types()` - Get all available entity types
|
|
230
|
+
- `available_sensors()` - Get available sensor configurations
|
|
231
|
+
- `available_binary_sensors()` - Get available binary sensor configurations
|
|
232
|
+
- `available_numbers()` - Get available number configurations
|
|
233
|
+
- `available_switches()` - Get available switch configurations
|
|
234
|
+
- `available_selects()` - Get available select configurations
|
|
235
|
+
|
|
236
|
+
### InstantValues
|
|
237
|
+
|
|
238
|
+
#### Dictionary Interface
|
|
239
|
+
```python
|
|
240
|
+
# Reading
|
|
241
|
+
value = instant_values["sensor_name"]
|
|
242
|
+
value = instant_values.get("sensor_name", default)
|
|
243
|
+
exists = "sensor_name" in instant_values
|
|
244
|
+
|
|
245
|
+
# Writing (async)
|
|
246
|
+
await instant_values.__setitem__("switch_name", True)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### Type-specific Methods
|
|
250
|
+
```python
|
|
251
|
+
# Getters
|
|
252
|
+
sensors = instant_values.get_sensors()
|
|
253
|
+
binary_sensors = instant_values.get_binary_sensors()
|
|
254
|
+
numbers = instant_values.get_numbers()
|
|
255
|
+
switches = instant_values.get_switches()
|
|
256
|
+
selects = instant_values.get_selects()
|
|
257
|
+
|
|
258
|
+
# Setters (async, with validation)
|
|
259
|
+
await instant_values.set_number("ph_target", 7.2)
|
|
260
|
+
await instant_values.set_switch("stop_dosing", True)
|
|
261
|
+
await instant_values.set_select("water_meter_unit", 1)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Supported Devices
|
|
265
|
+
|
|
266
|
+
This client has been tested with:
|
|
267
|
+
- **PoolDose Double/Dual WiFi** (Model: PDPR1H1HAW100, FW: 539187)
|
|
268
|
+
|
|
269
|
+
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`).
|
|
270
|
+
|
|
271
|
+
> **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.
|
|
272
|
+
|
|
273
|
+
## Security
|
|
274
|
+
|
|
275
|
+
By default, the client excludes sensitive information like WiFi passwords from device info. To include sensitive data:
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
status, client = await PooldoseClient.create(
|
|
279
|
+
host="192.168.1.100",
|
|
280
|
+
include_sensitive_data=True
|
|
281
|
+
)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Changelog
|
|
285
|
+
|
|
286
|
+
### [0.3.1] - 2025-07-04
|
|
287
|
+
- First official release, published on PyPi
|
|
288
|
+
- Install with ```pip install python-pooldose```
|
|
289
|
+
|
|
290
|
+
### [0.3.0] - 2025-07-02
|
|
291
|
+
- **BREAKING**: Changed from dataclass properties to dictionary-based access for instant values
|
|
292
|
+
- Added dynamic sensor discovery based on device mapping files
|
|
293
|
+
- Added type-specific getter methods (get_sensors, get_switches, etc.)
|
|
294
|
+
- Added type-specific setter methods with validation (set_number, set_switch, etc.)
|
|
295
|
+
- Added dictionary-style access (__getitem__, __setitem__, get, __contains__)
|
|
296
|
+
- Added configurable sensitive data handling (excludes WiFi passwords by default)
|
|
297
|
+
- Improved async file loading to prevent event loop blocking
|
|
298
|
+
- Enhanced error handling and logging
|
|
299
|
+
- Added comprehensive type annotations
|
|
300
|
+
|
|
301
|
+
### [0.2.0] - 2024-06-25
|
|
302
|
+
- Added query feature to list all available sensors and actuators
|
|
303
|
+
|
|
304
|
+
### [0.1.5] - 2024-06-24
|
|
305
|
+
- First working prototype for PoolDose Double/Dual WiFi supported
|
|
306
|
+
- All sensors and actuators for PoolDose Double/Dual WiFi supported
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "python-pooldose"
|
|
3
|
+
version = "0.3.1"
|
|
4
|
+
description = "Unoffical async Python client for SEKO PoolDose devices"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Lukas Maertin", email = "pypi@lukas-maertin.de" }
|
|
7
|
+
]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"aiohttp",
|
|
13
|
+
"aiofiles"
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://github.com/lmaertin/python-pooldose"
|
|
18
|
+
Repository = "https://github.com/lmaertin/python-pooldose"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
22
|
+
build-backend = "setuptools.build_meta"
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = ["pytest", "pytest-asyncio"]
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
package-dir = {"" = "src"}
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.package-data]
|
|
34
|
+
"pooldose.mappings" = ["*.json"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
testpaths = ["tests"]
|
|
38
|
+
asyncio_mode = "auto"
|