violet-poolController-api 0.0.11__tar.gz → 0.0.12__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.11 → violet_poolcontroller_api-0.0.12}/PKG-INFO +1 -1
  2. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/pyproject.toml +1 -1
  3. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/setup.py +1 -1
  4. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/tests/test_api.py +31 -5
  5. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/violet_poolController_api.egg-info/PKG-INFO +1 -1
  6. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/violet_poolcontroller_api/api.py +34 -7
  7. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/LICENSE +0 -0
  8. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/README.md +0 -0
  9. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/setup.cfg +0 -0
  10. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/violet_poolController_api.egg-info/SOURCES.txt +0 -0
  11. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/violet_poolController_api.egg-info/dependency_links.txt +0 -0
  12. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/violet_poolController_api.egg-info/requires.txt +0 -0
  13. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/violet_poolController_api.egg-info/top_level.txt +0 -0
  14. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/violet_poolcontroller_api/__init__.py +0 -0
  15. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/violet_poolcontroller_api/circuit_breaker.py +0 -0
  16. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/violet_poolcontroller_api/const_api.py +0 -0
  17. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/violet_poolcontroller_api/const_devices.py +0 -0
  18. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/violet_poolcontroller_api/utils_rate_limiter.py +0 -0
  19. {violet_poolcontroller_api-0.0.11 → violet_poolcontroller_api-0.0.12}/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.11
3
+ Version: 0.0.12
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.11"
7
+ version = "0.0.12"
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.11",
4
+ version="0.0.12",
5
5
  author="Basti (Xerolux)",
6
6
  author_email="git@xerolux.de",
7
7
  description="Asynchronous Python client for the Violet Pool Controller.",
@@ -257,7 +257,7 @@ async def test_dosing_standalone_detection_dict_format(mock_aioresponse, standal
257
257
 
258
258
  @pytest.mark.asyncio
259
259
  async def test_get_hardware_profile(mock_aioresponse, api_client):
260
- """Test get_hardware_profile correctly detects components."""
260
+ """Test get_hardware_profile correctly detects components via alive counters."""
261
261
  url = "http://192.168.1.100/getReadings?ALL"
262
262
 
263
263
  # 1. Base module only (no DOS, EXT)
@@ -270,8 +270,14 @@ async def test_get_hardware_profile(mock_aioresponse, api_client):
270
270
  "extension_module_2": False,
271
271
  }
272
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)
273
+ # 2. Base module + Dosing + Ext1 (via alive counters)
274
+ mock_aioresponse.get(url, payload={"getReadings": {
275
+ "PUMPSTATE": "2",
276
+ "SYSTEM_dosagemodule_alive_count": "20392243",
277
+ "SYSTEM_dosagemodule_cpu_temperature": 45.5,
278
+ "SYSTEM_ext1module_alive_count": "52443888",
279
+ "EXT1_1": "1",
280
+ }}, status=200)
275
281
  profile = await api_client.get_hardware_profile()
276
282
  assert profile == {
277
283
  "base_module": True,
@@ -280,8 +286,14 @@ async def test_get_hardware_profile(mock_aioresponse, api_client):
280
286
  "extension_module_2": False,
281
287
  }
282
288
 
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)
289
+ # 3. Base module + Ext1 + Ext2 (via alive counters, no Dosing)
290
+ mock_aioresponse.get(url, payload={"getReadings": {
291
+ "PUMPSTATE": "2",
292
+ "SYSTEM_ext1module_alive_count": "100",
293
+ "SYSTEM_ext2module_alive_count": "200",
294
+ "EXT1_1": "1",
295
+ "EXT2_1": "1",
296
+ }}, status=200)
285
297
  profile = await api_client.get_hardware_profile()
286
298
  assert profile == {
287
299
  "base_module": True,
@@ -290,6 +302,20 @@ async def test_get_hardware_profile(mock_aioresponse, api_client):
290
302
  "extension_module_2": True,
291
303
  }
292
304
 
305
+ # 4. Real-world scenario: EXT2 relay data present (value 0) but no ext2 module
306
+ # Controller always returns EXT2_* keys even when the module is absent.
307
+ mock_aioresponse.get(url, payload={"getReadings": {
308
+ "PUMPSTATE": "2",
309
+ "SYSTEM_dosagemodule_alive_count": "20392243",
310
+ "SYSTEM_ext1module_alive_count": "52443888",
311
+ "EXT1_1": 0, "EXT1_2": 0,
312
+ "EXT2_1": 0, "EXT2_2": 0,
313
+ "EXT2_1_LAST_ON": 0, "EXT2_1_LAST_OFF": 0,
314
+ }}, status=200)
315
+ profile = await api_client.get_hardware_profile()
316
+ assert profile["extension_module_1"] is True
317
+ assert profile["extension_module_2"] is False
318
+
293
319
  @pytest.mark.asyncio
294
320
  async def test_get_hardware_profile_standalone_dosing(mock_aioresponse, standalone_api_client):
295
321
  """Test get_hardware_profile with a standalone dosing configuration."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: violet-poolController-api
3
- Version: 0.0.11
3
+ Version: 0.0.12
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)
@@ -500,23 +500,50 @@ class VioletPoolAPI:
500
500
  def _build_hardware_profile(self, flattened: dict[str, Any]) -> dict[str, bool]:
501
501
  """Build a hardware presence profile from flattened getReadings data.
502
502
 
503
+ Uses the controller's alive-counters (``SYSTEM_*_alive_count``) to
504
+ reliably detect connected hardware modules. The controller always
505
+ emits relay keys (``EXT1_1``, ``EXT2_1``, …) with a default value of
506
+ ``0`` even when the physical module is absent, so checking those keys
507
+ would yield false positives.
508
+
503
509
  Args:
504
510
  flattened: The flattened key-value readings dict.
505
511
 
506
512
  Returns:
507
513
  A dictionary with boolean flags for connected hardware components.
508
514
  """
509
- def is_present(key: str) -> bool:
510
- if not isinstance(flattened, dict):
515
+ def _module_alive(alive_key: str) -> bool:
516
+ val = flattened.get(alive_key)
517
+ if val is None:
518
+ return False
519
+ try:
520
+ return float(str(val).strip()) > 0
521
+ except (ValueError, TypeError):
511
522
  return False
512
- val = flattened.get(key)
513
- return val is not None and str(val).strip().upper() != "N/A"
523
+
524
+ def _any_relay_used(prefix: str) -> bool:
525
+ for key, val in flattened.items():
526
+ if not key.startswith(prefix):
527
+ continue
528
+ if "_LAST_ON" not in key and "_LAST_OFF" not in key and "_RUNTIME" not in key:
529
+ last_on = flattened.get(f"{key}_LAST_ON")
530
+ if last_on is not None:
531
+ try:
532
+ if float(str(last_on).strip()) > 0:
533
+ return True
534
+ except (ValueError, TypeError):
535
+ pass
536
+ return False
537
+
538
+ ext1 = _module_alive("SYSTEM_ext1module_alive_count") or _any_relay_used("EXT1_")
539
+ ext2 = _module_alive("SYSTEM_ext2module_alive_count") or _any_relay_used("EXT2_")
540
+ dosing = self.dosing_standalone or _module_alive("SYSTEM_dosagemodule_alive_count")
514
541
 
515
542
  return {
516
543
  "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"),
544
+ "dosing_module": dosing,
545
+ "extension_module_1": ext1,
546
+ "extension_module_2": ext2,
520
547
  }
521
548
 
522
549
  async def get_readings(self) -> dict[str, Any]: