violet-poolController-api 0.0.9__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.9 → violet_poolcontroller_api-0.0.11}/PKG-INFO +17 -1
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/README.md +16 -0
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/pyproject.toml +1 -1
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/setup.py +35 -35
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/tests/test_api.py +61 -0
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/violet_poolController_api.egg-info/PKG-INFO +17 -1
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/__init__.py +4 -0
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/api.py +82 -5
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/circuit_breaker.py +8 -7
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/const_api.py +3 -1
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/utils_rate_limiter.py +12 -9
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/LICENSE +0 -0
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/setup.cfg +0 -0
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/violet_poolController_api.egg-info/SOURCES.txt +0 -0
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/violet_poolController_api.egg-info/dependency_links.txt +0 -0
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/violet_poolController_api.egg-info/requires.txt +0 -0
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/violet_poolController_api.egg-info/top_level.txt +0 -0
- {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.11}/violet_poolcontroller_api/const_devices.py +0 -0
- {violet_poolcontroller_api-0.0.9 → 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.
|
|
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)
|
|
@@ -136,6 +136,22 @@ In this mode, dosing functions (for example `manual_dosing` and dosing parameter
|
|
|
136
136
|
**Note on getReadings format:**
|
|
137
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
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
|
+
|
|
139
155
|
## License
|
|
140
156
|
GNU Affero General Public License v3.0 or later (AGPLv3+)
|
|
141
157
|
|
|
@@ -108,6 +108,22 @@ In this mode, dosing functions (for example `manual_dosing` and dosing parameter
|
|
|
108
108
|
**Note on getReadings format:**
|
|
109
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
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
|
+
|
|
111
127
|
## License
|
|
112
128
|
GNU Affero General Public License v3.0 or later (AGPLv3+)
|
|
113
129
|
|
|
@@ -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
|
+
)
|
|
@@ -254,3 +254,64 @@ async def test_dosing_standalone_detection_dict_format(mock_aioresponse, standal
|
|
|
254
254
|
|
|
255
255
|
assert isinstance(result, dict)
|
|
256
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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: violet-poolController-api
|
|
3
|
-
Version: 0.0.
|
|
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)
|
|
@@ -136,6 +136,22 @@ In this mode, dosing functions (for example `manual_dosing` and dosing parameter
|
|
|
136
136
|
**Note on getReadings format:**
|
|
137
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
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
|
+
|
|
139
155
|
## License
|
|
140
156
|
GNU Affero General Public License v3.0 or later (AGPLv3+)
|
|
141
157
|
|
|
@@ -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,6 +497,28 @@ class VioletPoolAPI:
|
|
|
497
497
|
|
|
498
498
|
return response
|
|
499
499
|
|
|
500
|
+
def _build_hardware_profile(self, flattened: dict[str, Any]) -> dict[str, bool]:
|
|
501
|
+
"""Build a hardware presence profile from flattened getReadings data.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
flattened: The flattened key-value readings dict.
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
A dictionary with boolean flags for connected hardware components.
|
|
508
|
+
"""
|
|
509
|
+
def is_present(key: str) -> bool:
|
|
510
|
+
if not isinstance(flattened, dict):
|
|
511
|
+
return False
|
|
512
|
+
val = flattened.get(key)
|
|
513
|
+
return val is not None and str(val).strip().upper() != "N/A"
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
"base_module": not self.dosing_standalone,
|
|
517
|
+
"dosing_module": self.dosing_standalone or is_present("SYSTEM_dosagemodule_cpu_temperature"),
|
|
518
|
+
"extension_module_1": is_present("EXT1_1"),
|
|
519
|
+
"extension_module_2": is_present("EXT2_1"),
|
|
520
|
+
}
|
|
521
|
+
|
|
500
522
|
async def get_readings(self) -> dict[str, Any]:
|
|
501
523
|
"""Returns the complete dataset from the controller.
|
|
502
524
|
|
|
@@ -511,7 +533,43 @@ class VioletPoolAPI:
|
|
|
511
533
|
query="ALL",
|
|
512
534
|
payload_name="getReadings",
|
|
513
535
|
)
|
|
514
|
-
|
|
536
|
+
flattened = self._flatten_getreadings_response(response)
|
|
537
|
+
profile = self._build_hardware_profile(flattened)
|
|
538
|
+
return self._filter_unsupported_readings(flattened, profile)
|
|
539
|
+
|
|
540
|
+
def _filter_unsupported_readings(self, readings: dict[str, Any], profile: dict[str, bool]) -> dict[str, Any]:
|
|
541
|
+
"""Filters out readings for hardware modules that are not present."""
|
|
542
|
+
if not isinstance(readings, dict):
|
|
543
|
+
return readings
|
|
544
|
+
|
|
545
|
+
filtered_readings = {}
|
|
546
|
+
for key, value in readings.items():
|
|
547
|
+
normalized_key = key.upper()
|
|
548
|
+
|
|
549
|
+
# Base Module specific keys
|
|
550
|
+
if not profile.get("base_module"):
|
|
551
|
+
if normalized_key in {
|
|
552
|
+
"PUMPSTATE", "PUMP_SPEED", "HEATER", "LIGHT", "ECO",
|
|
553
|
+
"BACKWASH", "BACKWASHRINSE", "REFILL", "PVSURPLUS", "TEMP_PUMP"
|
|
554
|
+
} or (normalized_key.startswith("SYSTEM_") and normalized_key != "SYSTEM_DOSAGEMODULE_CPU_TEMPERATURE"):
|
|
555
|
+
continue
|
|
556
|
+
|
|
557
|
+
# Extension Module 1 specific keys
|
|
558
|
+
if not profile.get("extension_module_1") and normalized_key.startswith("EXT1_"):
|
|
559
|
+
continue
|
|
560
|
+
|
|
561
|
+
# Extension Module 2 specific keys
|
|
562
|
+
if not profile.get("extension_module_2") and normalized_key.startswith("EXT2_"):
|
|
563
|
+
continue
|
|
564
|
+
|
|
565
|
+
# Dosing Module specific keys
|
|
566
|
+
if not profile.get("dosing_module"):
|
|
567
|
+
if normalized_key.startswith("DOS_") or normalized_key == "SYSTEM_DOSAGEMODULE_CPU_TEMPERATURE":
|
|
568
|
+
continue
|
|
569
|
+
|
|
570
|
+
filtered_readings[key] = value
|
|
571
|
+
|
|
572
|
+
return filtered_readings
|
|
515
573
|
|
|
516
574
|
async def get_specific_readings(
|
|
517
575
|
self, categories: list[str] | tuple[str, ...]
|
|
@@ -575,6 +633,24 @@ class VioletPoolAPI:
|
|
|
575
633
|
payload_name="getWeatherdata",
|
|
576
634
|
)
|
|
577
635
|
|
|
636
|
+
async def get_hardware_profile(self) -> dict[str, bool]:
|
|
637
|
+
"""Detects connected hardware based on available readings.
|
|
638
|
+
|
|
639
|
+
Returns:
|
|
640
|
+
A dictionary with boolean flags for connected hardware components:
|
|
641
|
+
- base_module: True if the base module is present.
|
|
642
|
+
- dosing_module: True if the dosing module is present.
|
|
643
|
+
- extension_module_1: True if the first relay extension is present.
|
|
644
|
+
- extension_module_2: True if the second relay extension is present.
|
|
645
|
+
"""
|
|
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)
|
|
653
|
+
|
|
578
654
|
async def get_overall_dosing(self) -> dict[str, Any]:
|
|
579
655
|
"""Returns aggregated dosing statistics.
|
|
580
656
|
|
|
@@ -760,7 +836,8 @@ class VioletPoolAPI:
|
|
|
760
836
|
if not output:
|
|
761
837
|
raise VioletPoolAPIError("Output identifier is required")
|
|
762
838
|
|
|
763
|
-
|
|
839
|
+
safe_duration = max(0, min(86400, int(duration))) # cap at 24 h
|
|
840
|
+
duration_ms = safe_duration * 1000
|
|
764
841
|
payload = f"{output},{mode},{duration_ms}"
|
|
765
842
|
body = await self._request(
|
|
766
843
|
API_SET_OUTPUT_TESTMODE,
|
|
@@ -866,7 +943,7 @@ class VioletPoolAPI:
|
|
|
866
943
|
raise VioletPoolAPIError(f"Unsupported DMX action: {action}")
|
|
867
944
|
|
|
868
945
|
tasks = []
|
|
869
|
-
for scene in range(1,
|
|
946
|
+
for scene in range(1, DMX_SCENE_COUNT + 1):
|
|
870
947
|
key = f"DMX_SCENE{scene}"
|
|
871
948
|
tasks.append(self.set_switch_state(key, action))
|
|
872
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
|