|
5 | 5 | from __future__ import annotations |
6 | 6 |
|
7 | 7 | import logging |
8 | | -import socket |
9 | | -import struct |
10 | | -import time |
11 | 8 |
|
12 | 9 | from luxtronik.common import get_host_lock |
13 | | -from luxtronik.calculations import Calculations |
14 | | -from luxtronik.parameters import Parameters |
15 | | -from luxtronik.visibilities import Visibilities |
16 | 10 | from luxtronik.discover import discover # noqa: F401 |
17 | | -from luxtronik.constants import ( |
| 11 | + |
| 12 | +from luxtronik.cfi import ( |
18 | 13 | LUXTRONIK_DEFAULT_PORT, |
19 | | - LUXTRONIK_PARAMETERS_WRITE, |
20 | | - LUXTRONIK_PARAMETERS_READ, |
21 | | - LUXTRONIK_CALCULATIONS_READ, |
22 | | - LUXTRONIK_VISIBILITIES_READ, |
23 | | - LUXTRONIK_SOCKET_READ_SIZE_INTEGER, |
24 | | - LUXTRONIK_SOCKET_READ_SIZE_CHAR, |
| 14 | + Calculations, # noqa: F401 |
| 15 | + Parameters, # noqa: F401 |
| 16 | + Visibilities, # noqa: F401 |
| 17 | + LuxtronikData, |
| 18 | + LuxtronikSocketInterface, |
25 | 19 | ) |
26 | | -from luxtronik.shi import resolve_version |
27 | | -from luxtronik.shi.modbus import LuxtronikModbusTcpInterface |
28 | | -from luxtronik.shi.holdings import Holdings |
29 | | -from luxtronik.shi.interface import LuxtronikSmartHomeData, LuxtronikSmartHomeInterface |
30 | | -from luxtronik.shi.constants import ( |
| 20 | +from luxtronik.shi import ( |
31 | 21 | LUXTRONIK_DEFAULT_MODBUS_PORT, |
| 22 | + LuxtronikModbusTcpInterface, |
| 23 | + Holdings, # noqa: F401 |
| 24 | + Inputs, # noqa: F401 |
| 25 | + LuxtronikSmartHomeData, |
| 26 | + LuxtronikSmartHomeInterface, |
| 27 | + resolve_version, |
32 | 28 | ) |
33 | | - |
34 | 29 | # endregion Imports |
35 | 30 |
|
36 | 31 | LOGGER = logging.getLogger("Luxtronik") |
37 | 32 |
|
38 | | -# Wait time (in seconds) after writing parameters to give controller |
39 | | -# some time to re-calculate values, etc. |
40 | | -WAIT_TIME_AFTER_PARAMETER_WRITE = 1 |
41 | | - |
42 | | - |
43 | | -class LuxtronikData: |
44 | | - """ |
45 | | - Collection of parameters, calculations and visiblities. |
46 | | - Also provide some high level access functions to their data values. |
47 | | - """ |
48 | | - |
49 | | - def __init__(self, parameters=None, calculations=None, visibilities=None, safe=True): |
50 | | - self.parameters = Parameters(safe) if parameters is None else parameters |
51 | | - self.calculations = Calculations() if calculations is None else calculations |
52 | | - self.visibilities = Visibilities() if visibilities is None else visibilities |
53 | | - |
54 | | - def get_firmware_version(self): |
55 | | - return self.calculations.get_firmware_version() |
56 | | - |
57 | | - |
58 | | -class LuxtronikSocketInterface: |
59 | | - """Luxtronik read/write interface via socket.""" |
60 | | - |
61 | | - def __init__(self, host, port=LUXTRONIK_DEFAULT_PORT): |
62 | | - # Acquire a lock object for this host to ensure thread safety |
63 | | - self._lock = get_host_lock(host) |
64 | | - |
65 | | - self._host = host |
66 | | - self._port = port |
67 | | - self._socket = None |
68 | | - |
69 | | - @property |
70 | | - def lock(self): |
71 | | - return self._lock |
72 | | - |
73 | | - def _with_lock_and_connect(self, func, *args, **kwargs): |
74 | | - """ |
75 | | - Decorator around various read/write functions to connect first. |
76 | | -
|
77 | | - This method is essentially a wrapper for the _read() and _write() methods. |
78 | | - Locking is being used to ensure that only a single socket operation is |
79 | | - performed at any point in time. This helps to avoid issues with the |
80 | | - Luxtronik controller, which seems unstable otherwise. |
81 | | - """ |
82 | | - with self.lock: |
83 | | - try: |
84 | | - ret_val = None |
85 | | - with socket.create_connection((self._host, self._port)) as sock: |
86 | | - self._socket = sock |
87 | | - LOGGER.info("Connected to Luxtronik heat pump %s:%s", self._host, self._port) |
88 | | - ret_val = func(*args, **kwargs) |
89 | | - except socket.gaierror as e: |
90 | | - LOGGER.error("Failed to connect to Luxtronik heat pump %s:%s. %s.", |
91 | | - self._host, self._port, f"Address-related error: {e}") |
92 | | - except socket.timeout as e: |
93 | | - LOGGER.error("Failed to connect to Luxtronik heat pump %s:%s. %s.", |
94 | | - self._host, self._port, f"Connection timed out: {e}") |
95 | | - except ConnectionRefusedError as e: |
96 | | - LOGGER.error("Failed to connect to Luxtronik heat pump %s:%s. %s.", |
97 | | - self._host, self._port, f"Connection refused: {e}") |
98 | | - except OSError as e: |
99 | | - LOGGER.error("Failed to connect to Luxtronik heat pump %s:%s. %s.", |
100 | | - self._host, self._port, f"OS error during connect: {e}") |
101 | | - except Exception as e: |
102 | | - LOGGER.error("Failed to connect to Luxtronik heat pump %s:%s. %s.", |
103 | | - self._host, self._port, f"Unknown exception: {e}") |
104 | | - self._socket = None |
105 | | - return ret_val |
106 | | - |
107 | | - def read(self, data=None): |
108 | | - """ |
109 | | - All available data will be read from the heat pump |
110 | | - and integrated to the passed data object. |
111 | | - This data object is returned afterwards, mainly for access to a newly created. |
112 | | - """ |
113 | | - if data is None: |
114 | | - data = LuxtronikData() |
115 | | - return self._with_lock_and_connect(self._read, data) |
116 | | - |
117 | | - def read_parameters(self, parameters=None): |
118 | | - """ |
119 | | - Read parameters from heat pump and integrate them to the passed dictionary. |
120 | | - This dictionary is returned afterwards, mainly for access to a newly created. |
121 | | - """ |
122 | | - if parameters is None: |
123 | | - parameters = Parameters() |
124 | | - return self._with_lock_and_connect(self._read_parameters, parameters) |
125 | | - |
126 | | - def read_calculations(self, calculations=None): |
127 | | - """ |
128 | | - Read calculations from heat pump and integrate them to the passed dictionary. |
129 | | - This dictionary is returned afterwards, mainly for access to a newly created. |
130 | | - """ |
131 | | - if calculations is None: |
132 | | - calculations = Calculations() |
133 | | - return self._with_lock_and_connect(self._read_calculations, calculations) |
134 | | - |
135 | | - def read_visibilities(self, visibilities=None): |
136 | | - """ |
137 | | - Read visibilities from heat pump and integrate them to the passed dictionary. |
138 | | - This dictionary is returned afterwards, mainly for access to a newly created. |
139 | | - """ |
140 | | - if visibilities is None: |
141 | | - visibilities = Visibilities() |
142 | | - return self._with_lock_and_connect(self._read_visibilities, visibilities) |
143 | | - |
144 | | - def write(self, parameters): |
145 | | - """ |
146 | | - Write all set parameters to the heat pump. |
147 | | - :param Parameters() parameters Parameter dictionary to be written |
148 | | - to the heatpump before reading all available data |
149 | | - from the heat pump. |
150 | | - """ |
151 | | - self._with_lock_and_connect(self._write, parameters) |
152 | | - |
153 | | - def write_and_read(self, parameters, data=None): |
154 | | - """ |
155 | | - Write all set parameter to the heat pump (see write()) |
156 | | - prior to reading back in all data from the heat pump (see read()) |
157 | | - after a short wait time |
158 | | - """ |
159 | | - if data is None: |
160 | | - data = LuxtronikData() |
161 | | - return self._with_lock_and_connect(self._write_and_read, parameters, data) |
162 | | - |
163 | | - def _read(self, data): |
164 | | - self._read_parameters(data.parameters) |
165 | | - self._read_calculations(data.calculations) |
166 | | - self._read_visibilities(data.visibilities) |
167 | | - return data |
168 | | - |
169 | | - def _write_and_read(self, parameters, data): |
170 | | - self._write(parameters) |
171 | | - return self._read(data) |
172 | | - |
173 | | - def _write(self, parameters): |
174 | | - for index, value in parameters.queue.items(): |
175 | | - if not isinstance(index, int) or not isinstance(value, int): |
176 | | - LOGGER.warning( |
177 | | - "%s: Parameter id '%s' or value '%s' invalid!", |
178 | | - self._host, |
179 | | - index, |
180 | | - value, |
181 | | - ) |
182 | | - continue |
183 | | - LOGGER.info("%s: Parameter '%d' set to '%s'", self._host, index, value) |
184 | | - self._send_ints(LUXTRONIK_PARAMETERS_WRITE, index, value) |
185 | | - cmd = self._read_int() |
186 | | - LOGGER.debug("%s: Command %s", self._host, cmd) |
187 | | - val = self._read_int() |
188 | | - LOGGER.debug("%s: Value %s", self._host, val) |
189 | | - # Flush queue after writing all values |
190 | | - parameters.queue = {} |
191 | | - # Give the heatpump a short time to handle the value changes/calculations: |
192 | | - time.sleep(WAIT_TIME_AFTER_PARAMETER_WRITE) |
193 | | - |
194 | | - def _read_parameters(self, parameters): |
195 | | - data = [] |
196 | | - self._send_ints(LUXTRONIK_PARAMETERS_READ, 0) |
197 | | - cmd = self._read_int() |
198 | | - LOGGER.debug("%s: Command %s", self._host, cmd) |
199 | | - length = self._read_int() |
200 | | - LOGGER.debug("%s: Length %s", self._host, length) |
201 | | - for _ in range(0, length): |
202 | | - data.append(self._read_int()) |
203 | | - LOGGER.info("%s: Read %d parameters", self._host, length) |
204 | | - parameters.parse(data) |
205 | | - return parameters |
206 | | - |
207 | | - def _read_calculations(self, calculations): |
208 | | - data = [] |
209 | | - self._send_ints(LUXTRONIK_CALCULATIONS_READ, 0) |
210 | | - cmd = self._read_int() |
211 | | - LOGGER.debug("%s: Command %s", self._host, cmd) |
212 | | - stat = self._read_int() |
213 | | - LOGGER.debug("%s: Stat %s", self._host, stat) |
214 | | - length = self._read_int() |
215 | | - LOGGER.debug("%s: Length %s", self._host, length) |
216 | | - for _ in range(0, length): |
217 | | - data.append(self._read_int()) |
218 | | - LOGGER.info("%s: Read %d calculations", self._host, length) |
219 | | - calculations.parse(data) |
220 | | - return calculations |
221 | | - |
222 | | - def _read_visibilities(self, visibilities): |
223 | | - data = [] |
224 | | - self._send_ints(LUXTRONIK_VISIBILITIES_READ, 0) |
225 | | - cmd = self._read_int() |
226 | | - LOGGER.debug("%s: Command %s", self._host, cmd) |
227 | | - length = self._read_int() |
228 | | - LOGGER.debug("%s: Length %s", self._host, length) |
229 | | - for _ in range(0, length): |
230 | | - data.append(self._read_char()) |
231 | | - LOGGER.info("%s: Read %d visibilities", self._host, length) |
232 | | - visibilities.parse(data) |
233 | | - return visibilities |
234 | | - |
235 | | - def _send_ints(self, *ints): |
236 | | - "Low-level helper to send a tuple of ints" |
237 | | - data = struct.pack(">" + "i" * len(ints), *ints) |
238 | | - LOGGER.debug("%s: sending %s", self._host, data) |
239 | | - self._socket.sendall(data) |
240 | | - |
241 | | - def _read_bytes(self, count): |
242 | | - "Low-level helper to receive a precise number of bytes" |
243 | | - total_reading = b"" |
244 | | - |
245 | | - while len(total_reading) is not count: |
246 | | - missing = count - len(total_reading) |
247 | | - |
248 | | - reading = self._socket.recv( missing ) |
249 | | - |
250 | | - if len(reading) == 0: |
251 | | - LOGGER.error("%s: Connection died.", self._host) |
252 | | - raise ConnectionError("Connection to %s died." % self._host) |
253 | | - |
254 | | - total_reading += reading |
255 | | - |
256 | | - if len(reading) is not missing: |
257 | | - LOGGER.debug("%s: received %s bytes out of %s bytes. Will read again.", self._host, len(reading), missing) |
258 | | - |
259 | | - return total_reading |
260 | | - |
261 | | - def _read_int(self): |
262 | | - "Low-level helper to receive an int" |
263 | | - reading = self._read_bytes(LUXTRONIK_SOCKET_READ_SIZE_INTEGER) |
264 | | - return struct.unpack(">i", reading)[0] |
265 | | - |
266 | | - def _read_char(self): |
267 | | - "Low-level helper to receive a signed int" |
268 | | - reading = self._read_bytes(LUXTRONIK_SOCKET_READ_SIZE_CHAR) |
269 | | - return struct.unpack(">b", reading)[0] |
270 | | - |
271 | 33 |
|
272 | 34 | class LuxtronikAllData(LuxtronikData, LuxtronikSmartHomeData): |
273 | 35 | """ |
|
0 commit comments