Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b7ec8d6
interim commit
lmaertin Oct 10, 2025
5906bfa
interim commit
lmaertin Oct 10, 2025
47693f9
Added initial mapping file for model PDHC1H1HAR1V1 with firmware FW53…
Oct 13, 2025
fed87c1
fix pylint false positive and updated readme.md
Oct 13, 2025
78b9664
fixed mock client and added demo code
Oct 14, 2025
5723dba
Added OFA Alarms for pH and ORP for PDPR1H1HAR1V0_FW539224
Oct 14, 2025
adc7f7a
Minor fix
Oct 14, 2025
69154d4
fixed handling of non-dict instant values, i.e., pure boolean values …
Oct 16, 2025
fa1c39e
fixed binary sensor on/off instead of active/ok
Oct 16, 2025
f5b297d
demo config reworked
Oct 16, 2025
f6dcffc
Added additional entities PDHC1H1HAR1V1_FW539224
Oct 16, 2025
752aa6d
Added additional entities for PDFPR1H1HAR1VO
Oct 16, 2025
d01f5c9
alligned entities of PDPR1H1HAW100 with other mapping files
Oct 16, 2025
788f55c
fixed demo config inports
Oct 16, 2025
b131f48
added cli parameters to call mock client with model_id and fw_code, u…
Oct 16, 2025
592fe02
fixed pylint issues
Oct 16, 2025
fe672a6
Add instructons to open a Github issues for supporting a new device
Oct 16, 2025
640ff8a
minor adjustment
Oct 16, 2025
7816901
structural changes
Oct 16, 2025
0ae72b0
experimental support for min/max number of ph OFA alarm values.
Oct 17, 2025
35d8505
MyPy issues fixed and added github action to check strict typing
Oct 17, 2025
f83b7fb
fixed correct packe name types-aiofiles
Oct 17, 2025
531aeff
fixed pylint issues in tests
Oct 17, 2025
5ebb3a9
fixed body of post requests to use always value-arrays, as well for s…
lmaertin Oct 20, 2025
fa26beb
feat: add payload debugging support with --print-payload CLI flag
lmaertin Oct 23, 2025
b1d7359
fix pylint issues in 0.7.6
lmaertin Oct 23, 2025
0c5d1c5
Refine mapping file keys (cosmetic change)
lmaertin Oct 24, 2025
5c2f25d
recovered OFA time values for pH and ORP
lmaertin Oct 24, 2025
e0b4157
Merge remote-tracking branch 'origin/main' into new-device-va-dos-v1
lmaertin Oct 27, 2025
9029f63
minor mapping key fixes
lmaertin Oct 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/mypy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Mypy Type Check

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
mypy:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install mypy
- name: Run mypy
run: |
mypy src
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,5 @@ ehthumbs.db
Thumbs.db

# Project specific
references/*
references/*
examples/demo_config.py
36 changes: 35 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.7.6] - 2025-10-23

### Added

- **CLI Debug Flag**: New `--print-payload` option for debugging HTTP payloads
- Usage: `pooldose --host 192.168.1.100 --print-payload`
- **Payload Debugging**: Added `debug_payload` parameter and `get_last_payload()` method
- **Enhanced Demo**: Updated `examples/demo.py` with payload inspection (only shows payloads when operations succeed)

## [7.5.0] - 2025-10-20

### Added

- Convenience setters on the client: `set_switch`, `set_number`, `set_select` to avoid fetching InstantValues manually.
- Mock client: now returns and stores the concrete POST payload for inspection (via `inspect_payload` / `get_last_payload`) and provides the same convenience setters as the real client.
- Updated `examples/demo.py` to demonstrate usage for both real and mock clients (mock prints payloads when `inspect_payload` is enabled).
- **New Device**: Added support for VA DOS EXACT (Model PDPR1H1HAR1V1, FW FW539224)

### Changed

- `InstantValues.set_number` now pairs `minT`/`maxT` fields and sends them as a single `[min,max]` payload when applicable.
- `RequestHandler.set_value` now always uses arrays for all value types, even for single values (e.g., `[{"value": 7, "type": "NUMBER"}]`, `[{"value": "O", "type": "STRING"}]`).
- Mock client behavior adjusted: tests/demos can opt-in to inspect payloads; tests were updated to expect payload inspection by default.

### Enhanced

- **Fix**: Removed Chlorine Sensor from PDPR1H1HAR1V0, as this is not supported.

### Fixed

- Tests updated to expect inspectable mock payloads by default; test suite now passes.
- Updated demo and tests; full test suite currently passes (119 tests).

---
## [0.7.0] - 2025-09-29

### Enhanced
Expand Down Expand Up @@ -264,4 +298,4 @@ numbers = structured_data.get("number", {})

### Added
- First working prototype for PoolDose Double/Dual WiFi supported
- All sensors and actuators for PoolDose Double/Dual WiFi supported
- All sensors and actuators for PoolDose Double/Dual WiFi supported
74 changes: 56 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# python-pooldose

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. Some devices from VÁGNER POOL are supported as well.
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. Some devices from [VÁGNER POOL](https://www.vagnerpool.com/web/en/) are compatible as well.

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.

> **Disclaimer:** Use at your own risk. No liability for damages or malfunctions.

## Features

- **Async/await support** for non-blocking operations
Expand All @@ -28,7 +30,7 @@ This client uses an undocumented local HTTP API. It provides live readings for p
│ ├── WiFi Station Info (optional)
│ ├── Access Point Info (optional)
│ └── Network Info
└── Load Mapping JSON (based on MODEL_ID + FW_CODE)
└── Load mapping JSON (based on model_id + fw_code)

2. Get Static Values
└── Device information and configuration
Expand Down Expand Up @@ -232,8 +234,8 @@ The analyzer provides comprehensive information about your device:
```
=== DEVICE ANALYSIS ===
Device: 01234567890A_DEVICE
Model: PDPR1H1HAW***
Firmware: FW53****
Model ID: PDZZ1H1HATEST1V1
Firmware Code: 654321

=== WIDGETS (Visible UI Elements) ===

Expand Down Expand Up @@ -277,8 +279,8 @@ pooldose --host 192.168.1.100 --analyze

# Output shows:
# Device: 01987654321B_DEVICE
# Model: PDPR2H2XYZ*** ← New model not yet supported
# Firmware: FW54**** ← New firmware version
# Model ID: PDZZ1H1HATEST1V1 ← New model not yet supported
# Firmware Code: 654321 ← New firmware version
#
# Widgets discovered: 15 sensors, 8 controls, 12 settings
```
Expand All @@ -288,7 +290,40 @@ With this information, you can:
- Share the widget structure for mapping development
- Help expand device support for the community

The device analyzer makes python-pooldose extensible and helps build support for the growing ecosystem of SEKO PoolDose devices.
The device analyzer makes python-pooldose extensible and helps build support for the growing ecosystem of SEKO PoolDose or VÁGNER POOL devices.

### How to request support for a new device

If your device is not yet supported, please help us by creating a GitHub issue and providing the following information:

1. **Run low-level analysis and share the output files:**
- Use the following curl commands.
- Replace the IP address and DeviceId (get the id from the header of the instantvalues.json file, e.g., '012345679_DEVICE') as needed:

- Download debug config info:
```bash
curl http://<YOUR_DEVICE_IP>/api/v1/debug/config/info -o debuginfo.json
```
**Important:** Before uploading, open `debuginfo.json` and remove any WiFi credentials.
- Download instant values
```bash
curl --location --request POST http://<YOUR_DEVICE_IP>/api/v1/DWI/getInstantValues -o instantvalues.json
```
- Download device language strings
```bash
curl --location http://<YOUR_DEVICE_IP>/api/v1/DWI/getDeviceLanguage --data-raw '{"DeviceId":"YOUR_DEVICE_ID","LANG":"en"}' -o strings.json
```
2. **Optional: Run the analyzer and share the output:**
- Run this command if you set up python-pooldose already:
```bash
pooldose --host <YOUR_DEVICE_IP> --analyze
```
- Copy and paste the full output into your issue (remove any sensitive data).

3. **Create a GitHub issue:**
- Attach the the 3 JSON files from above.
- Optionally attach the analyzer output if available.
- This will help us add support for your device faster!

## Examples

Expand Down Expand Up @@ -374,8 +409,13 @@ You can use the mock client with custom JSON files via the command line:
# Use mock client with JSON file
pooldose --mock path/to/your/data.json


# Use mock client with model and firmware code (Beispiel mit Fantasiewerten)
pooldose --mock path/to/your/data.json --model-id PDZZ1H1HATEST1V1 --fw-code 654321

# Or as Python module
python -m pooldose --mock path/to/your/data.json
python -m pooldose --mock path/to/your/data.json --model-id PDZZ1H1HATEST1V1 --fw-code 654321
```

### JSON Data Format
Expand Down Expand Up @@ -718,13 +758,13 @@ Mapping Discovery Process:
┌─────────────────┐
│ Get MODEL_ID │ ──────► PDPR1H1HAW***
│ Get FW_CODE │ ──────► 53****
│ Get Model ID │ ──────► PDZZ1H1HATEST1V1
│ Get Firmware Code │ ──────► 654321
└─────────────────┘
┌─────────────────┐
│ Load JSON File │ ──────► model_PDPR1H1HAW***_FW53****.json
│ Load JSON file │ ──────► model_PDZZ1H1HATEST1V1_FW654321.json
└─────────────────┘
Expand Down Expand Up @@ -843,10 +883,11 @@ The `instant_values_structured()` method returns data organized by type:

This client has been tested with:

- **SEKO PoolDose Double/Dual WiFi** (Model: PDPR1H1HAW***, FW: 53****)
- **VÁGNER POOL VA DOS BASIC** (Model: PDPR1H1HAR***, FW: 53****)
- **SEKO PoolDose Double** (Model: PDPR1H1HAW100, FW: 539187)
- **VÁGNER POOL VA DOS BASIC** (Model: PDHC1H1HAR1V0, FW: 539224)
- **VÁGNER POOL VA DOS EXACT** (Model: PDHC1H1HAR1V1, FW: 539224)

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_PDPR1H1HAW***_FW53****.json`).
Other SEKO or VÁGNER POOL 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`).

> **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.

Expand Down Expand Up @@ -925,9 +966,6 @@ Data Classification:

For detailed release notes and version history, please see [CHANGELOG.md](CHANGELOG.md).

### Latest Release (0.7.0)
### Latest Release (0.7.6)

- **Connection Handling**: Improved session management for more reliable connections
- **RequestHandler**: Centralized session management with internal _get_session method
- **Performance**: Reduced connection overhead for multiple consecutive API calls
- **Error Handling**: Better cleanup of HTTP sessions in error cases
- Added `--print-payload` option for debugging HTTP payloads
88 changes: 78 additions & 10 deletions examples/demo.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,56 @@
"""Demonstration for using the PooldoseClient."""
"""Demo showing common usage of PooldoseClient and the mock client.

This script fetches static and instant values and demonstrates how to use
the typed setters on `InstantValues` (switch, number, select). When the
mock client is used, setter calls print the concrete POST payload that
would be sent to the device.
"""

import asyncio
import sys


from demo_utils import display_structured_data, display_static_values
from pooldose.client import PooldoseClient, RequestStatus
from pooldose.mock_client import MockPooldoseClient

# Set UTF-8 encoding for output
# Ensure stdout is using UTF-8 encoding for consistent output
if sys.stdout.encoding != 'utf-8':
sys.stdout.reconfigure(encoding='utf-8')

# pylint: disable=line-too-long,too-many-branches,too-many-statements

HOST = "192.168.178.137" # Replace with your device's IP address
# Load optional demo configuration from demo_config.py (not checked in)
try:
from demo_config import HOST, USE_MOCK_CLIENT, FILE, MODEL_ID, FW_CODE, DEBUG_PAYLOAD
except ImportError:
# Fallback defaults when no config file is present
USE_MOCK_CLIENT = False
HOST = "kommspot"
FILE = None
MODEL_ID = None
FW_CODE = None
DEBUG_PAYLOAD = True


async def main() -> None:
"""Demonstrate all PooldoseClient calls."""
client = PooldoseClient(host=HOST, include_mac_lookup=True)

# Connect
# Choose real or mock client based on configuration
if USE_MOCK_CLIENT:
print("Using MockPooldoseClient with JSON file", FILE)
# Enable payload inspection so the demo can print the mock POST body
client = MockPooldoseClient(json_file_path=FILE, model_id=MODEL_ID, fw_code=FW_CODE, include_sensitive_data=True, inspect_payload=DEBUG_PAYLOAD)
else:
print("Using real PooldoseClient with network connection. Host:", HOST)
client = PooldoseClient(host=HOST, include_mac_lookup=True, debug_payload=DEBUG_PAYLOAD) # pylint: disable=no-value-for-parameter
# Connect to the device (real or mock)
client_status = await client.connect()
if client_status != RequestStatus.SUCCESS:
print(f"Error connecting to PooldoseClient: {client_status}")
return
print("Connected to Pooldose device.")

print(f"Connected to Pooldose device at {HOST}")

# Static values
# Fetch and display static values
print("\nFetching static values...")
static_values_status, static_values = client.static_values()
if static_values_status != RequestStatus.SUCCESS:
Expand All @@ -37,7 +61,7 @@ async def main() -> None:
print(f" IP: {static_values.sensor_ip}")
print(f" MAC: {static_values.sensor_mac}")

# Structured instant values
# Fetch and display structured instant values
print("\nFetching instant values...")
structured_status, structured_data = await client.instant_values_structured()
if structured_status != RequestStatus.SUCCESS:
Expand All @@ -46,6 +70,50 @@ async def main() -> None:

display_structured_data(structured_data)

# Demonstrate setting values using the client's setters.
print("\n" + "="*50)
print("DEMONSTRATING VALUE SETTERS")
print("="*50)

print("\nSetting switch 'pause_dosing' -> True")
ok = await client.set_switch('pause_dosing', True)
print("Result:", ok)
if ok and DEBUG_PAYLOAD and hasattr(client, 'get_last_payload'):
last_payload = client.get_last_payload()
if last_payload:
print("Payload sent:", last_payload)

print("\nSetting number 'ph_target' -> 7.2")
ok = await client.set_number('ph_target', 7.2)
print("Result:", ok)
if ok and DEBUG_PAYLOAD and hasattr(client, 'get_last_payload'):
last_payload = client.get_last_payload()
if last_payload:
print("Payload sent:", last_payload)

print("\nSetting select 'water_meter_unit' -> 'L'")
ok = await client.set_select('water_meter_unit', 'L')
print("Result:", ok)
if ok and DEBUG_PAYLOAD and hasattr(client, 'get_last_payload'):
last_payload = client.get_last_payload()
if last_payload:
print("Payload sent:", last_payload)

print("\nSetting lower/upper limits of 'ofa_ph' (pairing handled internally)")
ok = await client.set_number('ofa_ph_lower', 6.2)
print("ofa_ph_lower set result:", ok)
if ok and DEBUG_PAYLOAD and hasattr(client, 'get_last_payload'):
last_payload = client.get_last_payload()
if last_payload:
print("Payload sent:", last_payload)

ok = await client.set_number('ofa_ph_upper', 8.1)
print("ofa_ph_upper set result:", ok)
if ok and DEBUG_PAYLOAD and hasattr(client, 'get_last_payload'):
last_payload = client.get_last_payload()
if last_payload:
print("Payload sent:", last_payload)

print("\nDemo completed successfully!")

if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion examples/demo_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def _display_data_type(data_dict, type_name, title):
elif type_name in ["switch", "binary_sensor"]:
value = data.get("value")
if type_name == "binary_sensor":
status = "ACTIVE" if value else "OK"
status = "ON" if value else "OFF"
else:
status = "ON" if value else "OFF"
print(f" {formatted_key}: {status}")
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
aiohttp
aiofiles
types-aiofiles
getmac
pytest
pytest-asyncio
2 changes: 1 addition & 1 deletion src/pooldose/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Async API client for SEKO Pooldose."""
from .client import PooldoseClient

__version__ = "0.7.0"
__version__ = "0.7.6"
__all__ = ["PooldoseClient"]
Loading