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.
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/PKG-INFO +1 -1
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/pyproject.toml +1 -1
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/setup.py +35 -35
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolController_api.egg-info/PKG-INFO +1 -1
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/__init__.py +4 -0
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/api.py +35 -33
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/circuit_breaker.py +8 -7
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/const_api.py +3 -1
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/utils_rate_limiter.py +12 -9
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/LICENSE +0 -0
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/README.md +0 -0
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/setup.cfg +0 -0
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/tests/test_api.py +0 -0
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolController_api.egg-info/SOURCES.txt +0 -0
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolController_api.egg-info/dependency_links.txt +0 -0
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolController_api.egg-info/requires.txt +0 -0
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolController_api.egg-info/top_level.txt +0 -0
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/const_devices.py +0 -0
- {violet_poolcontroller_api-0.0.10 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/utils_sanitizer.py +0 -0
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
from setuptools import setup, find_packages
|
|
2
|
-
setup(
|
|
3
|
-
name="violet-poolController-api",
|
|
4
|
-
version="0.0.
|
|
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
|
+
)
|
|
@@ -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
|
-
|
|
501
|
-
"""
|
|
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
|
-
|
|
504
|
-
|
|
503
|
+
Args:
|
|
504
|
+
flattened: The flattened key-value readings dict.
|
|
505
505
|
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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,
|
|
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."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|