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.
@@ -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.1
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "python-qube-heatpump"
7
- version = "1.0.1"
7
+ version = "1.2.0"
8
8
  authors = [
9
9
  { name="MattieGit", email="6250046+MattieGit@users.noreply.github.com" },
10
10
  ]