python-qube-heatpump 1.0.1__tar.gz → 1.2.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.2.0/.gitignore +139 -0
- {python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/PKG-INFO +1 -1
- python_qube_heatpump-1.2.0/client.py +105 -0
- python_qube_heatpump-1.2.0/const.py +71 -0
- python_qube_heatpump-1.2.0/models.py +37 -0
- {python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/pyproject.toml +1 -1
- {python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/.github/workflows/ci.yml +0 -0
- {python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/.github/workflows/python-publish.yml +0 -0
- {python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/LICENSE +0 -0
- {python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/README.md +0 -0
- {python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/pytest.ini +0 -0
- {python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/src/python_qube_heatpump/__init__.py +0 -0
- {python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/src/python_qube_heatpump/client.py +0 -0
- {python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/tests/conftest.py +0 -0
- {python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/tests/test_client.py +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
|
|
77
|
+
# Jupyter Notebook
|
|
78
|
+
.ipynb_checkpoints
|
|
79
|
+
|
|
80
|
+
# IPython
|
|
81
|
+
profile_default/
|
|
82
|
+
ipython_config.py
|
|
83
|
+
|
|
84
|
+
# pyenv
|
|
85
|
+
.python-version
|
|
86
|
+
|
|
87
|
+
# pipenv
|
|
88
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
89
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
90
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
91
|
+
# install all needed dependencies.
|
|
92
|
+
#Pipfile.lock
|
|
93
|
+
|
|
94
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
95
|
+
__pypackages__/
|
|
96
|
+
|
|
97
|
+
# Celery stuff
|
|
98
|
+
celerybeat-schedule
|
|
99
|
+
celerybeat.pid
|
|
100
|
+
|
|
101
|
+
# SageMath parsed files
|
|
102
|
+
*.sage.py
|
|
103
|
+
|
|
104
|
+
# Environments
|
|
105
|
+
.env
|
|
106
|
+
.venv
|
|
107
|
+
env/
|
|
108
|
+
venv/
|
|
109
|
+
ENV/
|
|
110
|
+
env.bak/
|
|
111
|
+
venv.bak/
|
|
112
|
+
|
|
113
|
+
# Spyder project settings
|
|
114
|
+
.spyderproject
|
|
115
|
+
.spyproject
|
|
116
|
+
|
|
117
|
+
# Rope project settings
|
|
118
|
+
.ropeproject
|
|
119
|
+
|
|
120
|
+
# mkdocs documentation
|
|
121
|
+
/site
|
|
122
|
+
|
|
123
|
+
# mypy
|
|
124
|
+
.mypy_cache/
|
|
125
|
+
.dmypy.json
|
|
126
|
+
dmypy.json
|
|
127
|
+
|
|
128
|
+
# Pyre type checker
|
|
129
|
+
.pyre/
|
|
130
|
+
|
|
131
|
+
# pytype static type analyzer
|
|
132
|
+
.pytype/
|
|
133
|
+
|
|
134
|
+
# Cython debug symbols
|
|
135
|
+
cython_debug/
|
|
136
|
+
|
|
137
|
+
# IDEs
|
|
138
|
+
.idea/
|
|
139
|
+
.vscode/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-qube-heatpump
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.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
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Client for Qube Heat Pump."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pymodbus.client import AsyncModbusTcpClient
|
|
7
|
+
from pymodbus.payload import BinaryPayloadDecoder
|
|
8
|
+
from pymodbus.constants import Endian
|
|
9
|
+
|
|
10
|
+
from . import const
|
|
11
|
+
from .models import QubeState
|
|
12
|
+
|
|
13
|
+
_LOGGER = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class QubeClient:
|
|
17
|
+
"""Qube Modbus Client."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, host: str, port: int = 502, unit_id: int = 1):
|
|
20
|
+
"""Initialize."""
|
|
21
|
+
self.host = host
|
|
22
|
+
self.port = port
|
|
23
|
+
self.unit = unit_id
|
|
24
|
+
self._client = AsyncModbusTcpClient(host, port=port)
|
|
25
|
+
self._connected = False
|
|
26
|
+
|
|
27
|
+
async def connect(self) -> bool:
|
|
28
|
+
"""Connect to the Modbus server."""
|
|
29
|
+
if not self._connected:
|
|
30
|
+
self._connected = await self._client.connect()
|
|
31
|
+
return self._connected
|
|
32
|
+
|
|
33
|
+
async def close(self) -> None:
|
|
34
|
+
"""Close connection."""
|
|
35
|
+
self._client.close()
|
|
36
|
+
self._connected = False
|
|
37
|
+
|
|
38
|
+
async def get_all_data(self) -> QubeState:
|
|
39
|
+
"""Fetch all definition data and return a state object."""
|
|
40
|
+
# Note: In a real implementation you might want to optimize this
|
|
41
|
+
# by reading contiguous blocks instead of one-by-one.
|
|
42
|
+
# For now, we wrap the individual reads for abstraction.
|
|
43
|
+
|
|
44
|
+
state = QubeState()
|
|
45
|
+
|
|
46
|
+
# Helper to read and assign
|
|
47
|
+
async def _read(const_def):
|
|
48
|
+
return await self.read_value(const_def)
|
|
49
|
+
|
|
50
|
+
# Fetch basic sensors
|
|
51
|
+
state.temp_supply = await _read(const.TEMP_SUPPLY)
|
|
52
|
+
state.temp_return = await _read(const.TEMP_RETURN)
|
|
53
|
+
state.temp_outside = await _read(const.TEMP_OUTSIDE)
|
|
54
|
+
state.temp_dhw = await _read(const.TEMP_DHW)
|
|
55
|
+
|
|
56
|
+
return state
|
|
57
|
+
|
|
58
|
+
async def read_value(self, definition: tuple) -> Optional[float]:
|
|
59
|
+
"""Read a single value based on the constant definition."""
|
|
60
|
+
address, reg_type, data_type, scale, offset = definition
|
|
61
|
+
|
|
62
|
+
count = (
|
|
63
|
+
2
|
|
64
|
+
if data_type
|
|
65
|
+
in (const.DataType.FLOAT32, const.DataType.UINT32, const.DataType.INT32)
|
|
66
|
+
else 1
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
if reg_type == const.ModbusType.INPUT:
|
|
71
|
+
result = await self._client.read_input_registers(
|
|
72
|
+
address, count, slave=self.unit
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
result = await self._client.read_holding_registers(
|
|
76
|
+
address, count, slave=self.unit
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if result.isError():
|
|
80
|
+
_LOGGER.warning("Error reading address %s", address)
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
decoder = BinaryPayloadDecoder.fromRegisters(
|
|
84
|
+
result.registers, byteorder=Endian.Big, wordorder=Endian.Little
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if data_type == const.DataType.FLOAT32:
|
|
88
|
+
val = decoder.decode_32bit_float()
|
|
89
|
+
elif data_type == const.DataType.INT16:
|
|
90
|
+
val = decoder.decode_16bit_int()
|
|
91
|
+
elif data_type == const.DataType.UINT16:
|
|
92
|
+
val = decoder.decode_16bit_uint()
|
|
93
|
+
else:
|
|
94
|
+
val = 0
|
|
95
|
+
|
|
96
|
+
if scale is not None:
|
|
97
|
+
val *= scale
|
|
98
|
+
if offset is not None:
|
|
99
|
+
val += offset
|
|
100
|
+
|
|
101
|
+
return val
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
_LOGGER.error("Exception reading address %s: %s", address, e)
|
|
105
|
+
return None
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Constants for Qube Heat Pump."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ModbusType(str, Enum):
|
|
7
|
+
"""Modbus register type."""
|
|
8
|
+
|
|
9
|
+
HOLDING = "holding"
|
|
10
|
+
INPUT = "input"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DataType(str, Enum):
|
|
14
|
+
"""Data type."""
|
|
15
|
+
|
|
16
|
+
FLOAT32 = "float32"
|
|
17
|
+
INT16 = "int16"
|
|
18
|
+
UINT16 = "uint16"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Register definitions (Address, Type, Data Type, Scale, Offset)
|
|
22
|
+
# Scale/Offset are None if not used.
|
|
23
|
+
# Format: KEY = (Address, ModbusType, DataType, Scale, Offset)
|
|
24
|
+
|
|
25
|
+
# --- Sensors (Input Registers) ---
|
|
26
|
+
PCT_USER_PUMP = (4, ModbusType.INPUT, DataType.FLOAT32, -1, 100)
|
|
27
|
+
PCT_SOURCE_PUMP = (6, ModbusType.INPUT, DataType.FLOAT32, -1, 100)
|
|
28
|
+
PCT_SOURCE_VALVE = (8, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
29
|
+
REQ_DHW = (14, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
30
|
+
REQ_COMPRESSOR = (16, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
31
|
+
FLOW_RATE = (18, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
32
|
+
TEMP_SUPPLY = (20, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
33
|
+
TEMP_RETURN = (22, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
34
|
+
TEMP_SOURCE_IN = (24, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
35
|
+
TEMP_SOURCE_OUT = (26, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
36
|
+
TEMP_ROOM = (28, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
37
|
+
TEMP_DHW = (30, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
38
|
+
TEMP_OUTSIDE = (32, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
39
|
+
COP_CALC = (34, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
40
|
+
POWER_THERMIC = (36, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
41
|
+
STATUS_CODE = (38, ModbusType.INPUT, DataType.UINT16, None, None)
|
|
42
|
+
TEMP_REG_SETPOINT = (39, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
43
|
+
TEMP_COOL_SETPOINT = (41, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
44
|
+
TEMP_HEAT_SETPOINT = (43, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
45
|
+
COMPRESSOR_SPEED = (45, ModbusType.INPUT, DataType.FLOAT32, 60, None) # RPM
|
|
46
|
+
TEMP_DHW_SETPOINT = (47, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
47
|
+
HOURS_DHW = (50, ModbusType.INPUT, DataType.INT16, None, None)
|
|
48
|
+
HOURS_HEAT = (52, ModbusType.INPUT, DataType.INT16, None, None)
|
|
49
|
+
HOURS_COOL = (54, ModbusType.INPUT, DataType.INT16, None, None)
|
|
50
|
+
HOURS_HEATER_1 = (56, ModbusType.INPUT, DataType.INT16, None, None)
|
|
51
|
+
HOURS_HEATER_2 = (58, ModbusType.INPUT, DataType.INT16, None, None)
|
|
52
|
+
HOURS_HEATER_3 = (60, ModbusType.INPUT, DataType.INT16, None, None)
|
|
53
|
+
POWER_ELECTRIC_CALC = (61, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
54
|
+
TEMP_PLANT_SETPOINT = (65, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
55
|
+
ENERGY_ELECTRIC_TOTAL = (69, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
56
|
+
ENERGY_THERMIC_TOTAL = (71, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
57
|
+
TEMP_ROOM_MODBUS = (75, ModbusType.INPUT, DataType.FLOAT32, None, None)
|
|
58
|
+
|
|
59
|
+
# --- Configuration (Holding Registers) ---
|
|
60
|
+
SETPOINT_HEAT_DAY = (27, ModbusType.HOLDING, DataType.FLOAT32, None, None)
|
|
61
|
+
SETPOINT_HEAT_NIGHT = (29, ModbusType.HOLDING, DataType.FLOAT32, None, None)
|
|
62
|
+
SETPOINT_COOL_DAY = (31, ModbusType.HOLDING, DataType.FLOAT32, None, None)
|
|
63
|
+
SETPOINT_COOL_NIGHT = (33, ModbusType.HOLDING, DataType.FLOAT32, None, None)
|
|
64
|
+
DT_DHW = (43, ModbusType.HOLDING, DataType.INT16, None, None)
|
|
65
|
+
MIN_TEMP_DHW = (44, ModbusType.HOLDING, DataType.FLOAT32, None, None)
|
|
66
|
+
TEMP_DHW_PROG = (46, ModbusType.HOLDING, DataType.FLOAT32, None, None)
|
|
67
|
+
MIN_SETPOINT_BUFFER = (99, ModbusType.HOLDING, DataType.FLOAT32, None, None)
|
|
68
|
+
USER_HEAT_SETPOINT = (101, ModbusType.HOLDING, DataType.FLOAT32, None, None)
|
|
69
|
+
USER_COOL_SETPOINT = (103, ModbusType.HOLDING, DataType.FLOAT32, None, None)
|
|
70
|
+
MAX_SETPOINT_BUFFER = (169, ModbusType.HOLDING, DataType.FLOAT32, None, None)
|
|
71
|
+
USER_DHW_SETPOINT = (173, ModbusType.HOLDING, DataType.FLOAT32, None, None)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Models for Qube Heat Pump."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class QubeState:
|
|
9
|
+
"""Representation of the Qube Heat Pump state."""
|
|
10
|
+
|
|
11
|
+
# Temperatures
|
|
12
|
+
temp_supply: Optional[float] = None
|
|
13
|
+
temp_return: Optional[float] = None
|
|
14
|
+
temp_source_in: Optional[float] = None
|
|
15
|
+
temp_source_out: Optional[float] = None
|
|
16
|
+
temp_room: Optional[float] = None
|
|
17
|
+
temp_dhw: Optional[float] = None
|
|
18
|
+
temp_outside: Optional[float] = None
|
|
19
|
+
|
|
20
|
+
# Power/Energy
|
|
21
|
+
power_thermic: Optional[float] = None
|
|
22
|
+
power_electric: Optional[float] = None
|
|
23
|
+
energy_total_electric: Optional[float] = None
|
|
24
|
+
energy_total_thermic: Optional[float] = None
|
|
25
|
+
cop_calc: Optional[float] = None
|
|
26
|
+
|
|
27
|
+
# Operation
|
|
28
|
+
status_code: Optional[int] = None
|
|
29
|
+
compressor_speed: Optional[float] = None
|
|
30
|
+
flow_rate: Optional[float] = None
|
|
31
|
+
|
|
32
|
+
# Setpoints (Read/Write)
|
|
33
|
+
setpoint_room_heat_day: Optional[float] = None
|
|
34
|
+
setpoint_room_heat_night: Optional[float] = None
|
|
35
|
+
setpoint_room_cool_day: Optional[float] = None
|
|
36
|
+
setpoint_room_cool_night: Optional[float] = None
|
|
37
|
+
setpoint_dhw: Optional[float] = None
|
{python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/.github/ISSUE_TEMPLATE/bug_report.yml
RENAMED
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/.github/ISSUE_TEMPLATE/feature_request.yml
RENAMED
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/.github/workflows/python-publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/src/python_qube_heatpump/__init__.py
RENAMED
|
File without changes
|
{python_qube_heatpump-1.0.1 → python_qube_heatpump-1.2.0}/src/python_qube_heatpump/client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|