violet-poolController-api 0.0.9__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.9 → violet_poolcontroller_api-0.0.10}/PKG-INFO +17 -1
  2. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/README.md +16 -0
  3. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/pyproject.toml +1 -1
  4. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/setup.py +1 -1
  5. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/tests/test_api.py +61 -0
  6. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/violet_poolController_api.egg-info/PKG-INFO +17 -1
  7. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/violet_poolcontroller_api/api.py +76 -1
  8. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/LICENSE +0 -0
  9. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/setup.cfg +0 -0
  10. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/violet_poolController_api.egg-info/SOURCES.txt +0 -0
  11. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/violet_poolController_api.egg-info/dependency_links.txt +0 -0
  12. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/violet_poolController_api.egg-info/requires.txt +0 -0
  13. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/violet_poolController_api.egg-info/top_level.txt +0 -0
  14. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/violet_poolcontroller_api/__init__.py +0 -0
  15. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/violet_poolcontroller_api/circuit_breaker.py +0 -0
  16. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/violet_poolcontroller_api/const_api.py +0 -0
  17. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/violet_poolcontroller_api/const_devices.py +0 -0
  18. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/violet_poolcontroller_api/utils_rate_limiter.py +0 -0
  19. {violet_poolcontroller_api-0.0.9 → violet_poolcontroller_api-0.0.10}/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.9
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)
@@ -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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "violet-poolController-api"
7
- version = "0.0.9"
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.9",
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.",
@@ -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.9
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)
@@ -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
 
@@ -511,7 +511,57 @@ class VioletPoolAPI:
511
511
  query="ALL",
512
512
  payload_name="getReadings",
513
513
  )
514
- return self._flatten_getreadings_response(response)
514
+ flattened = self._flatten_getreadings_response(response)
515
+
516
+ # Determine hardware profile based on the flattened readings
517
+ def is_present(key: str) -> bool:
518
+ if not isinstance(flattened, dict):
519
+ return False
520
+ val = flattened.get(key)
521
+ return val is not None and str(val).strip().upper() != "N/A"
522
+
523
+ profile = {
524
+ "base_module": not self.dosing_standalone,
525
+ "dosing_module": self.dosing_standalone or is_present("SYSTEM_dosagemodule_cpu_temperature"),
526
+ "extension_module_1": is_present("EXT1_1"),
527
+ "extension_module_2": is_present("EXT2_1"),
528
+ }
529
+
530
+ return self._filter_unsupported_readings(flattened, profile)
531
+
532
+ def _filter_unsupported_readings(self, readings: dict[str, Any], profile: dict[str, bool]) -> dict[str, Any]:
533
+ """Filters out readings for hardware modules that are not present."""
534
+ if not isinstance(readings, dict):
535
+ return readings
536
+
537
+ filtered_readings = {}
538
+ for key, value in readings.items():
539
+ normalized_key = key.upper()
540
+
541
+ # Base Module specific keys
542
+ if not profile.get("base_module"):
543
+ if normalized_key in {
544
+ "PUMPSTATE", "PUMP_SPEED", "HEATER", "LIGHT", "ECO",
545
+ "BACKWASH", "BACKWASHRINSE", "REFILL", "PVSURPLUS", "TEMP_PUMP"
546
+ } or (normalized_key.startswith("SYSTEM_") and normalized_key != "SYSTEM_DOSAGEMODULE_CPU_TEMPERATURE"):
547
+ continue
548
+
549
+ # Extension Module 1 specific keys
550
+ if not profile.get("extension_module_1") and normalized_key.startswith("EXT1_"):
551
+ continue
552
+
553
+ # Extension Module 2 specific keys
554
+ if not profile.get("extension_module_2") and normalized_key.startswith("EXT2_"):
555
+ continue
556
+
557
+ # Dosing Module specific keys
558
+ if not profile.get("dosing_module"):
559
+ if normalized_key.startswith("DOS_") or normalized_key == "SYSTEM_DOSAGEMODULE_CPU_TEMPERATURE":
560
+ continue
561
+
562
+ filtered_readings[key] = value
563
+
564
+ return filtered_readings
515
565
 
516
566
  async def get_specific_readings(
517
567
  self, categories: list[str] | tuple[str, ...]
@@ -575,6 +625,31 @@ class VioletPoolAPI:
575
625
  payload_name="getWeatherdata",
576
626
  )
577
627
 
628
+ async def get_hardware_profile(self) -> dict[str, bool]:
629
+ """Detects connected hardware based on available readings.
630
+
631
+ Returns:
632
+ A dictionary with boolean flags for connected hardware components:
633
+ - base_module: True if the base module is present.
634
+ - dosing_module: True if the dosing module is present.
635
+ - extension_module_1: True if the first relay extension is present.
636
+ - extension_module_2: True if the second relay extension is present.
637
+ """
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
+ }
652
+
578
653
  async def get_overall_dosing(self) -> dict[str, Any]:
579
654
  """Returns aggregated dosing statistics.
580
655