violet-poolController-api 0.0.5__tar.gz → 0.0.6__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.5 → violet_poolcontroller_api-0.0.6}/PKG-INFO +19 -2
  2. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/README.md +23 -6
  3. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/pyproject.toml +1 -1
  4. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/setup.py +1 -1
  5. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/tests/test_api.py +42 -5
  6. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/violet_poolController_api.egg-info/PKG-INFO +19 -2
  7. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/violet_poolcontroller_api/api.py +83 -43
  8. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/violet_poolcontroller_api/circuit_breaker.py +2 -1
  9. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/violet_poolcontroller_api/const_devices.py +1 -1
  10. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/LICENSE +0 -0
  11. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/setup.cfg +0 -0
  12. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/violet_poolController_api.egg-info/SOURCES.txt +0 -0
  13. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/violet_poolController_api.egg-info/dependency_links.txt +0 -0
  14. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/violet_poolController_api.egg-info/requires.txt +0 -0
  15. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/violet_poolController_api.egg-info/top_level.txt +0 -0
  16. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/violet_poolcontroller_api/__init__.py +0 -0
  17. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/violet_poolcontroller_api/const_api.py +0 -0
  18. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/violet_poolcontroller_api/utils_rate_limiter.py +0 -0
  19. {violet_poolcontroller_api-0.0.5 → violet_poolcontroller_api-0.0.6}/violet_poolcontroller_api/utils_sanitizer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: violet-poolController-api
3
- Version: 0.0.5
3
+ Version: 0.0.6
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)
@@ -74,7 +74,8 @@ async def main():
74
74
  host="192.168.1.100",
75
75
  username="admin",
76
76
  password="your_password",
77
- session=session
77
+ session=session,
78
+ dosing_standalone=False, # True for Violet dosing standalone setups
78
79
  )
79
80
 
80
81
  try:
@@ -116,6 +117,22 @@ The API client includes many more functions tailored to the Violet Controller:
116
117
 
117
118
  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`.
118
119
 
120
+ ## Violet Dosing Standalone Mode
121
+
122
+ If your Violet setup runs as dosing standalone (without the base module), enable:
123
+
124
+ ```python
125
+ api = VioletPoolAPI(
126
+ host="192.168.1.100",
127
+ username="admin",
128
+ password="your_password",
129
+ session=session,
130
+ dosing_standalone=True,
131
+ )
132
+ ```
133
+
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
+
119
136
  ## License
120
137
  GNU Affero General Public License v3.0 or later (AGPLv3+)
121
138
 
@@ -12,11 +12,11 @@ An asynchronous Python client for interacting with the **Violet Pool Controller*
12
12
 
13
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
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.
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
20
 
21
21
  ## Features
22
22
  * **Asynchronous:** Fully async operations using `aiohttp`.
@@ -46,7 +46,8 @@ async def main():
46
46
  host="192.168.1.100",
47
47
  username="admin",
48
48
  password="your_password",
49
- session=session
49
+ session=session,
50
+ dosing_standalone=False, # True for Violet dosing standalone setups
50
51
  )
51
52
 
52
53
  try:
@@ -88,6 +89,22 @@ The API client includes many more functions tailored to the Violet Controller:
88
89
 
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`.
90
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
+
91
108
  ## License
92
109
  GNU Affero General Public License v3.0 or later (AGPLv3+)
93
110
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "violet-poolController-api"
7
- version = "0.0.5"
7
+ version = "0.0.6"
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.5",
4
+ version="0.0.6",
5
5
  author="Basti (Xerolux)",
6
6
  author_email="git@xerolux.de",
7
7
  description="Asynchronous Python client for the Violet Pool Controller.",
@@ -26,8 +26,8 @@ def mock_aioresponse():
26
26
  with aioresponses() as m:
27
27
  yield m
28
28
 
29
- @pytest_asyncio.fixture
30
- async def api_client():
29
+ @pytest_asyncio.fixture
30
+ async def api_client():
31
31
  async with aiohttp.ClientSession() as session:
32
32
  # Pass low retry counts to make error tests faster
33
33
  api = VioletPoolAPI(
@@ -36,9 +36,23 @@ async def api_client():
36
36
  username="admin",
37
37
  password="password",
38
38
  max_retries=1
39
- )
40
- yield api
41
-
39
+ )
40
+ yield api
41
+
42
+
43
+ @pytest_asyncio.fixture
44
+ async def standalone_api_client():
45
+ async with aiohttp.ClientSession() as session:
46
+ api = VioletPoolAPI(
47
+ host="192.168.1.100",
48
+ session=session,
49
+ username="admin",
50
+ password="password",
51
+ max_retries=1,
52
+ dosing_standalone=True,
53
+ )
54
+ yield api
55
+
42
56
  @pytest.mark.asyncio
43
57
  async def test_get_readings_success(mock_aioresponse, api_client):
44
58
  """Test get_readings returns the correct parsed JSON dictionary."""
@@ -165,3 +179,26 @@ async def test_set_config_sanitizes_payload_before_request(api_client, monkeypat
165
179
  assert result["success"] is True
166
180
  assert result["response"] == "OK"
167
181
  assert captured["json_payload"] == {"poolmode": "A<mode>", "speed": 3.7}
182
+
183
+
184
+ @pytest.mark.asyncio
185
+ async def test_standalone_mode_allows_manual_dosing(
186
+ mock_aioresponse, standalone_api_client
187
+ ):
188
+ """Standalone mode must still allow dosing outputs."""
189
+ url = "http://192.168.1.100/setFunctionManually?DOS_1_CL,ON,45,0"
190
+ mock_aioresponse.get(url, body="OK", status=200)
191
+
192
+ result = await standalone_api_client.manual_dosing("Chlor", 45)
193
+
194
+ assert result["success"] is True
195
+ assert result["response"] == "OK"
196
+
197
+
198
+ @pytest.mark.asyncio
199
+ async def test_standalone_mode_blocks_base_module_functions(standalone_api_client):
200
+ """Standalone mode must reject functions that require the base module."""
201
+ with pytest.raises(VioletPoolAPIError) as exc_info:
202
+ await standalone_api_client.set_pump_speed(speed=2, duration=0)
203
+
204
+ assert "requires the Violet base module" in str(exc_info.value)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: violet-poolController-api
3
- Version: 0.0.5
3
+ Version: 0.0.6
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)
@@ -74,7 +74,8 @@ async def main():
74
74
  host="192.168.1.100",
75
75
  username="admin",
76
76
  password="your_password",
77
- session=session
77
+ session=session,
78
+ dosing_standalone=False, # True for Violet dosing standalone setups
78
79
  )
79
80
 
80
81
  try:
@@ -116,6 +117,22 @@ The API client includes many more functions tailored to the Violet Controller:
116
117
 
117
118
  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`.
118
119
 
120
+ ## Violet Dosing Standalone Mode
121
+
122
+ If your Violet setup runs as dosing standalone (without the base module), enable:
123
+
124
+ ```python
125
+ api = VioletPoolAPI(
126
+ host="192.168.1.100",
127
+ username="admin",
128
+ password="your_password",
129
+ session=session,
130
+ dosing_standalone=True,
131
+ )
132
+ ```
133
+
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
+
119
136
  ## License
120
137
  GNU Affero General Public License v3.0 or later (AGPLv3+)
121
138
 
@@ -79,18 +79,19 @@ class VioletPoolAPI:
79
79
  endpoints.
80
80
  """
81
81
 
82
- def __init__(
83
- self,
84
- *,
85
- host: str,
86
- session: aiohttp.ClientSession,
82
+ def __init__(
83
+ self,
84
+ *,
85
+ host: str,
86
+ session: aiohttp.ClientSession,
87
87
  username: str | None = None,
88
88
  password: str | None = None,
89
- use_ssl: bool = False,
90
- verify_ssl: bool = True,
91
- timeout: int = 10,
92
- max_retries: int = 3,
93
- ) -> None:
89
+ use_ssl: bool = False,
90
+ verify_ssl: bool = True,
91
+ timeout: int = 10,
92
+ max_retries: int = 3,
93
+ dosing_standalone: bool = False,
94
+ ) -> None:
94
95
  """Initializes the API helper.
95
96
 
96
97
  Args:
@@ -99,10 +100,12 @@ class VioletPoolAPI:
99
100
  username: The username for authentication.
100
101
  password: The password for authentication.
101
102
  use_ssl: Whether to use SSL for the connection.
102
- verify_ssl: Whether to verify SSL certificates (security feature).
103
- timeout: The request timeout in seconds.
104
- max_retries: The maximum number of retries for failed requests.
105
- """
103
+ verify_ssl: Whether to verify SSL certificates (security feature).
104
+ timeout: The request timeout in seconds.
105
+ max_retries: The maximum number of retries for failed requests.
106
+ dosing_standalone: Whether the controller runs in dosing-standalone
107
+ mode without a connected base module.
108
+ """
106
109
  if session is None:
107
110
  raise ValueError("A valid aiohttp session must be provided")
108
111
 
@@ -110,15 +113,16 @@ class VioletPoolAPI:
110
113
 
111
114
  self._session = session
112
115
  total_timeout = max(float(timeout), 1.0)
113
- self._timeout = aiohttp.ClientTimeout(
114
- total=total_timeout,
115
- connect=total_timeout * 0.8,
116
- sock_connect=total_timeout * 0.8,
117
- )
118
- self._max_retries = max(1, int(max_retries))
119
- self._auth = None
120
- if username:
121
- self._auth = aiohttp.BasicAuth(username, password or "")
116
+ self._timeout = aiohttp.ClientTimeout(
117
+ total=total_timeout,
118
+ connect=total_timeout * 0.8,
119
+ sock_connect=total_timeout * 0.8,
120
+ )
121
+ self._max_retries = max(1, int(max_retries))
122
+ self._dosing_standalone = bool(dosing_standalone)
123
+ self._auth = None
124
+ if username:
125
+ self._auth = aiohttp.BasicAuth(username, password or "")
122
126
 
123
127
  # SSL/TLS security configuration
124
128
  self._verify_ssl = verify_ssl
@@ -158,13 +162,18 @@ class VioletPoolAPI:
158
162
  return self._timeout.total or 0.0
159
163
 
160
164
  @property
161
- def max_retries(self) -> int:
162
- """Get maximum retry attempts.
165
+ def max_retries(self) -> int:
166
+ """Get maximum retry attempts.
163
167
 
164
168
  Returns:
165
169
  The maximum number of retry attempts.
166
- """
167
- return self._max_retries
170
+ """
171
+ return self._max_retries
172
+
173
+ @property
174
+ def dosing_standalone(self) -> bool:
175
+ """Return whether dosing-standalone mode is enabled."""
176
+ return self._dosing_standalone
168
177
 
169
178
  # ---------------------------------------------------------------------
170
179
  # Generic helpers
@@ -399,9 +408,9 @@ class VioletPoolAPI:
399
408
  )
400
409
  return response
401
410
 
402
- def _sanitize_config_payload(self, config: Mapping[str, Any]) -> dict[str, Any]:
403
- """Sanitize and validate configuration payload before POSTing it."""
404
- sanitized_config: dict[str, str | int | float] = {}
411
+ def _sanitize_config_payload(self, config: Mapping[str, Any]) -> dict[str, Any]:
412
+ """Sanitize and validate configuration payload before POSTing it."""
413
+ sanitized_config: dict[str, str | int | float] = {}
405
414
 
406
415
  for key, value in config.items():
407
416
  try:
@@ -427,7 +436,31 @@ class VioletPoolAPI:
427
436
  f"Invalid configuration parameter: {key}"
428
437
  ) from err
429
438
 
430
- return sanitized_config
439
+ return sanitized_config
440
+
441
+ def _is_base_module_function(self, key: str) -> bool:
442
+ """Return True if the function depends on the base module."""
443
+ normalized = (key or "").strip().upper()
444
+ if not normalized:
445
+ return False
446
+
447
+ if normalized.startswith("DOS_"):
448
+ return False
449
+
450
+ if normalized.startswith(("EXT", "DMX_SCENE", "DIRULE_", "OMNI_DC")):
451
+ return True
452
+
453
+ return normalized in {
454
+ "PUMP",
455
+ "SOLAR",
456
+ "HEATER",
457
+ "LIGHT",
458
+ "ECO",
459
+ "BACKWASH",
460
+ "BACKWASHRINSE",
461
+ "REFILL",
462
+ "PVSURPLUS",
463
+ }
431
464
 
432
465
  # ---------------------------------------------------------------------
433
466
  # Public API surface
@@ -640,8 +673,9 @@ class VioletPoolAPI:
640
673
  "Skipping malformed calibration history line: %s", line
641
674
  )
642
675
  except (IndexError, AttributeError) as err:
676
+ err_msg = str(err) or type(err).__name__
643
677
  _LOGGER.warning(
644
- "Error parsing calibration history line '%s': %s", line, err
678
+ "Error parsing calibration history line '%s': %s", line, err_msg
645
679
  )
646
680
  return entries
647
681
 
@@ -701,10 +735,10 @@ class VioletPoolAPI:
701
735
  )
702
736
  return self._command_result(body)
703
737
 
704
- async def set_switch_state(
705
- self,
706
- key: str,
707
- action: str,
738
+ async def set_switch_state(
739
+ self,
740
+ key: str,
741
+ action: str,
708
742
  *,
709
743
  duration: int | float | None = None,
710
744
  last_value: int | float | None = None,
@@ -717,12 +751,18 @@ class VioletPoolAPI:
717
751
  duration: An optional duration for the action.
718
752
  last_value: An optional last value (e.g., speed).
719
753
 
720
- Returns:
721
- A dictionary with the command result.
722
- """
723
- payload = self._build_manual_command(
724
- key,
725
- action,
754
+ Returns:
755
+ A dictionary with the command result.
756
+ """
757
+ if self._dosing_standalone and self._is_base_module_function(key):
758
+ raise VioletPoolAPIError(
759
+ f"Function '{key}' requires the Violet base module and is not "
760
+ "available in dosing-standalone mode"
761
+ )
762
+
763
+ payload = self._build_manual_command(
764
+ key,
765
+ action,
726
766
  duration=duration,
727
767
  last_value=last_value,
728
768
  )
@@ -803,7 +843,7 @@ class VioletPoolAPI:
803
843
  results: list[dict[str, Any]] = []
804
844
  for res in raw_results:
805
845
  if isinstance(res, Exception):
806
- results.append({"success": False, "response": str(res)})
846
+ results.append({"success": False, "response": f"{type(res).__name__}: {str(res)}"})
807
847
  elif isinstance(res, dict):
808
848
  results.append(res)
809
849
 
@@ -22,12 +22,13 @@ import asyncio
22
22
  import logging
23
23
  import time
24
24
  from collections.abc import Callable
25
+ from enum import StrEnum
25
26
  from typing import Any
26
27
 
27
28
  _LOGGER = logging.getLogger(__name__)
28
29
 
29
30
 
30
- class CircuitBreakerState:
31
+ class CircuitBreakerState(StrEnum):
31
32
  """Circuit breaker states."""
32
33
 
33
34
  CLOSED = "CLOSED" # Normal operation
@@ -30,7 +30,7 @@ from typing import Any, cast
30
30
  # DEVICE PARAMETERS - Extended Configuration
31
31
  # =============================================================================
32
32
 
33
- DEVICE_PARAMETERS = {
33
+ DEVICE_PARAMETERS: dict[str, dict[str, Any]] = {
34
34
  "PUMP": {
35
35
  "supports_speed": True,
36
36
  "api_template": "PUMP,{action},{duration},{speed}",