python-qube-heatpump 1.2.0__py3-none-any.whl → 1.2.1__py3-none-any.whl
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/client.py +84 -145
- python_qube_heatpump/const.py +71 -0
- python_qube_heatpump/models.py +37 -0
- {python_qube_heatpump-1.2.0.dist-info → python_qube_heatpump-1.2.1.dist-info}/METADATA +1 -1
- python_qube_heatpump-1.2.1.dist-info/RECORD +8 -0
- python_qube_heatpump-1.2.0.dist-info/RECORD +0 -6
- {python_qube_heatpump-1.2.0.dist-info → python_qube_heatpump-1.2.1.dist-info}/WHEEL +0 -0
- {python_qube_heatpump-1.2.0.dist-info → python_qube_heatpump-1.2.1.dist-info}/licenses/LICENSE +0 -0
python_qube_heatpump/client.py
CHANGED
|
@@ -1,166 +1,105 @@
|
|
|
1
|
-
"""Qube Heat Pump
|
|
1
|
+
"""Client for Qube Heat Pump."""
|
|
2
2
|
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import contextlib
|
|
7
|
-
import ipaddress
|
|
8
3
|
import logging
|
|
9
|
-
import
|
|
10
|
-
import struct
|
|
11
|
-
from typing import Any
|
|
4
|
+
from typing import Optional
|
|
12
5
|
|
|
13
6
|
from pymodbus.client import AsyncModbusTcpClient
|
|
14
|
-
from pymodbus.
|
|
15
|
-
from pymodbus.
|
|
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__)
|
|
16
14
|
|
|
17
15
|
|
|
18
16
|
class QubeClient:
|
|
19
|
-
"""Qube
|
|
20
|
-
|
|
21
|
-
def __init__(
|
|
22
|
-
|
|
23
|
-
host
|
|
24
|
-
port
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
self._host = host
|
|
29
|
-
self._port = port
|
|
30
|
-
self._unit = unit_id
|
|
31
|
-
self._client: AsyncModbusTcpClient | None = None
|
|
32
|
-
self._io_timeout_s: float = 3.0
|
|
33
|
-
|
|
34
|
-
@property
|
|
35
|
-
def host(self) -> str:
|
|
36
|
-
"""Return host."""
|
|
37
|
-
return self._host
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def port(self) -> int:
|
|
41
|
-
"""Return port."""
|
|
42
|
-
return self._port
|
|
43
|
-
|
|
44
|
-
@property
|
|
45
|
-
def unit(self) -> int:
|
|
46
|
-
"""Return unit ID."""
|
|
47
|
-
return self._unit
|
|
48
|
-
|
|
49
|
-
def set_unit_id(self, unit_id: int) -> None:
|
|
50
|
-
"""Set unit ID."""
|
|
51
|
-
self._unit = int(unit_id)
|
|
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
|
|
52
26
|
|
|
53
27
|
async def connect(self) -> bool:
|
|
54
28
|
"""Connect to the Modbus server."""
|
|
55
|
-
if self.
|
|
56
|
-
self.
|
|
29
|
+
if not self._connected:
|
|
30
|
+
self._connected = await self._client.connect()
|
|
31
|
+
return self._connected
|
|
57
32
|
|
|
58
|
-
|
|
59
|
-
|
|
33
|
+
async def close(self) -> None:
|
|
34
|
+
"""Close connection."""
|
|
35
|
+
self._client.close()
|
|
36
|
+
self._connected = False
|
|
60
37
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
logging.getLogger(__name__).debug("Failed to connect: %s", exc)
|
|
67
|
-
return False
|
|
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.
|
|
68
43
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
+
)
|
|
73
68
|
|
|
74
|
-
async def close(self) -> None:
|
|
75
|
-
"""Close the connection."""
|
|
76
|
-
if self._client:
|
|
77
|
-
with contextlib.suppress(Exception):
|
|
78
|
-
self._client.close()
|
|
79
|
-
self._client = None
|
|
80
|
-
|
|
81
|
-
async def _call(self, method: str, **kwargs: Any) -> Any:
|
|
82
|
-
if self._client is None:
|
|
83
|
-
raise ModbusException("Client not connected")
|
|
84
|
-
func = getattr(self._client, method)
|
|
85
|
-
# Try with 'slave' then 'unit', finally without either
|
|
86
69
|
try:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
except TypeError:
|
|
91
|
-
try:
|
|
92
|
-
resp = await asyncio.wait_for(
|
|
93
|
-
func(**{**kwargs, "unit": self._unit}), timeout=self._io_timeout_s
|
|
70
|
+
if reg_type == const.ModbusType.INPUT:
|
|
71
|
+
result = await self._client.read_input_registers(
|
|
72
|
+
address, count, slave=self.unit
|
|
94
73
|
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
74
|
+
else:
|
|
75
|
+
result = await self._client.read_holding_registers(
|
|
76
|
+
address, count, slave=self.unit
|
|
98
77
|
)
|
|
99
|
-
# Normalize error checking
|
|
100
|
-
if isinstance(resp, ExceptionResponse) or (
|
|
101
|
-
hasattr(resp, "isError") and resp.isError()
|
|
102
|
-
):
|
|
103
|
-
raise ModbusException(f"Modbus error on {method} with {kwargs}")
|
|
104
|
-
return resp
|
|
105
|
-
|
|
106
|
-
async def read_registers(
|
|
107
|
-
self, address: int, count: int, input_type: str = "holding"
|
|
108
|
-
) -> list[int]:
|
|
109
|
-
"""Read registers from the device."""
|
|
110
|
-
if input_type == "input":
|
|
111
|
-
rr = await self._call("read_input_registers", address=address, count=count)
|
|
112
|
-
else:
|
|
113
|
-
rr = await self._call(
|
|
114
|
-
"read_holding_registers", address=address, count=count
|
|
115
|
-
)
|
|
116
78
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return list(regs)
|
|
121
|
-
|
|
122
|
-
@staticmethod
|
|
123
|
-
def decode_registers(regs: list[int], data_type: str | None) -> float | int:
|
|
124
|
-
"""Decode registers to a value."""
|
|
125
|
-
# All decoding assumes big-endian word and byte order.
|
|
126
|
-
if data_type == "float32":
|
|
127
|
-
raw = struct.pack(">HH", int(regs[0]) & 0xFFFF, int(regs[1]) & 0xFFFF)
|
|
128
|
-
return float(struct.unpack(">f", raw)[0])
|
|
129
|
-
if data_type == "int16":
|
|
130
|
-
v = int(regs[0]) & 0xFFFF
|
|
131
|
-
return v - 0x10000 if v & 0x8000 else v
|
|
132
|
-
if data_type == "uint16":
|
|
133
|
-
return int(regs[0]) & 0xFFFF
|
|
134
|
-
if data_type == "uint32":
|
|
135
|
-
return ((int(regs[0]) & 0xFFFF) << 16) | (int(regs[1]) & 0xFFFF)
|
|
136
|
-
if data_type == "int32":
|
|
137
|
-
u = ((int(regs[0]) & 0xFFFF) << 16) | (int(regs[1]) & 0xFFFF)
|
|
138
|
-
return u - 0x1_0000_0000 if u & 0x8000_0000 else u
|
|
139
|
-
# Fallback to first register as unsigned 16-bit
|
|
140
|
-
return int(regs[0]) & 0xFFFF
|
|
141
|
-
|
|
142
|
-
async def resolve_ip(self) -> str | None:
|
|
143
|
-
"""Resolve the host to an IP address."""
|
|
144
|
-
with contextlib.suppress(ValueError):
|
|
145
|
-
return str(ipaddress.ip_address(self._host))
|
|
79
|
+
if result.isError():
|
|
80
|
+
_LOGGER.warning("Error reading address %s", address)
|
|
81
|
+
return None
|
|
146
82
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
self._host,
|
|
150
|
-
None,
|
|
151
|
-
type=socket.SOCK_STREAM,
|
|
83
|
+
decoder = BinaryPayloadDecoder.fromRegisters(
|
|
84
|
+
result.registers, byteorder=Endian.Big, wordorder=Endian.Little
|
|
152
85
|
)
|
|
153
|
-
except OSError:
|
|
154
|
-
return None
|
|
155
86
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-qube-heatpump
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.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
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
python_qube_heatpump/__init__.py,sha256=eW8_tyAweg7YTOI599pi8gf3mt7aXW0SCw5ZyrBVJpU,57
|
|
2
|
+
python_qube_heatpump/client.py,sha256=XNVCydP6uKIlUbH28FjcA9oXYumNRy5NBCqPrVMCX60,3360
|
|
3
|
+
python_qube_heatpump/const.py,sha256=bAFNIN8uwXIdQ5VGgSYdFrQYUNDcDXlxr0OkBxlKUYw,3638
|
|
4
|
+
python_qube_heatpump/models.py,sha256=3I5U9_rEe1r73hwY4sNAMXfIxyPjBOtx06c_mAURLDk,1141
|
|
5
|
+
python_qube_heatpump-1.2.1.dist-info/METADATA,sha256=nqZty88ZKHR7RvM4F1TsxsuFpQoBetXHva29iOv8n1Q,950
|
|
6
|
+
python_qube_heatpump-1.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
+
python_qube_heatpump-1.2.1.dist-info/licenses/LICENSE,sha256=qpuQXN7QwpILG9GYcmqrd3Ax5CxBZUBoT295xTgKnOM,1062
|
|
8
|
+
python_qube_heatpump-1.2.1.dist-info/RECORD,,
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
python_qube_heatpump/__init__.py,sha256=eW8_tyAweg7YTOI599pi8gf3mt7aXW0SCw5ZyrBVJpU,57
|
|
2
|
-
python_qube_heatpump/client.py,sha256=ubYFve5PH_i4Euxyq1vWy5HLcNA00lispL-3c_csH84,5414
|
|
3
|
-
python_qube_heatpump-1.2.0.dist-info/METADATA,sha256=6vXJXkd1y9LEuEMTHAMjv1RB-75i0xNxnvLJBsQi6Sg,950
|
|
4
|
-
python_qube_heatpump-1.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
5
|
-
python_qube_heatpump-1.2.0.dist-info/licenses/LICENSE,sha256=qpuQXN7QwpILG9GYcmqrd3Ax5CxBZUBoT295xTgKnOM,1062
|
|
6
|
-
python_qube_heatpump-1.2.0.dist-info/RECORD,,
|
|
File without changes
|
{python_qube_heatpump-1.2.0.dist-info → python_qube_heatpump-1.2.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|