python-qube-heatpump 1.10.0__tar.gz → 1.11.0__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 (31) hide show
  1. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/PKG-INFO +1 -1
  2. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/pyproject.toml +1 -1
  3. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/src/python_qube_heatpump/client.py +41 -0
  4. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/tests/test_client.py +94 -0
  5. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  6. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  7. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  8. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/.github/workflows/ci.yml +0 -0
  9. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/.github/workflows/python-publish.yml +0 -0
  10. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/.gitignore +0 -0
  11. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/AGENTS.md +0 -0
  12. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/CLAUDE.md +0 -0
  13. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/LICENSE +0 -0
  14. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/README.md +0 -0
  15. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/docs/modbus-lijst-qube-totaal.pdf +0 -0
  16. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/pytest.ini +0 -0
  17. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/src/python_qube_heatpump/__init__.py +0 -0
  18. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/src/python_qube_heatpump/const.py +0 -0
  19. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/src/python_qube_heatpump/entities/__init__.py +0 -0
  20. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/src/python_qube_heatpump/entities/base.py +0 -0
  21. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/src/python_qube_heatpump/entities/binary_sensors.py +0 -0
  22. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/src/python_qube_heatpump/entities/sensors.py +0 -0
  23. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/src/python_qube_heatpump/entities/switches.py +0 -0
  24. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/src/python_qube_heatpump/models.py +0 -0
  25. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/src/python_qube_heatpump/network.py +0 -0
  26. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/src/python_qube_heatpump/py.typed +0 -0
  27. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/tests/conftest.py +0 -0
  28. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/tests/test_const.py +0 -0
  29. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/tests/test_entities.py +0 -0
  30. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/tests/test_models.py +0 -0
  31. {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.0}/tests/test_network.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-qube-heatpump
3
- Version: 1.10.0
3
+ Version: 1.11.0
4
4
  Summary: Async Modbus client for Qube Heat Pumps
5
5
  Project-URL: Homepage, https://github.com/MattieGit/python-qube-heatpump
6
6
  Project-URL: Bug Tracker, https://github.com/MattieGit/python-qube-heatpump/issues
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "python-qube-heatpump"
7
- version = "1.10.0"
7
+ version = "1.11.0"
8
8
  authors = [
9
9
  { name="MattieGit", email="6250046+MattieGit@users.noreply.github.com" },
10
10
  ]
@@ -512,6 +512,47 @@ class QubeClient:
512
512
  _LOGGER.error("Exception writing switch %s: %s", key, e)
513
513
  return False
514
514
 
515
+ # SG Ready mode API
516
+ SG_READY_MODES = ("off", "block", "plus", "max")
517
+ _SGREADY_MODE_TO_BITS: dict[str, tuple[bool, bool]] = {
518
+ "off": (False, False),
519
+ "block": (True, False),
520
+ "plus": (False, True),
521
+ "max": (True, True),
522
+ }
523
+ _SGREADY_BITS_TO_MODE: dict[tuple[bool, bool], str] = {
524
+ v: k for k, v in _SGREADY_MODE_TO_BITS.items()
525
+ }
526
+
527
+ async def get_sg_ready_mode(self) -> str | None:
528
+ """Read the current SG Ready mode.
529
+
530
+ Returns:
531
+ Mode string ("off", "block", "plus", "max"), or None on error.
532
+ """
533
+ bit_a = await self.read_switch("bms_sgready_a")
534
+ bit_b = await self.read_switch("bms_sgready_b")
535
+ if bit_a is None or bit_b is None:
536
+ return None
537
+ return self._SGREADY_BITS_TO_MODE.get((bool(bit_a), bool(bit_b)))
538
+
539
+ async def set_sg_ready_mode(self, mode: str) -> bool:
540
+ """Set the SG Ready mode.
541
+
542
+ Args:
543
+ mode: One of "off", "block", "plus", "max".
544
+
545
+ Returns:
546
+ True if both writes succeeded, False otherwise.
547
+ """
548
+ bits = self._SGREADY_MODE_TO_BITS.get(mode)
549
+ if bits is None:
550
+ _LOGGER.warning("Unknown SG Ready mode: %s", mode)
551
+ return False
552
+ success_a = await self.write_switch("bms_sgready_a", bits[0])
553
+ success_b = await self.write_switch("bms_sgready_b", bits[1])
554
+ return success_a and success_b
555
+
515
556
  async def write_setpoint(self, key: str, value: float) -> bool:
516
557
  """Write a setpoint value by key.
517
558
 
@@ -435,3 +435,97 @@ async def test_get_all_data_applies_clamping(mock_modbus_client):
435
435
  # Energy values stored in cache
436
436
  assert client.monotonic_cache.get("energy_total_electric") == 0.0
437
437
  assert client.monotonic_cache.get("energy_total_thermic") == 0.0
438
+
439
+
440
+ @pytest.mark.asyncio
441
+ async def test_get_sg_ready_mode(mock_modbus_client):
442
+ """Test reading SG Ready mode from coil bits."""
443
+ client = QubeClient("1.2.3.4", 502)
444
+ mock_instance = mock_modbus_client.return_value
445
+ client._client = mock_instance
446
+
447
+ # Mock coil reads: A=False, B=True -> "plus"
448
+ mock_resp_a = MagicMock()
449
+ mock_resp_a.isError.return_value = False
450
+ mock_resp_a.bits = [False]
451
+
452
+ mock_resp_b = MagicMock()
453
+ mock_resp_b.isError.return_value = False
454
+ mock_resp_b.bits = [True]
455
+
456
+ mock_instance.read_coils = AsyncMock(side_effect=[mock_resp_a, mock_resp_b])
457
+
458
+ result = await client.get_sg_ready_mode()
459
+ assert result == "plus"
460
+
461
+
462
+ @pytest.mark.asyncio
463
+ async def test_get_sg_ready_mode_all_modes(mock_modbus_client):
464
+ """Test all SG Ready mode combinations."""
465
+ client = QubeClient("1.2.3.4", 502)
466
+ mock_instance = mock_modbus_client.return_value
467
+ client._client = mock_instance
468
+
469
+ cases = [
470
+ ((False, False), "off"),
471
+ ((True, False), "block"),
472
+ ((False, True), "plus"),
473
+ ((True, True), "max"),
474
+ ]
475
+ for (bit_a, bit_b), expected_mode in cases:
476
+ resp_a = MagicMock()
477
+ resp_a.isError.return_value = False
478
+ resp_a.bits = [bit_a]
479
+ resp_b = MagicMock()
480
+ resp_b.isError.return_value = False
481
+ resp_b.bits = [bit_b]
482
+ mock_instance.read_coils = AsyncMock(side_effect=[resp_a, resp_b])
483
+
484
+ result = await client.get_sg_ready_mode()
485
+ assert result == expected_mode, (
486
+ f"Expected {expected_mode} for bits ({bit_a}, {bit_b})"
487
+ )
488
+
489
+
490
+ @pytest.mark.asyncio
491
+ async def test_get_sg_ready_mode_read_error(mock_modbus_client):
492
+ """Test SG Ready mode returns None on read error."""
493
+ client = QubeClient("1.2.3.4", 502)
494
+ mock_instance = mock_modbus_client.return_value
495
+ client._client = mock_instance
496
+
497
+ mock_resp = MagicMock()
498
+ mock_resp.isError.return_value = True
499
+ mock_instance.read_coils = AsyncMock(return_value=mock_resp)
500
+
501
+ result = await client.get_sg_ready_mode()
502
+ assert result is None
503
+
504
+
505
+ @pytest.mark.asyncio
506
+ async def test_set_sg_ready_mode(mock_modbus_client):
507
+ """Test setting SG Ready mode writes both coils."""
508
+ client = QubeClient("1.2.3.4", 502)
509
+ mock_instance = mock_modbus_client.return_value
510
+ client._client = mock_instance
511
+
512
+ mock_resp = MagicMock()
513
+ mock_resp.isError.return_value = False
514
+ mock_instance.write_coil = AsyncMock(return_value=mock_resp)
515
+
516
+ result = await client.set_sg_ready_mode("plus")
517
+ assert result is True
518
+
519
+ # "plus" = (False, True) -> A=False, B=True
520
+ calls = mock_instance.write_coil.call_args_list
521
+ assert len(calls) == 2
522
+ assert calls[0].args == (65, False) # bms_sgready_a address
523
+ assert calls[1].args == (66, True) # bms_sgready_b address
524
+
525
+
526
+ @pytest.mark.asyncio
527
+ async def test_set_sg_ready_mode_unknown(mock_modbus_client):
528
+ """Test setting unknown SG Ready mode returns False."""
529
+ client = QubeClient("1.2.3.4", 502)
530
+ result = await client.set_sg_ready_mode("turbo")
531
+ assert result is False