python-qube-heatpump 1.7.0__tar.gz → 1.9.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.
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/PKG-INFO +1 -1
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/pyproject.toml +1 -1
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/src/python_qube_heatpump/__init__.py +2 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/src/python_qube_heatpump/client.py +10 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/src/python_qube_heatpump/const.py +28 -0
- python_qube_heatpump-1.9.0/src/python_qube_heatpump/models.py +111 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/tests/test_const.py +28 -0
- python_qube_heatpump-1.7.0/src/python_qube_heatpump/models.py +0 -55
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/.github/workflows/ci.yml +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/.github/workflows/python-publish.yml +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/.gitignore +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/AGENTS.md +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/CLAUDE.md +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/LICENSE +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/README.md +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/docs/modbus-lijst-qube-totaal.pdf +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/pytest.ini +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/src/python_qube_heatpump/entities/__init__.py +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/src/python_qube_heatpump/entities/base.py +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/src/python_qube_heatpump/entities/binary_sensors.py +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/src/python_qube_heatpump/entities/sensors.py +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/src/python_qube_heatpump/entities/switches.py +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/src/python_qube_heatpump/network.py +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/src/python_qube_heatpump/py.typed +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/tests/conftest.py +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/tests/test_client.py +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/tests/test_entities.py +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/tests/test_models.py +0 -0
- {python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.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.
|
|
3
|
+
Version: 1.9.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
|
{python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/src/python_qube_heatpump/__init__.py
RENAMED
|
@@ -7,6 +7,7 @@ from .const import (
|
|
|
7
7
|
StatusCode,
|
|
8
8
|
STATUS_CODE_MAP,
|
|
9
9
|
get_status_code,
|
|
10
|
+
resolve_status,
|
|
10
11
|
)
|
|
11
12
|
from .entities import (
|
|
12
13
|
BINARY_SENSORS,
|
|
@@ -37,6 +38,7 @@ __all__ = [
|
|
|
37
38
|
"StatusCode",
|
|
38
39
|
"STATUS_CODE_MAP",
|
|
39
40
|
"get_status_code",
|
|
41
|
+
"resolve_status",
|
|
40
42
|
# Network
|
|
41
43
|
"async_get_mac_address",
|
|
42
44
|
]
|
{python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/src/python_qube_heatpump/client.py
RENAMED
|
@@ -121,6 +121,16 @@ class QubeClient:
|
|
|
121
121
|
state.setpoint_dhw = await _read(const.USER_DHW_SETPOINT)
|
|
122
122
|
|
|
123
123
|
self._apply_monotonic_clamping(state)
|
|
124
|
+
|
|
125
|
+
# Fetch binary sensors
|
|
126
|
+
binary_data = await self.read_all_binary_sensors()
|
|
127
|
+
for key, value in binary_data.items():
|
|
128
|
+
if hasattr(state, key):
|
|
129
|
+
setattr(state, key, value)
|
|
130
|
+
|
|
131
|
+
# Compute unified status (status_code + anti-legionella override)
|
|
132
|
+
state.status = const.resolve_status(state.status_code, state.req_antileg_1)
|
|
133
|
+
|
|
124
134
|
return state
|
|
125
135
|
|
|
126
136
|
@property
|
|
@@ -32,6 +32,7 @@ class StatusCode(str, Enum):
|
|
|
32
32
|
HEATING = "heating"
|
|
33
33
|
START_FAIL = "start_fail"
|
|
34
34
|
HEATING_DHW = "heating_dhw"
|
|
35
|
+
ANTI_LEGIONELLA = "anti_legionella"
|
|
35
36
|
UNKNOWN = "unknown"
|
|
36
37
|
|
|
37
38
|
|
|
@@ -56,6 +57,33 @@ def get_status_code(code: int) -> StatusCode:
|
|
|
56
57
|
return STATUS_CODE_MAP.get(code, StatusCode.UNKNOWN)
|
|
57
58
|
|
|
58
59
|
|
|
60
|
+
def resolve_status(code: int | None, antileg_active: bool | None) -> StatusCode:
|
|
61
|
+
"""Resolve the unified heat pump status.
|
|
62
|
+
|
|
63
|
+
Combines the raw status code with the anti-legionella signal so consumers
|
|
64
|
+
receive a single status value. When anti-legionella is active the heat
|
|
65
|
+
pump is running a special DHW cycle that is otherwise hidden behind the
|
|
66
|
+
regular ``HEATING_DHW`` code; surfacing it as its own status makes the
|
|
67
|
+
device state visible without a separate binary sensor.
|
|
68
|
+
|
|
69
|
+
``ALARM`` always takes precedence over anti-legionella: safety signals
|
|
70
|
+
must not be masked by normal operational modes.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
code: Raw numeric status code from register 38, or ``None`` when
|
|
74
|
+
unavailable.
|
|
75
|
+
antileg_active: Current value of the anti-legionella discrete input
|
|
76
|
+
(``req_antileg_1``), or ``None`` when unavailable.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The resolved :class:`StatusCode` value.
|
|
80
|
+
"""
|
|
81
|
+
status = get_status_code(code) if code is not None else StatusCode.UNKNOWN
|
|
82
|
+
if antileg_active and status is not StatusCode.ALARM:
|
|
83
|
+
return StatusCode.ANTI_LEGIONELLA
|
|
84
|
+
return status
|
|
85
|
+
|
|
86
|
+
|
|
59
87
|
# Register definitions (Address, Type, Data Type, Scale, Offset)
|
|
60
88
|
# Scale/Offset are None if not used.
|
|
61
89
|
# Format: KEY = (Address, ModbusType, DataType, Scale, Offset)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Models for Qube Heat Pump."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .const import StatusCode
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class QubeState:
|
|
11
|
+
"""Representation of the Qube Heat Pump state.
|
|
12
|
+
|
|
13
|
+
Typed fields for core sensors (used by official HA integration).
|
|
14
|
+
Extended dict for additional entities (used by HACS integration).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
# Temperatures
|
|
18
|
+
temp_supply: float | None = None
|
|
19
|
+
temp_return: float | None = None
|
|
20
|
+
temp_source_in: float | None = None
|
|
21
|
+
temp_source_out: float | None = None
|
|
22
|
+
temp_room: float | None = None
|
|
23
|
+
temp_dhw: float | None = None
|
|
24
|
+
temp_outside: float | None = None
|
|
25
|
+
|
|
26
|
+
# Power/Energy
|
|
27
|
+
power_thermic: float | None = None
|
|
28
|
+
power_electric: float | None = None
|
|
29
|
+
energy_total_electric: float | None = None
|
|
30
|
+
energy_total_thermic: float | None = None
|
|
31
|
+
cop_calc: float | None = None
|
|
32
|
+
|
|
33
|
+
# Operation
|
|
34
|
+
status_code: int | None = None
|
|
35
|
+
# Unified status combining status_code with req_antileg_1 override.
|
|
36
|
+
# Computed by QubeClient.get_all_data(); see const.resolve_status().
|
|
37
|
+
status: StatusCode | None = None
|
|
38
|
+
compressor_speed: float | None = None
|
|
39
|
+
flow_rate: float | None = None
|
|
40
|
+
|
|
41
|
+
# Setpoints (Read/Write)
|
|
42
|
+
setpoint_room_heat_day: float | None = None
|
|
43
|
+
setpoint_room_heat_night: float | None = None
|
|
44
|
+
setpoint_room_cool_day: float | None = None
|
|
45
|
+
setpoint_room_cool_night: float | None = None
|
|
46
|
+
setpoint_dhw: float | None = None
|
|
47
|
+
|
|
48
|
+
# Binary sensors - Outputs
|
|
49
|
+
dout_srcpmp_val: bool | None = None
|
|
50
|
+
dout_usrpmp_val: bool | None = None
|
|
51
|
+
dout_fourwayvlv_val: bool | None = None
|
|
52
|
+
dout_cooling_val: bool | None = None
|
|
53
|
+
dout_threewayvlv_val: bool | None = None
|
|
54
|
+
dout_bufferpmp_val: bool | None = None
|
|
55
|
+
dout_heaterstep1_val: bool | None = None
|
|
56
|
+
dout_heaterstep2_val: bool | None = None
|
|
57
|
+
dout_heaterstep3_val: bool | None = None
|
|
58
|
+
|
|
59
|
+
# Binary sensors - System status
|
|
60
|
+
keybonoff: bool | None = None
|
|
61
|
+
daynightmode: bool | None = None
|
|
62
|
+
|
|
63
|
+
# Binary sensors - Alarms
|
|
64
|
+
al_maxtime_antileg_active: bool | None = None
|
|
65
|
+
al_maxtime_dhw_active: bool | None = None
|
|
66
|
+
al_dewpoint_active: bool | None = None
|
|
67
|
+
al_underfloorsafety_active: bool | None = None
|
|
68
|
+
alrm_flw: bool | None = None
|
|
69
|
+
usralrms: bool | None = None
|
|
70
|
+
coolingalrms: bool | None = None
|
|
71
|
+
heatingalrms: bool | None = None
|
|
72
|
+
alarmmng_al_workinghour: bool | None = None
|
|
73
|
+
srsalrm: bool | None = None
|
|
74
|
+
glbal: bool | None = None
|
|
75
|
+
alarmmng_al_pwrplus: bool | None = None
|
|
76
|
+
|
|
77
|
+
# Binary sensors - Sensor/controller status
|
|
78
|
+
roomprb_en: bool | None = None
|
|
79
|
+
plantprb_en: bool | None = None
|
|
80
|
+
bufferprb_en: bool | None = None
|
|
81
|
+
en_dhwpid: bool | None = None
|
|
82
|
+
|
|
83
|
+
# Binary sensors - Demand signals
|
|
84
|
+
plantdemand: bool | None = None
|
|
85
|
+
id_demand: bool | None = None
|
|
86
|
+
thermostatdemand: bool | None = None
|
|
87
|
+
bms_demand: bool | None = None
|
|
88
|
+
|
|
89
|
+
# Binary sensors - Digital inputs
|
|
90
|
+
id_summerwinter: bool | None = None
|
|
91
|
+
dewpoint: bool | None = None
|
|
92
|
+
boostersecurity: bool | None = None
|
|
93
|
+
srcflw: bool | None = None
|
|
94
|
+
req_antileg_1: bool | None = None
|
|
95
|
+
|
|
96
|
+
# Binary sensors - Energy
|
|
97
|
+
surplus_pv: bool | None = None
|
|
98
|
+
|
|
99
|
+
# Extended dict for additional entities not covered by typed fields
|
|
100
|
+
_extended: dict[str, Any] = field(default_factory=dict)
|
|
101
|
+
|
|
102
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
103
|
+
"""Get a value by key, checking typed fields first, then _extended."""
|
|
104
|
+
if hasattr(self, key) and key != "_extended":
|
|
105
|
+
value = getattr(self, key)
|
|
106
|
+
return value if value is not None else default
|
|
107
|
+
return self._extended.get(key, default)
|
|
108
|
+
|
|
109
|
+
def set_extended(self, key: str, value: Any) -> None:
|
|
110
|
+
"""Set a value in the extended dict."""
|
|
111
|
+
self._extended[key] = value
|
|
@@ -4,6 +4,7 @@ from python_qube_heatpump.const import (
|
|
|
4
4
|
StatusCode,
|
|
5
5
|
STATUS_CODE_MAP,
|
|
6
6
|
get_status_code,
|
|
7
|
+
resolve_status,
|
|
7
8
|
)
|
|
8
9
|
|
|
9
10
|
|
|
@@ -55,3 +56,30 @@ def test_get_status_code_unknown():
|
|
|
55
56
|
assert get_status_code(0) == StatusCode.UNKNOWN
|
|
56
57
|
assert get_status_code(99) == StatusCode.UNKNOWN
|
|
57
58
|
assert get_status_code(-1) == StatusCode.UNKNOWN
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_resolve_status_without_antileg():
|
|
62
|
+
"""Without anti-legionella active, resolve_status follows the code map."""
|
|
63
|
+
assert resolve_status(16, False) == StatusCode.HEATING
|
|
64
|
+
assert resolve_status(22, False) == StatusCode.HEATING_DHW
|
|
65
|
+
assert resolve_status(1, None) == StatusCode.STANDBY
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_resolve_status_antileg_overrides_normal_statuses():
|
|
69
|
+
"""Anti-legionella overrides every non-alarm status."""
|
|
70
|
+
assert resolve_status(16, True) == StatusCode.ANTI_LEGIONELLA
|
|
71
|
+
assert resolve_status(22, True) == StatusCode.ANTI_LEGIONELLA
|
|
72
|
+
assert resolve_status(1, True) == StatusCode.ANTI_LEGIONELLA
|
|
73
|
+
assert resolve_status(99, True) == StatusCode.ANTI_LEGIONELLA
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_resolve_status_alarm_not_masked_by_antileg():
|
|
77
|
+
"""ALARM must not be masked by anti-legionella."""
|
|
78
|
+
assert resolve_status(2, True) == StatusCode.ALARM
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_resolve_status_none_code():
|
|
82
|
+
"""None code resolves to UNKNOWN (or ANTI_LEGIONELLA if active)."""
|
|
83
|
+
assert resolve_status(None, False) == StatusCode.UNKNOWN
|
|
84
|
+
assert resolve_status(None, None) == StatusCode.UNKNOWN
|
|
85
|
+
assert resolve_status(None, True) == StatusCode.ANTI_LEGIONELLA
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"""Models for Qube Heat Pump."""
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass, field
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@dataclass
|
|
8
|
-
class QubeState:
|
|
9
|
-
"""Representation of the Qube Heat Pump state.
|
|
10
|
-
|
|
11
|
-
Typed fields for core sensors (used by official HA integration).
|
|
12
|
-
Extended dict for additional entities (used by HACS integration).
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
# Temperatures
|
|
16
|
-
temp_supply: float | None = None
|
|
17
|
-
temp_return: float | None = None
|
|
18
|
-
temp_source_in: float | None = None
|
|
19
|
-
temp_source_out: float | None = None
|
|
20
|
-
temp_room: float | None = None
|
|
21
|
-
temp_dhw: float | None = None
|
|
22
|
-
temp_outside: float | None = None
|
|
23
|
-
|
|
24
|
-
# Power/Energy
|
|
25
|
-
power_thermic: float | None = None
|
|
26
|
-
power_electric: float | None = None
|
|
27
|
-
energy_total_electric: float | None = None
|
|
28
|
-
energy_total_thermic: float | None = None
|
|
29
|
-
cop_calc: float | None = None
|
|
30
|
-
|
|
31
|
-
# Operation
|
|
32
|
-
status_code: int | None = None
|
|
33
|
-
compressor_speed: float | None = None
|
|
34
|
-
flow_rate: float | None = None
|
|
35
|
-
|
|
36
|
-
# Setpoints (Read/Write)
|
|
37
|
-
setpoint_room_heat_day: float | None = None
|
|
38
|
-
setpoint_room_heat_night: float | None = None
|
|
39
|
-
setpoint_room_cool_day: float | None = None
|
|
40
|
-
setpoint_room_cool_night: float | None = None
|
|
41
|
-
setpoint_dhw: float | None = None
|
|
42
|
-
|
|
43
|
-
# Extended dict for additional entities not covered by typed fields
|
|
44
|
-
_extended: dict[str, Any] = field(default_factory=dict)
|
|
45
|
-
|
|
46
|
-
def get(self, key: str, default: Any = None) -> Any:
|
|
47
|
-
"""Get a value by key, checking typed fields first, then _extended."""
|
|
48
|
-
if hasattr(self, key) and key != "_extended":
|
|
49
|
-
value = getattr(self, key)
|
|
50
|
-
return value if value is not None else default
|
|
51
|
-
return self._extended.get(key, default)
|
|
52
|
-
|
|
53
|
-
def set_extended(self, key: str, value: Any) -> None:
|
|
54
|
-
"""Set a value in the extended dict."""
|
|
55
|
-
self._extended[key] = value
|
{python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/.github/ISSUE_TEMPLATE/bug_report.yml
RENAMED
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/.github/ISSUE_TEMPLATE/feature_request.yml
RENAMED
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/.github/workflows/python-publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/src/python_qube_heatpump/entities/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.7.0 → python_qube_heatpump-1.9.0}/src/python_qube_heatpump/network.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|