violet-poolController-api 0.0.10__tar.gz → 0.0.11__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.10 → violet_poolcontroller_api-0.0.11}/PKG-INFO +1 -1
  2. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/pyproject.toml +1 -1
  3. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/setup.py +35 -35
  4. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolController_api.egg-info/PKG-INFO +1 -1
  5. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/__init__.py +4 -0
  6. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/api.py +35 -33
  7. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/circuit_breaker.py +8 -7
  8. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/const_api.py +3 -1
  9. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/utils_rate_limiter.py +12 -9
  10. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/LICENSE +0 -0
  11. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/README.md +0 -0
  12. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/setup.cfg +0 -0
  13. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/tests/test_api.py +0 -0
  14. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolController_api.egg-info/SOURCES.txt +0 -0
  15. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolController_api.egg-info/dependency_links.txt +0 -0
  16. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolController_api.egg-info/requires.txt +0 -0
  17. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolController_api.egg-info/top_level.txt +0 -0
  18. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/const_devices.py +0 -0
  19. {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/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.10
3
+ Version: 0.0.11
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)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "violet-poolController-api"
7
- version = "0.0.10"
7
+ version = "0.0.11"
8
8
  authors = [
9
9
  { name="Basti (Xerolux)", email="git@xerolux.de" },
10
10
  ]
@@ -1,35 +1,35 @@
1
- from setuptools import setup, find_packages
2
- setup(
3
- name="violet-poolController-api",
4
- version="0.0.10",
5
- author="Basti (Xerolux)",
6
- author_email="git@xerolux.de",
7
- description="Asynchronous Python client for the Violet Pool Controller.",
8
- long_description=open("README.md").read(),
9
- long_description_content_type="text/markdown",
10
- url="https://github.com/Xerolux/violet-poolController-api",
11
- packages=find_packages(),
12
- license="AGPL-3.0-or-later",
13
- classifiers=[
14
- "Programming Language :: Python :: 3",
15
- "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
16
- "Operating System :: OS Independent",
17
- "Intended Audience :: Developers",
18
- "Topic :: Home Automation",
19
- ],
20
- python_requires=">=3.12",
21
- install_requires=[
22
- "aiohttp>=3.9.0",
23
- ],
24
- extras_require={
25
- "test": [
26
- "aioresponses>=0.7.6",
27
- "pytest>=8.0",
28
- "pytest-asyncio>=0.23",
29
- ],
30
- },
31
- project_urls={
32
- "Bug Tracker": "https://github.com/Xerolux/violet-poolController-api/issues",
33
- "License": "https://www.gnu.org/licenses/agpl-3.0.html",
34
- },
35
- )
1
+ from setuptools import setup, find_packages
2
+ setup(
3
+ name="violet-poolController-api",
4
+ version="0.0.11",
5
+ author="Basti (Xerolux)",
6
+ author_email="git@xerolux.de",
7
+ description="Asynchronous Python client for the Violet Pool Controller.",
8
+ long_description=open("README.md").read(),
9
+ long_description_content_type="text/markdown",
10
+ url="https://github.com/Xerolux/violet-poolController-api",
11
+ packages=find_packages(),
12
+ license="AGPL-3.0-or-later",
13
+ classifiers=[
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
16
+ "Operating System :: OS Independent",
17
+ "Intended Audience :: Developers",
18
+ "Topic :: Home Automation",
19
+ ],
20
+ python_requires=">=3.12",
21
+ install_requires=[
22
+ "aiohttp>=3.9.0",
23
+ ],
24
+ extras_require={
25
+ "test": [
26
+ "aioresponses>=0.7.6",
27
+ "pytest>=8.0",
28
+ "pytest-asyncio>=0.23",
29
+ ],
30
+ },
31
+ project_urls={
32
+ "Bug Tracker": "https://github.com/Xerolux/violet-poolController-api/issues",
33
+ "License": "https://www.gnu.org/licenses/agpl-3.0.html",
34
+ },
35
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: violet-poolController-api
3
- Version: 0.0.10
3
+ Version: 0.0.11
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)
@@ -13,3 +13,7 @@
13
13
  #
14
14
  # You should have received a copy of the GNU Affero General Public License
15
15
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ from .api import VioletPoolAPI, VioletPoolAPIError
18
+
19
+ __all__ = ["VioletPoolAPI", "VioletPoolAPIError"]
@@ -18,8 +18,6 @@
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
- from .const_devices import DEVICE_PARAMETERS
22
-
23
21
  import asyncio
24
22
  import json
25
23
  import logging
@@ -55,12 +53,14 @@ from .const_api import (
55
53
  API_SET_FUNCTION_MANUALLY,
56
54
  API_SET_OUTPUT_TESTMODE,
57
55
  API_SET_TARGET_VALUES,
56
+ DMX_SCENE_COUNT,
58
57
  DOSING_FUNCTIONS,
59
58
  TARGET_MIN_CHLORINE,
60
59
  TARGET_ORP,
61
60
  TARGET_PH,
62
61
  )
63
62
  from .circuit_breaker import CircuitBreaker, CircuitBreakerOpenError
63
+ from .const_devices import DEVICE_PARAMETERS
64
64
  from .utils_rate_limiter import get_global_rate_limiter
65
65
  from .utils_sanitizer import InputSanitizer
66
66
 
@@ -497,36 +497,44 @@ class VioletPoolAPI:
497
497
 
498
498
  return response
499
499
 
500
- async def get_readings(self) -> dict[str, Any]:
501
- """Returns the complete dataset from the controller.
500
+ def _build_hardware_profile(self, flattened: dict[str, Any]) -> dict[str, bool]:
501
+ """Build a hardware presence profile from flattened getReadings data.
502
502
 
503
- Returns:
504
- A dictionary containing all readings.
503
+ Args:
504
+ flattened: The flattened key-value readings dict.
505
505
 
506
- Raises:
507
- VioletPoolAPIError: If the payload is unexpected.
506
+ Returns:
507
+ A dictionary with boolean flags for connected hardware components.
508
508
  """
509
- response = await self._request_json_dict(
510
- API_READINGS,
511
- query="ALL",
512
- payload_name="getReadings",
513
- )
514
- flattened = self._flatten_getreadings_response(response)
515
-
516
- # Determine hardware profile based on the flattened readings
517
509
  def is_present(key: str) -> bool:
518
510
  if not isinstance(flattened, dict):
519
511
  return False
520
512
  val = flattened.get(key)
521
513
  return val is not None and str(val).strip().upper() != "N/A"
522
514
 
523
- profile = {
515
+ return {
524
516
  "base_module": not self.dosing_standalone,
525
517
  "dosing_module": self.dosing_standalone or is_present("SYSTEM_dosagemodule_cpu_temperature"),
526
518
  "extension_module_1": is_present("EXT1_1"),
527
519
  "extension_module_2": is_present("EXT2_1"),
528
520
  }
529
521
 
522
+ async def get_readings(self) -> dict[str, Any]:
523
+ """Returns the complete dataset from the controller.
524
+
525
+ Returns:
526
+ A dictionary containing all readings.
527
+
528
+ Raises:
529
+ VioletPoolAPIError: If the payload is unexpected.
530
+ """
531
+ response = await self._request_json_dict(
532
+ API_READINGS,
533
+ query="ALL",
534
+ payload_name="getReadings",
535
+ )
536
+ flattened = self._flatten_getreadings_response(response)
537
+ profile = self._build_hardware_profile(flattened)
530
538
  return self._filter_unsupported_readings(flattened, profile)
531
539
 
532
540
  def _filter_unsupported_readings(self, readings: dict[str, Any], profile: dict[str, bool]) -> dict[str, Any]:
@@ -635,20 +643,13 @@ class VioletPoolAPI:
635
643
  - extension_module_1: True if the first relay extension is present.
636
644
  - extension_module_2: True if the second relay extension is present.
637
645
  """
638
- readings = await self.get_readings()
639
-
640
- def is_present(key: str) -> bool:
641
- if not isinstance(readings, dict):
642
- return False
643
- val = readings.get(key)
644
- return val is not None and str(val).strip().upper() != "N/A"
645
-
646
- return {
647
- "base_module": not self.dosing_standalone,
648
- "dosing_module": self.dosing_standalone or is_present("SYSTEM_dosagemodule_cpu_temperature"),
649
- "extension_module_1": is_present("EXT1_1"),
650
- "extension_module_2": is_present("EXT2_1"),
651
- }
646
+ response = await self._request_json_dict(
647
+ API_READINGS,
648
+ query="ALL",
649
+ payload_name="getReadings",
650
+ )
651
+ flattened = self._flatten_getreadings_response(response)
652
+ return self._build_hardware_profile(flattened)
652
653
 
653
654
  async def get_overall_dosing(self) -> dict[str, Any]:
654
655
  """Returns aggregated dosing statistics.
@@ -835,7 +836,8 @@ class VioletPoolAPI:
835
836
  if not output:
836
837
  raise VioletPoolAPIError("Output identifier is required")
837
838
 
838
- duration_ms = max(0, int(duration)) * 1000
839
+ safe_duration = max(0, min(86400, int(duration))) # cap at 24 h
840
+ duration_ms = safe_duration * 1000
839
841
  payload = f"{output},{mode},{duration_ms}"
840
842
  body = await self._request(
841
843
  API_SET_OUTPUT_TESTMODE,
@@ -941,7 +943,7 @@ class VioletPoolAPI:
941
943
  raise VioletPoolAPIError(f"Unsupported DMX action: {action}")
942
944
 
943
945
  tasks = []
944
- for scene in range(1, 13):
946
+ for scene in range(1, DMX_SCENE_COUNT + 1):
945
947
  key = f"DMX_SCENE{scene}"
946
948
  tasks.append(self.set_switch_state(key, action))
947
949
 
@@ -22,7 +22,7 @@ import asyncio
22
22
  import logging
23
23
  import time
24
24
  from collections.abc import Callable
25
- from enum import StrEnum
25
+ from enum import StrEnum
26
26
  from typing import Any
27
27
 
28
28
  _LOGGER = logging.getLogger(__name__)
@@ -175,12 +175,13 @@ class CircuitBreaker:
175
175
  "half_open_start_time": self.half_open_start_time,
176
176
  }
177
177
 
178
- def reset(self) -> None:
179
- """Manually reset the circuit breaker (call from sync context only)."""
180
- self.state = CircuitBreakerState.CLOSED
181
- self.failure_count = 0
182
- self.last_failure_time = 0.0
183
- self.half_open_start_time = 0.0
178
+ async def reset(self) -> None:
179
+ """Manually reset the circuit breaker to CLOSED state."""
180
+ async with self._lock:
181
+ self.state = CircuitBreakerState.CLOSED
182
+ self.failure_count = 0
183
+ self.last_failure_time = 0.0
184
+ self.half_open_start_time = 0.0
184
185
  _LOGGER.info("Circuit breaker manually reset to CLOSED state")
185
186
 
186
187
 
@@ -123,8 +123,10 @@ for ext_bank in [1, 2]:
123
123
  f"Erweiterung {ext_bank}.{relay_num}"
124
124
  )
125
125
 
126
+ DMX_SCENE_COUNT = 12 # Number of DMX scenes supported by the controller
127
+
126
128
  # Dynamically add DMX scenes
127
- for scene_num in range(1, 13):
129
+ for scene_num in range(1, DMX_SCENE_COUNT + 1):
128
130
  SWITCH_FUNCTIONS[f"DMX_SCENE{scene_num}"] = f"DMX Szene {scene_num}"
129
131
 
130
132
  # Dynamically add digital input rules
@@ -125,17 +125,20 @@ class RateLimiter:
125
125
  # Track failures efficiently
126
126
  self.blocked_requests += 1
127
127
  self._recent_stats["blocked_last_minute"] += 1
128
+ self.request_history.append(
129
+ {"time": current_time, "priority": priority, "blocked": True}
130
+ )
128
131
  return False
129
132
 
130
- def _cleanup_history(self, current_time: float) -> None:
131
- """Clean up old history entries to prevent memory leaks."""
132
- # Remove entries older than 1 hour
133
- cutoff_time = current_time - 3600
134
-
135
- while self.request_history and self.request_history[0]["time"] <= cutoff_time:
136
- self.request_history.popleft()
137
-
138
- _LOGGER.debug("Rate limiter history cleanup completed")
133
+ def _cleanup_history(self, current_time: float) -> None:
134
+ """Clean up old history entries to prevent memory leaks."""
135
+ # Remove entries older than 1 hour
136
+ cutoff_time = current_time - 3600
137
+
138
+ while self.request_history and self.request_history[0]["time"] <= cutoff_time:
139
+ self.request_history.popleft()
140
+
141
+ _LOGGER.debug("Rate limiter history cleanup completed")
139
142
 
140
143
  def _update_recent_stats(self, current_time: float) -> None:
141
144
  """Update memory-efficient recent statistics."""