python-qube-heatpump 1.10.0__tar.gz → 1.11.1__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.
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/PKG-INFO +1 -1
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/pyproject.toml +1 -1
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/client.py +42 -1
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/tests/test_client.py +94 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/.github/workflows/ci.yml +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/.github/workflows/python-publish.yml +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/.gitignore +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/AGENTS.md +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/CLAUDE.md +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/LICENSE +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/README.md +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/docs/modbus-lijst-qube-totaal.pdf +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/pytest.ini +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/__init__.py +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/const.py +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/entities/__init__.py +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/entities/base.py +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/entities/binary_sensors.py +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/entities/sensors.py +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/entities/switches.py +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/models.py +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/network.py +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/py.typed +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/tests/conftest.py +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/tests/test_const.py +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/tests/test_entities.py +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/tests/test_models.py +0 -0
- {python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/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.
|
|
3
|
+
Version: 1.11.1
|
|
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
|
{python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/client.py
RENAMED
|
@@ -192,7 +192,7 @@ class QubeClient:
|
|
|
192
192
|
Version as string (e.g., "2.15"), or None on error.
|
|
193
193
|
"""
|
|
194
194
|
value = await self.read_value(const.SOFTWARE_VERSION)
|
|
195
|
-
if value is None:
|
|
195
|
+
if value is None or value == 0.0:
|
|
196
196
|
return None
|
|
197
197
|
return str(round(value, 2))
|
|
198
198
|
|
|
@@ -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
|
{python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/.github/ISSUE_TEMPLATE/bug_report.yml
RENAMED
|
File without changes
|
{python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/.github/ISSUE_TEMPLATE/config.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/.github/workflows/python-publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/docs/modbus-lijst-qube-totaal.pdf
RENAMED
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/__init__.py
RENAMED
|
File without changes
|
{python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/const.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/models.py
RENAMED
|
File without changes
|
{python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/network.py
RENAMED
|
File without changes
|
{python_qube_heatpump-1.10.0 → python_qube_heatpump-1.11.1}/src/python_qube_heatpump/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|