From 68e7f0ef277fe9817fff90d060d9b5272851bc33 Mon Sep 17 00:00:00 2001 From: Lukas <12813107+lmaertin@users.noreply.github.com> Date: Thu, 28 May 2026 08:11:22 +0200 Subject: [PATCH 01/10] docs: mention BWT as compatible manufacturer in README --- README.md | 3 +- docs/device-support.md | 1 + .../model_PDPH1H1HAW1B0_FW539494.json | 140 ++++++++++++++++++ tests/test_mapping_info.py | 34 +++++ 4 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/pooldose/mappings/model_PDPH1H1HAW1B0_FW539494.json diff --git a/README.md b/README.md index 2faaf08..34a733e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Mypy](https://github.com/lmaertin/python-pooldose/actions/workflows/mypy.yml/badge.svg)](https://github.com/lmaertin/python-pooldose/actions/workflows/mypy.yml) [![Tests](https://github.com/lmaertin/python-pooldose/actions/workflows/python-app.yml/badge.svg)](https://github.com/lmaertin/python-pooldose/actions/workflows/python-app.yml) -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. +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/) and [BWT](https://www.bwt-group.com/) 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. @@ -101,6 +101,7 @@ See [docs/cli.md](docs/cli.md) for full CLI documentation and device analysis de | SEKO PoolDose Double Spa | PDPR1H04AW100 | 539292 | | | SEKO POOLDOSE pH+ORP CF Group Wi-Fi | PDPR1H1HAW102 | 539187 | Alias for PDPR1H1HAW100 mapping | | SEKO PoolDose pH | PDPH1H1HAW100 | 539176 | pH-only device | +| BWT MEDO CONNECT Wi-Fi | PDPH1H1HAW1B0 | 539494 | Based on SEKO PoolDose pH mapping | | VÁGNER POOL VA DOS BASIC | PDHC1H1HAR1V0 | 539224 | | | VÁGNER POOL VA DOS EXACT | PDHC1H1HAR1V1 | 539224 | Alias for PDPR1H1HAR1V0 mapping | diff --git a/docs/device-support.md b/docs/device-support.md index 8ebf393..da477fe 100644 --- a/docs/device-support.md +++ b/docs/device-support.md @@ -10,6 +10,7 @@ This client has been tested with: | SEKO PoolDose Double Spa | PDPR1H04AW100 | 539292 | | | SEKO POOLDOSE pH+ORP CF Group Wi-Fi | PDPR1H1HAW102 | 539187 | Alias for PDPR1H1HAW100 mapping | | SEKO PoolDose pH | PDPH1H1HAW100 | 539176 | pH-only device | +| BWT MEDO CONNECT Wi-Fi | PDPH1H1HAW1B0 | 539494 | Based on SEKO PoolDose pH mapping | | VÁGNER POOL VA DOS BASIC | PDHC1H1HAR1V0 | 539224 | | | VÁGNER POOL VA DOS EXACT | PDHC1H1HAR1V1 | 539224 | Alias for PDPR1H1HAR1V0 mapping | diff --git a/src/pooldose/mappings/model_PDPH1H1HAW1B0_FW539494.json b/src/pooldose/mappings/model_PDPH1H1HAW1B0_FW539494.json new file mode 100644 index 0000000..d61a57c --- /dev/null +++ b/src/pooldose/mappings/model_PDPH1H1HAW1B0_FW539494.json @@ -0,0 +1,140 @@ +{ + "temperature": { + "key": "w_1eomiide3", + "type": "sensor" + }, + "ph": { + "key": "w_1ekeigkin", + "type": "sensor" + }, + "orp": { + "key": "w_1eklenb23", + "type": "sensor" + }, + "ph_type_dosing": { + "key": "w_1eklg44ro", + "type": "sensor", + "conversion": { + "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklg44ro_ALCALYNE|": "alcalyne", + "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklg44ro_ACID|": "acid" + } + }, + "peristaltic_ph_dosing": { + "key": "w_1eklj6euj", + "type": "sensor", + "conversion": { + "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklj6euj_OFF|": "off", + "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklj6euj_PROPORTIONAL|": "proportional", + "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklj6euj_ON_OFF|": "on / off", + "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklj6euj_TIMED|": "timed" + } + }, + "orp_type_dosing": { + "key": "w_1eklgnolb", + "type": "sensor", + "conversion": { + "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklgnolb_LOW|": "low", + "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklgnolb_HIGH|": "high" + } + }, + "peristaltic_orp_dosing": { + "key": "w_1eklj12vv", + "type": "sensor", + "conversion": { + "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklj12vv_OFF|": "off", + "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklj12vv_PROPORTIONAL|": "proportional", + "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklj12vv_ON_OFF|": "on / off", + "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklj12vv_TIMED|": "timed" + } + }, + "ph_calibration_type": { + "key": "w_1eklh8gb7", + "type": "sensor", + "conversion": { + "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklh8gb7_OFF|": "off", + "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklh8gb7_REFERENCE|": "reference", + "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklh8gb7_1_POINT|": "1_point", + "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklh8gb7_2_POINTS|": "2_points" + } + }, + "ph_calibration_offset": { + "key": "w_1eklhs3b4", + "type": "sensor" + }, + "ph_calibration_slope": { + "key": "w_1eklhs65u", + "type": "sensor" + }, + "orp_calibration_slope": { + "key": "w_1eklhsase", + "type": "sensor" + }, + "ofa_value": { + "key": "w_1eklhj58e", + "type": "sensor" + }, + "pump_alarm": { + "key": "w_1ekga097n", + "type": "binary_sensor" + }, + "ph_level_alarm": { + "key": "w_1eklf77pm", + "type": "binary_sensor" + }, + "relay_alarm": { + "key": "w_1eklffdl0", + "type": "binary_sensor" + }, + "relay_aux": { + "key": "w_1eklfgs1t", + "type": "binary_sensor" + }, + "alarm_ofa_ph": { + "key": "w_1eklfb73r", + "type": "binary_sensor" + }, + "alarm_ofa_orp": { + "key": "w_1eklfb8ob", + "type": "binary_sensor" + }, + "ph_target": { + "key": "w_1ekeiqfat", + "type": "number" + }, + "time_off_ph_dosing": { + "key": "w_1eklj30b7", + "type": "number" + }, + "power_on_delay_timer": { + "key": "w_1ffnkkn14", + "type": "number" + }, + "flow_delay_timer": { + "key": "w_1ffnkkp29", + "type": "number" + }, + "pause_dosing": { + "key": "w_1emtltkel", + "type": "switch" + }, + "pump_monitoring": { + "key": "w_1eklft47q", + "type": "switch" + }, + "frequency_input": { + "key": "w_1eklft5qt", + "type": "switch" + }, + "water_meter_unit": { + "key": "w_1eklinki6", + "type": "select", + "options": { + "0": "PDPH1H1HAW1B0_FW539494_COMBO_w_1eklinki6_M_", + "1": "PDPH1H1HAW1B0_FW539494_COMBO_w_1eklinki6_LITER" + }, + "conversion": { + "PDPH1H1HAW1B0_FW539494_COMBO_w_1eklinki6_M_": "m3", + "PDPH1H1HAW1B0_FW539494_COMBO_w_1eklinki6_LITER": "L" + } + } +} \ No newline at end of file diff --git a/tests/test_mapping_info.py b/tests/test_mapping_info.py index d187fe0..ad37d7b 100644 --- a/tests/test_mapping_info.py +++ b/tests/test_mapping_info.py @@ -192,3 +192,37 @@ async def test_double_spa_entity_counts(self): assert len(types.get("number", [])) == 11 assert len(types.get("switch", [])) == 3 assert len(types.get("select", [])) == 1 + + +class TestMedoConnectMapping: + """Tests for the BWT MEDO CONNECT Wi-Fi mapping file (PDPH1H1HAW1B0_FW539494).""" + + @pytest.mark.asyncio + async def test_load_medo_connect_mapping(self): + """Test that the MEDO CONNECT mapping file loads successfully.""" + mapping_info = await MappingInfo.load("PDPH1H1HAW1B0", "539494") + assert mapping_info.status == RequestStatus.SUCCESS + assert mapping_info.mapping is not None + + @pytest.mark.asyncio + async def test_medo_connect_conversion_prefixes(self): + """Test conversion entries use the expected model/firmware label prefix.""" + mapping_info = await MappingInfo.load("PDPH1H1HAW1B0", "539494") + sensors = mapping_info.available_sensors() + + ph_type = sensors["ph_type_dosing"] + assert ph_type.conversion is not None + assert "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklg44ro_ACID|" in ph_type.conversion + assert ph_type.conversion["|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklg44ro_ACID|"] == "acid" + + @pytest.mark.asyncio + async def test_medo_connect_entity_counts(self): + """Test the total entity counts for the MEDO CONNECT mapping.""" + mapping_info = await MappingInfo.load("PDPH1H1HAW1B0", "539494") + types = mapping_info.available_types() + + assert len(types.get("sensor", [])) == 12 + assert len(types.get("binary_sensor", [])) == 6 + assert len(types.get("number", [])) == 4 + assert len(types.get("switch", [])) == 3 + assert len(types.get("select", [])) == 1 From 089d4be633fc106442bc681af81c886ac7a688ee Mon Sep 17 00:00:00 2001 From: Lukas <12813107+lmaertin@users.noreply.github.com> Date: Fri, 29 May 2026 23:20:46 +0200 Subject: [PATCH 02/10] Mapping cleanup: Use PDPH1H1HAW100 mapping for PDPH1H1HAW1B0 via alias, remove redundant mapping file --- src/pooldose/constants.py | 1 + .../model_PDPH1H1HAW1B0_FW539494.json | 140 ------------------ 2 files changed, 1 insertion(+), 140 deletions(-) delete mode 100644 src/pooldose/mappings/model_PDPH1H1HAW1B0_FW539494.json diff --git a/src/pooldose/constants.py b/src/pooldose/constants.py index 4fdf582..f9f2a64 100644 --- a/src/pooldose/constants.py +++ b/src/pooldose/constants.py @@ -9,6 +9,7 @@ "PDHC1H1HAR1V1": "PDPR1H1HAR1V0", "PDHC1H1HAR1V0": "PDPR1H1HAR1V0", "PDPR1H1HAW102": "PDPR1H1HAW100", + "PDPH1H1HAW1B0": "PDPH1H1HAW100", # Alias für Mapping-Wiederverwendung } # Default device info structure diff --git a/src/pooldose/mappings/model_PDPH1H1HAW1B0_FW539494.json b/src/pooldose/mappings/model_PDPH1H1HAW1B0_FW539494.json deleted file mode 100644 index d61a57c..0000000 --- a/src/pooldose/mappings/model_PDPH1H1HAW1B0_FW539494.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "temperature": { - "key": "w_1eomiide3", - "type": "sensor" - }, - "ph": { - "key": "w_1ekeigkin", - "type": "sensor" - }, - "orp": { - "key": "w_1eklenb23", - "type": "sensor" - }, - "ph_type_dosing": { - "key": "w_1eklg44ro", - "type": "sensor", - "conversion": { - "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklg44ro_ALCALYNE|": "alcalyne", - "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklg44ro_ACID|": "acid" - } - }, - "peristaltic_ph_dosing": { - "key": "w_1eklj6euj", - "type": "sensor", - "conversion": { - "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklj6euj_OFF|": "off", - "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklj6euj_PROPORTIONAL|": "proportional", - "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklj6euj_ON_OFF|": "on / off", - "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklj6euj_TIMED|": "timed" - } - }, - "orp_type_dosing": { - "key": "w_1eklgnolb", - "type": "sensor", - "conversion": { - "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklgnolb_LOW|": "low", - "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklgnolb_HIGH|": "high" - } - }, - "peristaltic_orp_dosing": { - "key": "w_1eklj12vv", - "type": "sensor", - "conversion": { - "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklj12vv_OFF|": "off", - "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklj12vv_PROPORTIONAL|": "proportional", - "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklj12vv_ON_OFF|": "on / off", - "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklj12vv_TIMED|": "timed" - } - }, - "ph_calibration_type": { - "key": "w_1eklh8gb7", - "type": "sensor", - "conversion": { - "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklh8gb7_OFF|": "off", - "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklh8gb7_REFERENCE|": "reference", - "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklh8gb7_1_POINT|": "1_point", - "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklh8gb7_2_POINTS|": "2_points" - } - }, - "ph_calibration_offset": { - "key": "w_1eklhs3b4", - "type": "sensor" - }, - "ph_calibration_slope": { - "key": "w_1eklhs65u", - "type": "sensor" - }, - "orp_calibration_slope": { - "key": "w_1eklhsase", - "type": "sensor" - }, - "ofa_value": { - "key": "w_1eklhj58e", - "type": "sensor" - }, - "pump_alarm": { - "key": "w_1ekga097n", - "type": "binary_sensor" - }, - "ph_level_alarm": { - "key": "w_1eklf77pm", - "type": "binary_sensor" - }, - "relay_alarm": { - "key": "w_1eklffdl0", - "type": "binary_sensor" - }, - "relay_aux": { - "key": "w_1eklfgs1t", - "type": "binary_sensor" - }, - "alarm_ofa_ph": { - "key": "w_1eklfb73r", - "type": "binary_sensor" - }, - "alarm_ofa_orp": { - "key": "w_1eklfb8ob", - "type": "binary_sensor" - }, - "ph_target": { - "key": "w_1ekeiqfat", - "type": "number" - }, - "time_off_ph_dosing": { - "key": "w_1eklj30b7", - "type": "number" - }, - "power_on_delay_timer": { - "key": "w_1ffnkkn14", - "type": "number" - }, - "flow_delay_timer": { - "key": "w_1ffnkkp29", - "type": "number" - }, - "pause_dosing": { - "key": "w_1emtltkel", - "type": "switch" - }, - "pump_monitoring": { - "key": "w_1eklft47q", - "type": "switch" - }, - "frequency_input": { - "key": "w_1eklft5qt", - "type": "switch" - }, - "water_meter_unit": { - "key": "w_1eklinki6", - "type": "select", - "options": { - "0": "PDPH1H1HAW1B0_FW539494_COMBO_w_1eklinki6_M_", - "1": "PDPH1H1HAW1B0_FW539494_COMBO_w_1eklinki6_LITER" - }, - "conversion": { - "PDPH1H1HAW1B0_FW539494_COMBO_w_1eklinki6_M_": "m3", - "PDPH1H1HAW1B0_FW539494_COMBO_w_1eklinki6_LITER": "L" - } - } -} \ No newline at end of file From 615d44577575396c07fe70e355b0d816940c6b85 Mon Sep 17 00:00:00 2001 From: Lukas <12813107+lmaertin@users.noreply.github.com> Date: Fri, 29 May 2026 23:26:04 +0200 Subject: [PATCH 03/10] removed additional tests, alias tests done already --- src/pooldose/constants.py | 2 +- tests/test_mapping_info.py | 34 ---------------------------------- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/src/pooldose/constants.py b/src/pooldose/constants.py index f9f2a64..33ec9bc 100644 --- a/src/pooldose/constants.py +++ b/src/pooldose/constants.py @@ -9,7 +9,7 @@ "PDHC1H1HAR1V1": "PDPR1H1HAR1V0", "PDHC1H1HAR1V0": "PDPR1H1HAR1V0", "PDPR1H1HAW102": "PDPR1H1HAW100", - "PDPH1H1HAW1B0": "PDPH1H1HAW100", # Alias für Mapping-Wiederverwendung + "PDPH1H1HAW1B0": "PDPH1H1HAW100", } # Default device info structure diff --git a/tests/test_mapping_info.py b/tests/test_mapping_info.py index ad37d7b..d187fe0 100644 --- a/tests/test_mapping_info.py +++ b/tests/test_mapping_info.py @@ -192,37 +192,3 @@ async def test_double_spa_entity_counts(self): assert len(types.get("number", [])) == 11 assert len(types.get("switch", [])) == 3 assert len(types.get("select", [])) == 1 - - -class TestMedoConnectMapping: - """Tests for the BWT MEDO CONNECT Wi-Fi mapping file (PDPH1H1HAW1B0_FW539494).""" - - @pytest.mark.asyncio - async def test_load_medo_connect_mapping(self): - """Test that the MEDO CONNECT mapping file loads successfully.""" - mapping_info = await MappingInfo.load("PDPH1H1HAW1B0", "539494") - assert mapping_info.status == RequestStatus.SUCCESS - assert mapping_info.mapping is not None - - @pytest.mark.asyncio - async def test_medo_connect_conversion_prefixes(self): - """Test conversion entries use the expected model/firmware label prefix.""" - mapping_info = await MappingInfo.load("PDPH1H1HAW1B0", "539494") - sensors = mapping_info.available_sensors() - - ph_type = sensors["ph_type_dosing"] - assert ph_type.conversion is not None - assert "|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklg44ro_ACID|" in ph_type.conversion - assert ph_type.conversion["|PDPH1H1HAW1B0_FW539494_LABEL_w_1eklg44ro_ACID|"] == "acid" - - @pytest.mark.asyncio - async def test_medo_connect_entity_counts(self): - """Test the total entity counts for the MEDO CONNECT mapping.""" - mapping_info = await MappingInfo.load("PDPH1H1HAW1B0", "539494") - types = mapping_info.available_types() - - assert len(types.get("sensor", [])) == 12 - assert len(types.get("binary_sensor", [])) == 6 - assert len(types.get("number", [])) == 4 - assert len(types.get("switch", [])) == 3 - assert len(types.get("select", [])) == 1 From 70ddf816f53f89317019c64e5dfb911a8dd2913d Mon Sep 17 00:00:00 2001 From: Lukas <12813107+lmaertin@users.noreply.github.com> Date: Mon, 25 May 2026 20:25:00 +0200 Subject: [PATCH 04/10] Update device language strings instructions Added note about adapting language variable for non-English configurations. --- docs/device-support.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/device-support.md b/docs/device-support.md index da477fe..14ce890 100644 --- a/docs/device-support.md +++ b/docs/device-support.md @@ -39,6 +39,8 @@ If your device is not yet supported, please help us by creating a GitHub issue a ```bash curl --location http:///api/v1/DWI/getDeviceLanguage --data-raw '{"DeviceId":"YOUR_DEVICE_ID","LANG":"en"}' -o strings.json ``` + Note: When you device is configred to another language then English, please adapt the variable to your needs, e.g. "LANG":"fr". + 2. **Optional: Run the analyzer and share the output:** - Run this command if you set up python-pooldose already: ```bash From 8e516af08a89965695ba77e4259e8e074f76c42b Mon Sep 17 00:00:00 2001 From: Lukas <12813107+lmaertin@users.noreply.github.com> Date: Thu, 28 May 2026 08:19:07 +0200 Subject: [PATCH 05/10] Clarify DeviceId retrieval in device support documentation Updated instructions for obtaining DeviceId to include DID from debuginfo.json. --- docs/device-support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/device-support.md b/docs/device-support.md index 14ce890..f31d730 100644 --- a/docs/device-support.md +++ b/docs/device-support.md @@ -24,7 +24,7 @@ If your device is not yet supported, please help us by creating a GitHub issue a 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: + - Replace the IP address and DeviceId (get the id from the header of the instantvalues.json file or DID from debuginfo.json, e.g., '012345679_DEVICE') as needed. The postfix '_DEVICE' is mandatory: - Download debug config info: ```bash From 0ff430c6225584616461497bae99dc2bc511dff9 Mon Sep 17 00:00:00 2001 From: Lukas <12813107+lmaertin@users.noreply.github.com> Date: Fri, 29 May 2026 22:49:36 +0200 Subject: [PATCH 06/10] Add get_cloud_status() and get_wifi_rssi() methods, update docs and tests, refactor code (pylint/mypy clean) --- CHANGELOG.md | 7 ++++ README.md | 5 +++ docs/api-reference.md | 2 ++ src/pooldose/__init__.py | 2 +- src/pooldose/request_handler.py | 58 +++++++++++++++++++++++++++++++++ tests/test_request_handler.py | 46 ++++++++++++++++++++++++++ 6 files changed, 119 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f9a6c..1b4f7a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ 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.9.3] - 2026-05-29 + +### Added + +- `get_cloud_status()` for cloud connection status +- `get_wifi_rssi()` for WiFi signal strength + ## [0.9.1] - 2026-05-11 ## Changed diff --git a/README.md b/README.md index 34a733e..6a6409e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ This client uses an undocumented local HTTP API. It provides live readings for p > **Disclaimer:** Use at your own risk. No liability for damages or malfunctions. + ## Features - **Async/await support** for non-blocking operations @@ -24,6 +25,10 @@ This client uses an undocumented local HTTP API. It provides live readings for p - **Command-line interface** for direct device interaction and testing - **Secure by default** - WiFi passwords excluded unless explicitly requested - **Comprehensive error handling** with detailed logging +- **Cloud connection** status +- **WiFi RSSI** signal + +Each method queries the device live and returns the current value. - **SSL/HTTPS support** for secure communication ## Prerequisites diff --git a/docs/api-reference.md b/docs/api-reference.md index efc56ef..4f76c17 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -24,6 +24,8 @@ PooldoseClient(host, timeout=30, *, websession=None, include_sensitive_data=Fals ### Methods - `async connect()` → `RequestStatus` - Connect to device and initialize all components +- `async get_cloud_status()` → `Optional[bool]` — Retrieve the current cloud connection status +- `async get_wifi_rssi()` → `Optional[int]` — Retrieve the current WiFi RSSI (signal strength) - `static_values()` → `tuple[RequestStatus, StaticValues | None]` - Get static device information - `async instant_values()` → `tuple[RequestStatus, InstantValues | None]` - Get current sensor readings and device state - `async instant_values_structured()` → `tuple[RequestStatus, dict[str, Any]]` - Get structured data organized by type diff --git a/src/pooldose/__init__.py b/src/pooldose/__init__.py index 49caa91..ea9928a 100644 --- a/src/pooldose/__init__.py +++ b/src/pooldose/__init__.py @@ -1,5 +1,5 @@ """Async API client for SEKO Pooldose.""" from .client import PooldoseClient -__version__ = "0.9.1" +__version__ = "0.9.3" __all__ = ["PooldoseClient"] diff --git a/src/pooldose/request_handler.py b/src/pooldose/request_handler.py index 948c269..44704c5 100644 --- a/src/pooldose/request_handler.py +++ b/src/pooldose/request_handler.py @@ -1,3 +1,5 @@ +# + """Request Handler for async API client for SEKO Pooldose.""" import asyncio @@ -8,6 +10,8 @@ from typing import Any, Optional, Tuple, Union, List, Dict import aiohttp +import websockets +import websockets.exceptions from pooldose.type_definitions import ( AccessPointDict, @@ -541,3 +545,57 @@ async def reboot_device(self): except (aiohttp.ClientError, asyncio.TimeoutError) as err: _LOGGER.warning("Error sending reboot command: %s", err) return RequestStatus.UNKNOWN_ERROR, False + + async def _get_websocket_data(self, topic_filter: Union[str, List[str]], value_path: List[str]) -> Optional[Any]: + """ + Open a WebSocket connection and extract a value from the first matching topic. + + Args: + topic_filter: Topic name or list of topic names to match. + value_path: List of keys to traverse in the data dict to extract the value. + + Returns: + The extracted value or None if not found or error. + """ + url = f"ws://{self.host}:1334" + try: + async with websockets.connect(url) as ws: + while True: + msg = await ws.recv() + try: + data = json.loads(msg) + except json.JSONDecodeError: + continue + topic = data.get("topic") + if (isinstance(topic_filter, str) and topic == topic_filter) or ( + isinstance(topic_filter, list) and topic in topic_filter + ): + val = data.get("data", {}) + for key in value_path: + if isinstance(val, dict): + val = val.get(key) + else: + return None + return val + except (OSError, websockets.exceptions.WebSocketException) as err: + _LOGGER.error("WebSocket error: %s", err) + return None + + + async def get_cloud_status(self) -> Optional[bool]: + """ + Retrieve the current cloud connection status (wdp_status) live via WebSocket. + + Returns: + Optional[bool]: Cloud connection status (True/False/None) + """ + return await self._get_websocket_data(["wdp_status", "wdp_connection"], ["connection"]) + + async def get_wifi_rssi(self) -> Optional[int]: + """ + Retrieve the current WiFi RSSI (signal strength) live via WebSocket. + + Returns: + Optional[int]: WiFi RSSI (int/None) + """ + return await self._get_websocket_data("wifi_station", ["rssi"]) diff --git a/tests/test_request_handler.py b/tests/test_request_handler.py index aa1e524..0ef4282 100644 --- a/tests/test_request_handler.py +++ b/tests/test_request_handler.py @@ -1,6 +1,7 @@ """Tests for RequestHandler for Async API client for SEKO Pooldose.""" from unittest.mock import AsyncMock, MagicMock, patch +import json import asyncio import aiohttp import pytest @@ -576,3 +577,48 @@ async def test_get_wifi_station_timeout_error(self): assert status == RequestStatus.UNKNOWN_ERROR assert data is None + +class TestWebsocketParsing: + """Tests for the get_cloud_status() and get_wifi_rssi() parsing.""" + + @pytest.mark.asyncio + async def test_get_cloud_status_success(self): + """Test successful retrieval of cloud connection via WebSocket.""" + handler = RequestHandler("localhost") + ws_mock = AsyncMock() + ws_instance = AsyncMock() + wdp_msg = json.dumps({"topic": "wdp_status", "data": {"connection": True}}) + ws_instance.recv = AsyncMock(side_effect=[wdp_msg]) + ws_mock.__aenter__.return_value = ws_instance + with patch("websockets.connect", return_value=ws_mock): + status = await handler.get_cloud_status() + assert status is True + + @pytest.mark.asyncio + async def test_get_cloud_status_error(self): + """Test error case when WebSocket connection fails for cloud status.""" + handler = RequestHandler("localhost") + with patch("websockets.connect", side_effect=OSError("Verbindungsfehler")): + status = await handler.get_cloud_status() + assert status is None + + @pytest.mark.asyncio + async def test_get_wifi_rssi_success(self): + """Test successful retrieval of WiFi RSSI via WebSocket.""" + handler = RequestHandler("localhost") + ws_mock = AsyncMock() + ws_instance = AsyncMock() + wifi_msg = json.dumps({"topic": "wifi_station", "data": {"rssi": -42}}) + ws_instance.recv = AsyncMock(side_effect=[wifi_msg]) + ws_mock.__aenter__.return_value = ws_instance + with patch("websockets.connect", return_value=ws_mock): + rssi = await handler.get_wifi_rssi() + assert rssi == -42 + + @pytest.mark.asyncio + async def test_get_wifi_rssi_error(self): + """Test error case when WebSocket connection fails for WiFi RSSI.""" + handler = RequestHandler("localhost") + with patch("websockets.connect", side_effect=OSError("Verbindungsfehler")): + rssi = await handler.get_wifi_rssi() + assert rssi is None From 10b2e24a68d83f3ff15f3b9ecfc18953fe14392b Mon Sep 17 00:00:00 2001 From: Lukas <12813107+lmaertin@users.noreply.github.com> Date: Fri, 29 May 2026 22:52:18 +0200 Subject: [PATCH 07/10] added deps --- pyproject.toml | 8 +++++++- requirements.txt | 4 +++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 690f57e..b7f3e85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,13 @@ authors = [{ name = "Lukas Maertin", email = "pypi@lukas-maertin.de" }] license = "MIT" readme = "README.md" requires-python = ">=3.11" -dependencies = ["aiohttp", "aiofiles", "getmac"] +dependencies = [ + "aiohttp", + "aiofiles", + "getmac", + "websockets", + "websockets.exceptions", +] [project.urls] Homepage = "https://github.com/lmaertin/python-pooldose" diff --git a/requirements.txt b/requirements.txt index 633be76..d98df0c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,6 @@ aiofiles types-aiofiles getmac pytest -pytest-asyncio \ No newline at end of file +pytest-asyncio +websockets +websockets.exceptions \ No newline at end of file From a61115df3ea58a708a58fe43bc29f9eae8598ab0 Mon Sep 17 00:00:00 2001 From: Lukas <12813107+lmaertin@users.noreply.github.com> Date: Fri, 29 May 2026 22:53:41 +0200 Subject: [PATCH 08/10] fixed deps --- pyproject.toml | 8 +------- requirements.txt | 3 +-- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b7f3e85..54b89f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,13 +6,7 @@ authors = [{ name = "Lukas Maertin", email = "pypi@lukas-maertin.de" }] license = "MIT" readme = "README.md" requires-python = ">=3.11" -dependencies = [ - "aiohttp", - "aiofiles", - "getmac", - "websockets", - "websockets.exceptions", -] +dependencies = ["aiohttp", "aiofiles", "getmac", "websockets"] [project.urls] Homepage = "https://github.com/lmaertin/python-pooldose" diff --git a/requirements.txt b/requirements.txt index d98df0c..9b12420 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,4 @@ types-aiofiles getmac pytest pytest-asyncio -websockets -websockets.exceptions \ No newline at end of file +websockets \ No newline at end of file From 0a8a678202fce0de795dbb5d8ace5e246eb970e5 Mon Sep 17 00:00:00 2001 From: Lukas <12813107+lmaertin@users.noreply.github.com> Date: Fri, 29 May 2026 22:58:19 +0200 Subject: [PATCH 09/10] add cloudstatus/rssi request for demo.py --- examples/demo.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/demo.py b/examples/demo.py index 4201956..79b452b 100644 --- a/examples/demo.py +++ b/examples/demo.py @@ -48,8 +48,16 @@ async def main() -> None: if client_status != RequestStatus.SUCCESS: print(f"Error connecting to PooldoseClient: {client_status}") return + print("Connected to Pooldose device.") + # Fetch and display cloud status and WiFi RSSI (real client only, but mock can return fixed values if desired) + cloud_status = await client.request_handler.get_cloud_status() + print(f"\nCloud-Status (wdp_status): {cloud_status}") + + wifi_rssi = await client.request_handler.get_wifi_rssi() + print(f"WiFi RSSI: {wifi_rssi}") + # Fetch and display static values print("\nFetching static values...") static_values_status, static_values = client.static_values() From c9ced7b616ca5e80f3a387c8083f32b2f4711d4c Mon Sep 17 00:00:00 2001 From: Lukas <12813107+lmaertin@users.noreply.github.com> Date: Fri, 29 May 2026 23:35:32 +0200 Subject: [PATCH 10/10] Release 0.9.4: add BWT MEDO CONNECT alias, improve rebrand support, mapping cleanup --- CHANGELOG.md | 6 ++++++ src/pooldose/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b4f7a2..c91504e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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.9.4] - 2026-05-29 + +### Added + +- Support for BWT MEDO CONNECT Wi-Fi (PDPH1H1HAW1B0) via alias to SEKO PoolDose pH mapping + ## [0.9.3] - 2026-05-29 ### Added diff --git a/src/pooldose/__init__.py b/src/pooldose/__init__.py index ea9928a..ae53aee 100644 --- a/src/pooldose/__init__.py +++ b/src/pooldose/__init__.py @@ -1,5 +1,5 @@ """Async API client for SEKO Pooldose.""" from .client import PooldoseClient -__version__ = "0.9.3" +__version__ = "0.9.4" __all__ = ["PooldoseClient"]