python-qube-heatpump 1.2.0__tar.gz → 1.2.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.2.0 → python_qube_heatpump-1.2.1}/PKG-INFO +1 -1
- {python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/pyproject.toml +1 -1
- python_qube_heatpump-1.2.0/src/python_qube_heatpump/client.py +0 -166
- {python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/.github/workflows/ci.yml +0 -0
- {python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/.github/workflows/python-publish.yml +0 -0
- {python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/.gitignore +0 -0
- {python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/LICENSE +0 -0
- {python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/README.md +0 -0
- {python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/pytest.ini +0 -0
- {python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/src/python_qube_heatpump/__init__.py +0 -0
- {python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1/src/python_qube_heatpump}/client.py +0 -0
- {python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1/src/python_qube_heatpump}/const.py +0 -0
- {python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1/src/python_qube_heatpump}/models.py +0 -0
- {python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/tests/conftest.py +0 -0
- {python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/tests/test_client.py +0 -0
|
@@ -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
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
"""Qube Heat Pump Client Library."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import contextlib
|
|
7
|
-
import ipaddress
|
|
8
|
-
import logging
|
|
9
|
-
import socket
|
|
10
|
-
import struct
|
|
11
|
-
from typing import Any
|
|
12
|
-
|
|
13
|
-
from pymodbus.client import AsyncModbusTcpClient
|
|
14
|
-
from pymodbus.exceptions import ModbusException
|
|
15
|
-
from pymodbus.pdu import ExceptionResponse
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class QubeClient:
|
|
19
|
-
"""Qube Heat Pump Client."""
|
|
20
|
-
|
|
21
|
-
def __init__(
|
|
22
|
-
self,
|
|
23
|
-
host: str,
|
|
24
|
-
port: int,
|
|
25
|
-
unit_id: int = 1,
|
|
26
|
-
) -> None:
|
|
27
|
-
"""Initialize the client."""
|
|
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)
|
|
52
|
-
|
|
53
|
-
async def connect(self) -> bool:
|
|
54
|
-
"""Connect to the Modbus server."""
|
|
55
|
-
if self._client is None:
|
|
56
|
-
self._client = AsyncModbusTcpClient(self._host, port=self._port)
|
|
57
|
-
|
|
58
|
-
if self.is_connected:
|
|
59
|
-
return True
|
|
60
|
-
|
|
61
|
-
try:
|
|
62
|
-
return await asyncio.wait_for(
|
|
63
|
-
self._client.connect(), timeout=self._io_timeout_s
|
|
64
|
-
)
|
|
65
|
-
except Exception as exc: # pylint: disable=broad-except # noqa: BLE001
|
|
66
|
-
logging.getLogger(__name__).debug("Failed to connect: %s", exc)
|
|
67
|
-
return False
|
|
68
|
-
|
|
69
|
-
@property
|
|
70
|
-
def is_connected(self) -> bool:
|
|
71
|
-
"""Return True if connected."""
|
|
72
|
-
return bool(self._client and getattr(self._client, "connected", False))
|
|
73
|
-
|
|
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
|
-
try:
|
|
87
|
-
resp = await asyncio.wait_for(
|
|
88
|
-
func(**{**kwargs, "slave": self._unit}), timeout=self._io_timeout_s
|
|
89
|
-
)
|
|
90
|
-
except TypeError:
|
|
91
|
-
try:
|
|
92
|
-
resp = await asyncio.wait_for(
|
|
93
|
-
func(**{**kwargs, "unit": self._unit}), timeout=self._io_timeout_s
|
|
94
|
-
)
|
|
95
|
-
except TypeError:
|
|
96
|
-
resp = await asyncio.wait_for(
|
|
97
|
-
func(**kwargs), timeout=self._io_timeout_s
|
|
98
|
-
)
|
|
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
|
-
|
|
117
|
-
regs = getattr(rr, "registers", None)
|
|
118
|
-
if regs is None:
|
|
119
|
-
raise ModbusException("No registers returned")
|
|
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))
|
|
146
|
-
|
|
147
|
-
try:
|
|
148
|
-
infos = await asyncio.get_running_loop().getaddrinfo(
|
|
149
|
-
self._host,
|
|
150
|
-
None,
|
|
151
|
-
type=socket.SOCK_STREAM,
|
|
152
|
-
)
|
|
153
|
-
except OSError:
|
|
154
|
-
return None
|
|
155
|
-
|
|
156
|
-
for family, _, _, _, sockaddr in infos:
|
|
157
|
-
if not sockaddr:
|
|
158
|
-
continue
|
|
159
|
-
addr = sockaddr[0]
|
|
160
|
-
if not isinstance(addr, str):
|
|
161
|
-
continue
|
|
162
|
-
if family == socket.AF_INET6 and addr.startswith("::ffff:"):
|
|
163
|
-
addr = addr.removeprefix("::ffff:")
|
|
164
|
-
return addr
|
|
165
|
-
|
|
166
|
-
return None
|
{python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/.github/ISSUE_TEMPLATE/bug_report.yml
RENAMED
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/.github/ISSUE_TEMPLATE/feature_request.yml
RENAMED
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/.github/workflows/python-publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1}/src/python_qube_heatpump/__init__.py
RENAMED
|
File without changes
|
{python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1/src/python_qube_heatpump}/client.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_qube_heatpump-1.2.0 → python_qube_heatpump-1.2.1/src/python_qube_heatpump}/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|