violet-poolController-api 0.0.6__tar.gz → 0.0.10__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 (19) hide show
  1. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/PKG-INFO +20 -1
  2. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/README.md +143 -124
  3. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/pyproject.toml +1 -1
  4. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/setup.py +1 -1
  5. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/tests/test_api.py +200 -87
  6. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/violet_poolController_api.egg-info/PKG-INFO +20 -1
  7. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/violet_poolcontroller_api/api.py +1055 -947
  8. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/violet_poolcontroller_api/utils_rate_limiter.py +9 -13
  9. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/violet_poolcontroller_api/utils_sanitizer.py +12 -9
  10. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/LICENSE +0 -0
  11. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/setup.cfg +0 -0
  12. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/violet_poolController_api.egg-info/SOURCES.txt +0 -0
  13. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/violet_poolController_api.egg-info/dependency_links.txt +0 -0
  14. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/violet_poolController_api.egg-info/requires.txt +0 -0
  15. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/violet_poolController_api.egg-info/top_level.txt +0 -0
  16. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/violet_poolcontroller_api/__init__.py +0 -0
  17. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/violet_poolcontroller_api/circuit_breaker.py +0 -0
  18. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/violet_poolcontroller_api/const_api.py +0 -0
  19. {violet_poolcontroller_api-0.0.6 → violet_poolcontroller_api-0.0.10}/violet_poolcontroller_api/const_devices.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: violet-poolController-api
3
- Version: 0.0.6
3
+ Version: 0.0.10
4
4
  Summary: Asynchronous Python client for the Violet Pool Controller.
5
5
  Home-page: https://github.com/Xerolux/violet-poolController-api
6
6
  Author: Basti (Xerolux)
@@ -133,6 +133,25 @@ api = VioletPoolAPI(
133
133
 
134
134
  In this mode, dosing functions (for example `manual_dosing` and dosing parameter/target updates) stay available, while base-module-only switch functions (for example pump/light/backwash) are blocked with a clear error message.
135
135
 
136
+ **Note on getReadings format:**
137
+ As of version `0.0.7`, the API client automatically detects and normalizes the payload output from the controller. Whether your Violet Controller returns the classic base-module `dict` structure (`{"PUMPSTATE": "2", "PH": 7.2}`) or the new standalone `list` structure, the `get_readings()` and `get_specific_readings()` functions will always return a seamless, flattened key-value dictionary. Your Home Assistant integration or downstream application will work uniformly with both formats without requiring any extra code!
138
+
139
+ **Hardware Profile Detection:**
140
+ As of the latest release, the API client provides a method to detect the specific hardware configuration of your Violet Controller.
141
+ The API automatically detects the connected modules and updates internal states based on the available readings.
142
+ ```python
143
+ profile = await api.get_hardware_profile()
144
+ print(profile)
145
+ # Output example:
146
+ # {
147
+ # "base_module": True,
148
+ # "dosing_module": True,
149
+ # "extension_module_1": True,
150
+ # "extension_module_2": False,
151
+ # }
152
+ ```
153
+ This detection parses `get_readings()` to check for the presence of certain internal status parameters (`SYSTEM_dosagemodule_cpu_temperature`, `EXT1_1`, `EXT2_1`), allowing your application to dynamically adapt to the connected modules (Base Module, Dosing Module, Relay Extension 1 and 2). By utilizing this detection, developers and integrations can accurately filter out features for missing hardware, ensuring that only supported options are exposed to the user.
154
+
136
155
  ## License
137
156
  GNU Affero General Public License v3.0 or later (AGPLv3+)
138
157
 
@@ -1,124 +1,143 @@
1
- # Violet Pool Controller API
2
-
3
- [![PyPI version](https://img.shields.io/pypi/v/violet-poolController-api.svg?style=for-the-badge)](https://pypi.org/project/violet-poolController-api/)
4
- [![PyPI downloads](https://img.shields.io/pypi/dm/violet-poolController-api.svg?style=for-the-badge)](https://pypistats.org/packages/violet-poolcontroller-api)
5
- [![Python versions](https://img.shields.io/pypi/pyversions/violet-poolController-api.svg?style=for-the-badge)](https://pypi.org/project/violet-poolController-api/)
6
- [![License: AGPL v3+](https://img.shields.io/badge/License-AGPL_v3%2B-blue?style=for-the-badge)](LICENSE)
7
-
8
- [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-xerolux-yellow?logo=buy-me-a-coffee&style=for-the-badge)](https://www.buymeacoffee.com/xerolux)
9
- [![Tesla](https://img.shields.io/badge/Tesla-Referral-red?style=for-the-badge&logo=tesla)](https://ts.la/sebastian564489)
10
-
11
- An asynchronous Python client for interacting with the **Violet Pool Controller**.
12
-
13
- This library is primarily designed to power the official [Violet Pool Controller Home Assistant Integration](https://github.com/Xerolux/violet-hass), but it can be used independently for any Python project that needs to fetch readings or control a Violet Pool system.
14
-
15
- > **📖 Documentation:**
16
- > - GitHub Pages: https://xerolux.github.io/violet-poolController-api/
17
- > - GitHub Wiki: https://github.com/Xerolux/violet-poolController-api/wiki
18
- >
19
- > The `docs/` directory is the single source of truth and is used for both GitHub Pages and Wiki sync.
20
-
21
- ## Features
22
- * **Asynchronous:** Fully async operations using `aiohttp`.
23
- * **Resilient:** Built-in Circuit Breaker and Rate Limiter to protect both the client and the controller from overload.
24
- * **Sanitization:** Strict payload input sanitization to prevent injection and invalid settings.
25
-
26
- ## Installation
27
-
28
- ```bash
29
- pip install violet-poolController-api
30
- ```
31
-
32
- ## Basic Usage
33
-
34
- ```python
35
- import asyncio
36
- import aiohttp
37
- from violet_poolcontroller_api.api import VioletPoolAPI, VioletPoolAPIError
38
-
39
- async def main():
40
- # Create an aiohttp ClientSession
41
- async with aiohttp.ClientSession() as session:
42
- # Initialize the API
43
- # Note: In a standard setup, just enter the IP address without a port.
44
- # A port (e.g. "192.168.1.100:8080") can optionally be provided if you use a proxy or alternative setup.
45
- api = VioletPoolAPI(
46
- host="192.168.1.100",
47
- username="admin",
48
- password="your_password",
49
- session=session,
50
- dosing_standalone=False, # True for Violet dosing standalone setups
51
- )
52
-
53
- try:
54
- # --- 1. Fetch current sensor readings ---
55
- readings = await api.get_readings()
56
- print("Current Pool Readings:")
57
- print(readings)
58
-
59
- # --- 2. Control the Filter Pump ---
60
- # Set pump speed to 2 (Normal) permanently (duration=0)
61
- await api.set_pump_speed(speed=2, duration=0)
62
- print("\nPump speed set to 2.")
63
-
64
- # --- 3. Set Target Temperature ---
65
- # Set the target temperature for the heater to 28.5 degrees
66
- await api.set_device_temperature("HEATER", 28.5)
67
- print("\nHeater target temperature set to 28.5°C.")
68
-
69
- # --- 4. Control Pool Lights ---
70
- # Trigger the color pulse animation for the pool light
71
- await api.set_light_color_pulse()
72
- print("\nLight color pulse triggered.")
73
-
74
- except VioletPoolAPIError as e:
75
- print(f"An error occurred while communicating with the Violet controller: {e}")
76
-
77
- if __name__ == "__main__":
78
- asyncio.run(main())
79
- ```
80
-
81
- ## Advanced Operations
82
-
83
- The API client includes many more functions tailored to the Violet Controller:
84
- - `get_config(["PUMP_SPEED_1", "PUMP_SPEED_2"])`: Fetch specific configuration values.
85
- - `set_ph_target(7.2)`: Change the pH target value.
86
- - `set_orp_target(750)`: Change the ORP (Redox) target value.
87
- - `set_pv_surplus(active=True)`: Enable the PV-Surplus mode.
88
- - `manual_dosing(dosing_type="Chlor", duration=120)`: Trigger manual chemical dosing.
89
-
90
- For a full list of available commands and more detailed examples, please refer to the [Wiki](https://github.com/Xerolux/violet-poolController-api/wiki) or the source code in `api.py`.
91
-
92
- ## Violet Dosing Standalone Mode
93
-
94
- If your Violet setup runs as dosing standalone (without the base module), enable:
95
-
96
- ```python
97
- api = VioletPoolAPI(
98
- host="192.168.1.100",
99
- username="admin",
100
- password="your_password",
101
- session=session,
102
- dosing_standalone=True,
103
- )
104
- ```
105
-
106
- In this mode, dosing functions (for example `manual_dosing` and dosing parameter/target updates) stay available, while base-module-only switch functions (for example pump/light/backwash) are blocked with a clear error message.
107
-
108
- ## License
109
- GNU Affero General Public License v3.0 or later (AGPLv3+)
110
-
111
- ---
112
-
113
- ## About the Violet Pool Controller
114
-
115
- Der **VIOLET Pool Controller** von [PoolDigital GmbH & Co. KG](https://www.pooldigital.de/) ist ein Premium Smart Pool Automation System aus deutscher Entwicklung – mit JSON API für nahtlose Home Assistant Integration.
116
-
117
- - **Offizieller Shop:** [pooldigital.de](https://www.pooldigital.de/)
118
- - **Community:** [PoolDigital Forum](http://forum.pooldigital.de/)
119
-
120
- **Disclaimer:**
121
- *This is an unofficial, community-driven project. It is not affiliated with, endorsed by, or associated with PoolDigital GmbH & Co. KG in any way. "VIOLET" and any related trademarks are the property of their respective owners.*
122
-
123
- ⚠️ **WARNING - USE AT YOUR OWN RISK:**
124
- *This software interacts with physical hardware and automation systems that control water chemistry (pH, Chlorine/ORP) and electrical equipment (pumps, heaters). A bug, network issue, or incorrect configuration could result in hardware damage, unsafe water conditions, or other hazards. By using this software, you acknowledge and agree that you are solely responsible for any damage, injury, or loss of property that may occur. Please always monitor your pool's chemistry and hardware independently.*
1
+ # Violet Pool Controller API
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/violet-poolController-api.svg?style=for-the-badge)](https://pypi.org/project/violet-poolController-api/)
4
+ [![PyPI downloads](https://img.shields.io/pypi/dm/violet-poolController-api.svg?style=for-the-badge)](https://pypistats.org/packages/violet-poolcontroller-api)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/violet-poolController-api.svg?style=for-the-badge)](https://pypi.org/project/violet-poolController-api/)
6
+ [![License: AGPL v3+](https://img.shields.io/badge/License-AGPL_v3%2B-blue?style=for-the-badge)](LICENSE)
7
+
8
+ [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-xerolux-yellow?logo=buy-me-a-coffee&style=for-the-badge)](https://www.buymeacoffee.com/xerolux)
9
+ [![Tesla](https://img.shields.io/badge/Tesla-Referral-red?style=for-the-badge&logo=tesla)](https://ts.la/sebastian564489)
10
+
11
+ An asynchronous Python client for interacting with the **Violet Pool Controller**.
12
+
13
+ This library is primarily designed to power the official [Violet Pool Controller Home Assistant Integration](https://github.com/Xerolux/violet-hass), but it can be used independently for any Python project that needs to fetch readings or control a Violet Pool system.
14
+
15
+ > **📖 Documentation:**
16
+ > - GitHub Pages: https://xerolux.github.io/violet-poolController-api/
17
+ > - GitHub Wiki: https://github.com/Xerolux/violet-poolController-api/wiki
18
+ >
19
+ > The `docs/` directory is the single source of truth and is used for both GitHub Pages and Wiki sync.
20
+
21
+ ## Features
22
+ * **Asynchronous:** Fully async operations using `aiohttp`.
23
+ * **Resilient:** Built-in Circuit Breaker and Rate Limiter to protect both the client and the controller from overload.
24
+ * **Sanitization:** Strict payload input sanitization to prevent injection and invalid settings.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install violet-poolController-api
30
+ ```
31
+
32
+ ## Basic Usage
33
+
34
+ ```python
35
+ import asyncio
36
+ import aiohttp
37
+ from violet_poolcontroller_api.api import VioletPoolAPI, VioletPoolAPIError
38
+
39
+ async def main():
40
+ # Create an aiohttp ClientSession
41
+ async with aiohttp.ClientSession() as session:
42
+ # Initialize the API
43
+ # Note: In a standard setup, just enter the IP address without a port.
44
+ # A port (e.g. "192.168.1.100:8080") can optionally be provided if you use a proxy or alternative setup.
45
+ api = VioletPoolAPI(
46
+ host="192.168.1.100",
47
+ username="admin",
48
+ password="your_password",
49
+ session=session,
50
+ dosing_standalone=False, # True for Violet dosing standalone setups
51
+ )
52
+
53
+ try:
54
+ # --- 1. Fetch current sensor readings ---
55
+ readings = await api.get_readings()
56
+ print("Current Pool Readings:")
57
+ print(readings)
58
+
59
+ # --- 2. Control the Filter Pump ---
60
+ # Set pump speed to 2 (Normal) permanently (duration=0)
61
+ await api.set_pump_speed(speed=2, duration=0)
62
+ print("\nPump speed set to 2.")
63
+
64
+ # --- 3. Set Target Temperature ---
65
+ # Set the target temperature for the heater to 28.5 degrees
66
+ await api.set_device_temperature("HEATER", 28.5)
67
+ print("\nHeater target temperature set to 28.5°C.")
68
+
69
+ # --- 4. Control Pool Lights ---
70
+ # Trigger the color pulse animation for the pool light
71
+ await api.set_light_color_pulse()
72
+ print("\nLight color pulse triggered.")
73
+
74
+ except VioletPoolAPIError as e:
75
+ print(f"An error occurred while communicating with the Violet controller: {e}")
76
+
77
+ if __name__ == "__main__":
78
+ asyncio.run(main())
79
+ ```
80
+
81
+ ## Advanced Operations
82
+
83
+ The API client includes many more functions tailored to the Violet Controller:
84
+ - `get_config(["PUMP_SPEED_1", "PUMP_SPEED_2"])`: Fetch specific configuration values.
85
+ - `set_ph_target(7.2)`: Change the pH target value.
86
+ - `set_orp_target(750)`: Change the ORP (Redox) target value.
87
+ - `set_pv_surplus(active=True)`: Enable the PV-Surplus mode.
88
+ - `manual_dosing(dosing_type="Chlor", duration=120)`: Trigger manual chemical dosing.
89
+
90
+ For a full list of available commands and more detailed examples, please refer to the [Wiki](https://github.com/Xerolux/violet-poolController-api/wiki) or the source code in `api.py`.
91
+
92
+ ## Violet Dosing Standalone Mode
93
+
94
+ If your Violet setup runs as dosing standalone (without the base module), enable:
95
+
96
+ ```python
97
+ api = VioletPoolAPI(
98
+ host="192.168.1.100",
99
+ username="admin",
100
+ password="your_password",
101
+ session=session,
102
+ dosing_standalone=True,
103
+ )
104
+ ```
105
+
106
+ In this mode, dosing functions (for example `manual_dosing` and dosing parameter/target updates) stay available, while base-module-only switch functions (for example pump/light/backwash) are blocked with a clear error message.
107
+
108
+ **Note on getReadings format:**
109
+ As of version `0.0.7`, the API client automatically detects and normalizes the payload output from the controller. Whether your Violet Controller returns the classic base-module `dict` structure (`{"PUMPSTATE": "2", "PH": 7.2}`) or the new standalone `list` structure, the `get_readings()` and `get_specific_readings()` functions will always return a seamless, flattened key-value dictionary. Your Home Assistant integration or downstream application will work uniformly with both formats without requiring any extra code!
110
+
111
+ **Hardware Profile Detection:**
112
+ As of the latest release, the API client provides a method to detect the specific hardware configuration of your Violet Controller.
113
+ The API automatically detects the connected modules and updates internal states based on the available readings.
114
+ ```python
115
+ profile = await api.get_hardware_profile()
116
+ print(profile)
117
+ # Output example:
118
+ # {
119
+ # "base_module": True,
120
+ # "dosing_module": True,
121
+ # "extension_module_1": True,
122
+ # "extension_module_2": False,
123
+ # }
124
+ ```
125
+ This detection parses `get_readings()` to check for the presence of certain internal status parameters (`SYSTEM_dosagemodule_cpu_temperature`, `EXT1_1`, `EXT2_1`), allowing your application to dynamically adapt to the connected modules (Base Module, Dosing Module, Relay Extension 1 and 2). By utilizing this detection, developers and integrations can accurately filter out features for missing hardware, ensuring that only supported options are exposed to the user.
126
+
127
+ ## License
128
+ GNU Affero General Public License v3.0 or later (AGPLv3+)
129
+
130
+ ---
131
+
132
+ ## About the Violet Pool Controller
133
+
134
+ Der **VIOLET Pool Controller** von [PoolDigital GmbH & Co. KG](https://www.pooldigital.de/) ist ein Premium Smart Pool Automation System aus deutscher Entwicklung – mit JSON API für nahtlose Home Assistant Integration.
135
+
136
+ - **Offizieller Shop:** [pooldigital.de](https://www.pooldigital.de/)
137
+ - **Community:** [PoolDigital Forum](http://forum.pooldigital.de/)
138
+
139
+ **Disclaimer:**
140
+ *This is an unofficial, community-driven project. It is not affiliated with, endorsed by, or associated with PoolDigital GmbH & Co. KG in any way. "VIOLET" and any related trademarks are the property of their respective owners.*
141
+
142
+ ⚠️ **WARNING - USE AT YOUR OWN RISK:**
143
+ *This software interacts with physical hardware and automation systems that control water chemistry (pH, Chlorine/ORP) and electrical equipment (pumps, heaters). A bug, network issue, or incorrect configuration could result in hardware damage, unsafe water conditions, or other hazards. By using this software, you acknowledge and agree that you are solely responsible for any damage, injury, or loss of property that may occur. Please always monitor your pool's chemistry and hardware independently.*
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "violet-poolController-api"
7
- version = "0.0.6"
7
+ version = "0.0.10"
8
8
  authors = [
9
9
  { name="Basti (Xerolux)", email="git@xerolux.de" },
10
10
  ]
@@ -1,7 +1,7 @@
1
1
  from setuptools import setup, find_packages
2
2
  setup(
3
3
  name="violet-poolController-api",
4
- version="0.0.6",
4
+ version="0.0.10",
5
5
  author="Basti (Xerolux)",
6
6
  author_email="git@xerolux.de",
7
7
  description="Asynchronous Python client for the Violet Pool Controller.",
@@ -1,41 +1,41 @@
1
- # violet-poolController-api - API für Violet Pool Controller
2
- # Copyright (C) 2024–2026 Xerolux
3
- #
4
- # This program is free software: you can redistribute it and/or modify
5
- # it under the terms of the GNU Affero General Public License as published
6
- # by the Free Software Foundation, either version 3 of the License, or
7
- # (at your option) any later version.
8
- #
9
- # This program is distributed in the hope that it will be useful,
10
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
- # GNU Affero General Public License for more details.
13
- #
14
- # You should have received a copy of the GNU Affero General Public License
15
- # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
-
1
+ # violet-poolController-api - API für Violet Pool Controller
2
+ # Copyright (C) 2024–2026 Xerolux
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU Affero General Public License as published
6
+ # by the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU Affero General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Affero General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
17
  import aiohttp
18
18
  import pytest
19
19
  import pytest_asyncio
20
20
  from aioresponses import aioresponses
21
21
  from violet_poolcontroller_api.api import VioletPoolAPI, VioletPoolAPIError
22
22
  from violet_poolcontroller_api.circuit_breaker import CircuitBreakerOpenError
23
-
24
- @pytest.fixture
25
- def mock_aioresponse():
26
- with aioresponses() as m:
27
- yield m
28
-
23
+
24
+ @pytest.fixture
25
+ def mock_aioresponse():
26
+ with aioresponses() as m:
27
+ yield m
28
+
29
29
  @pytest_asyncio.fixture
30
30
  async def api_client():
31
- async with aiohttp.ClientSession() as session:
32
- # Pass low retry counts to make error tests faster
33
- api = VioletPoolAPI(
34
- host="192.168.1.100",
35
- session=session,
36
- username="admin",
37
- password="password",
38
- max_retries=1
31
+ async with aiohttp.ClientSession() as session:
32
+ # Pass low retry counts to make error tests faster
33
+ api = VioletPoolAPI(
34
+ host="192.168.1.100",
35
+ session=session,
36
+ username="admin",
37
+ password="password",
38
+ max_retries=1
39
39
  )
40
40
  yield api
41
41
 
@@ -53,64 +53,64 @@ async def standalone_api_client():
53
53
  )
54
54
  yield api
55
55
 
56
- @pytest.mark.asyncio
57
- async def test_get_readings_success(mock_aioresponse, api_client):
58
- """Test get_readings returns the correct parsed JSON dictionary."""
59
- url = "http://192.168.1.100/getReadings?ALL"
60
- mock_data = {"PUMPSTATE": "2", "PH": 7.2}
61
- mock_aioresponse.get(url, payload=mock_data, status=200)
62
-
63
- result = await api_client.get_readings()
64
-
65
- assert isinstance(result, dict)
66
- assert result == mock_data
67
-
68
- @pytest.mark.asyncio
69
- async def test_set_pump_speed_success(mock_aioresponse, api_client):
70
- """Test set_pump_speed formats the request correctly and returns success."""
71
- url = "http://192.168.1.100/setFunctionManually?PUMP,ON,0,2"
72
- mock_aioresponse.get(url, body="OK", status=200)
73
-
74
- result = await api_client.set_pump_speed(speed=2, duration=0)
75
-
76
- assert result["success"] is True
77
- assert result["response"] == "OK"
78
-
79
- @pytest.mark.asyncio
80
- async def test_request_server_error(mock_aioresponse, api_client):
81
- """Test that a 500 error raises VioletPoolAPIError after retrying."""
82
- url = "http://192.168.1.100/getReadings?ALL"
83
- mock_aioresponse.get(url, status=500)
84
- # the second time it retries
85
- mock_aioresponse.get(url, status=500)
86
-
87
- with pytest.raises(VioletPoolAPIError) as exc_info:
88
- await api_client.get_readings()
89
-
90
- assert "Error communicating with Violet controller" in str(exc_info.value)
91
-
92
- @pytest.mark.asyncio
93
- async def test_init_with_port():
94
- """Test initializing API with a port in the hostname."""
95
- async with aiohttp.ClientSession() as session:
96
- api = VioletPoolAPI(
97
- host="192.168.1.100:8080",
98
- session=session,
99
- username="admin",
100
- password="password"
101
- )
102
- assert api._base_url == "http://192.168.1.100:8080"
103
-
104
-
105
- @pytest.mark.asyncio
56
+ @pytest.mark.asyncio
57
+ async def test_get_readings_success(mock_aioresponse, api_client):
58
+ """Test get_readings returns the correct parsed JSON dictionary."""
59
+ url = "http://192.168.1.100/getReadings?ALL"
60
+ mock_data = {"PUMPSTATE": "2", "PH": 7.2}
61
+ mock_aioresponse.get(url, payload=mock_data, status=200)
62
+
63
+ result = await api_client.get_readings()
64
+
65
+ assert isinstance(result, dict)
66
+ assert result == mock_data
67
+
68
+ @pytest.mark.asyncio
69
+ async def test_set_pump_speed_success(mock_aioresponse, api_client):
70
+ """Test set_pump_speed formats the request correctly and returns success."""
71
+ url = "http://192.168.1.100/setFunctionManually?PUMP,ON,0,2"
72
+ mock_aioresponse.get(url, body="OK", status=200)
73
+
74
+ result = await api_client.set_pump_speed(speed=2, duration=0)
75
+
76
+ assert result["success"] is True
77
+ assert result["response"] == "OK"
78
+
79
+ @pytest.mark.asyncio
80
+ async def test_request_server_error(mock_aioresponse, api_client):
81
+ """Test that a 500 error raises VioletPoolAPIError after retrying."""
82
+ url = "http://192.168.1.100/getReadings?ALL"
83
+ mock_aioresponse.get(url, status=500)
84
+ # the second time it retries
85
+ mock_aioresponse.get(url, status=500)
86
+
87
+ with pytest.raises(VioletPoolAPIError) as exc_info:
88
+ await api_client.get_readings()
89
+
90
+ assert "Error communicating with Violet controller" in str(exc_info.value)
91
+
92
+ @pytest.mark.asyncio
93
+ async def test_init_with_port():
94
+ """Test initializing API with a port in the hostname."""
95
+ async with aiohttp.ClientSession() as session:
96
+ api = VioletPoolAPI(
97
+ host="192.168.1.100:8080",
98
+ session=session,
99
+ username="admin",
100
+ password="password"
101
+ )
102
+ assert api._base_url == "http://192.168.1.100:8080"
103
+
104
+
105
+ @pytest.mark.asyncio
106
106
  async def test_circuit_breaker_open_is_wrapped(api_client, monkeypatch):
107
- """Test circuit breaker open errors are exposed as VioletPoolAPIError."""
108
-
109
- async def raise_open(_func, *args, **kwargs):
110
- raise CircuitBreakerOpenError("Circuit breaker is OPEN")
111
-
112
- monkeypatch.setattr(api_client._circuit_breaker, "call", raise_open)
113
-
107
+ """Test circuit breaker open errors are exposed as VioletPoolAPIError."""
108
+
109
+ async def raise_open(_func, *args, **kwargs):
110
+ raise CircuitBreakerOpenError("Circuit breaker is OPEN")
111
+
112
+ monkeypatch.setattr(api_client._circuit_breaker, "call", raise_open)
113
+
114
114
  with pytest.raises(VioletPoolAPIError) as exc_info:
115
115
  await api_client.get_readings()
116
116
 
@@ -202,3 +202,116 @@ async def test_standalone_mode_blocks_base_module_functions(standalone_api_clien
202
202
  await standalone_api_client.set_pump_speed(speed=2, duration=0)
203
203
 
204
204
  assert "requires the Violet base module" in str(exc_info.value)
205
+
206
+ @pytest.mark.asyncio
207
+ async def test_get_readings_standalone_list_format(mock_aioresponse, api_client):
208
+ """Test get_readings parses the standalone list format correctly."""
209
+ url = "http://192.168.1.100/getReadings?ALL"
210
+ mock_data = {
211
+ "getReadings": [
212
+ {
213
+ "VALUE NAME": " \"date\"",
214
+ "DESCRIPTION": "System-date",
215
+ "FORMAT": "STRING",
216
+ "DETAILS": "deliverd as TT.MM.YYYY",
217
+ "VALUE": "12.04.2023"
218
+ },
219
+ {
220
+ "VALUE NAME": " \"CPU_TEMP\"",
221
+ "DESCRIPTION": "CPU-Temperature",
222
+ "FORMAT": "FLOAT",
223
+ "DETAILS": None,
224
+ "VALUE": 45.5
225
+ }
226
+ ]
227
+ }
228
+ mock_aioresponse.get(url, payload=mock_data, status=200)
229
+
230
+ assert api_client.dosing_standalone is False
231
+
232
+ result = await api_client.get_readings()
233
+
234
+ assert isinstance(result, dict)
235
+ assert result == {"date": "12.04.2023", "CPU_TEMP": 45.5}
236
+ assert api_client.dosing_standalone is True
237
+
238
+
239
+ @pytest.mark.asyncio
240
+ async def test_dosing_standalone_detection_dict_format(mock_aioresponse, standalone_api_client):
241
+ """Test dosing_standalone is set to False when dict format is received."""
242
+ url = "http://192.168.1.100/getReadings?ALL"
243
+ mock_data = {
244
+ "getReadings": {
245
+ "PUMPSTATE": "2",
246
+ "PH": 7.2
247
+ }
248
+ }
249
+ mock_aioresponse.get(url, payload=mock_data, status=200)
250
+
251
+ assert standalone_api_client.dosing_standalone is True
252
+
253
+ result = await standalone_api_client.get_readings()
254
+
255
+ assert isinstance(result, dict)
256
+ assert standalone_api_client.dosing_standalone is False
257
+
258
+ @pytest.mark.asyncio
259
+ async def test_get_hardware_profile(mock_aioresponse, api_client):
260
+ """Test get_hardware_profile correctly detects components."""
261
+ url = "http://192.168.1.100/getReadings?ALL"
262
+
263
+ # 1. Base module only (no DOS, EXT)
264
+ mock_aioresponse.get(url, payload={"getReadings": {"PUMPSTATE": "2", "SYSTEM_dosagemodule_cpu_temperature": "N/A"}}, status=200)
265
+ profile = await api_client.get_hardware_profile()
266
+ assert profile == {
267
+ "base_module": True,
268
+ "dosing_module": False,
269
+ "extension_module_1": False,
270
+ "extension_module_2": False,
271
+ }
272
+
273
+ # 2. Base module + Dosing + Ext1
274
+ mock_aioresponse.get(url, payload={"getReadings": {"PUMPSTATE": "2", "SYSTEM_dosagemodule_cpu_temperature": 45.5, "EXT1_1": "1"}}, status=200)
275
+ profile = await api_client.get_hardware_profile()
276
+ assert profile == {
277
+ "base_module": True,
278
+ "dosing_module": True,
279
+ "extension_module_1": True,
280
+ "extension_module_2": False,
281
+ }
282
+
283
+ # 3. Base module + Ext1 + Ext2 (no Dosing)
284
+ mock_aioresponse.get(url, payload={"getReadings": {"PUMPSTATE": "2", "EXT1_1": "1", "EXT2_1": "1"}}, status=200)
285
+ profile = await api_client.get_hardware_profile()
286
+ assert profile == {
287
+ "base_module": True,
288
+ "dosing_module": False,
289
+ "extension_module_1": True,
290
+ "extension_module_2": True,
291
+ }
292
+
293
+ @pytest.mark.asyncio
294
+ async def test_get_hardware_profile_standalone_dosing(mock_aioresponse, standalone_api_client):
295
+ """Test get_hardware_profile with a standalone dosing configuration."""
296
+ url = "http://192.168.1.100/getReadings?ALL"
297
+ # Using the standalone list format
298
+ mock_data = {
299
+ "getReadings": [
300
+ {
301
+ "VALUE NAME": " \"DOS_1_CL\"",
302
+ "DESCRIPTION": "Current state of OUTPUT: CL-DOSING",
303
+ "FORMAT": "INTEGER",
304
+ "DETAILS": "0 - AUTO (not on)",
305
+ "VALUE": 2
306
+ }
307
+ ]
308
+ }
309
+ mock_aioresponse.get(url, payload=mock_data, status=200)
310
+
311
+ profile = await standalone_api_client.get_hardware_profile()
312
+ assert profile == {
313
+ "base_module": False,
314
+ "dosing_module": True,
315
+ "extension_module_1": False,
316
+ "extension_module_2": False,
317
+ }