Skip to content

Commit 548050a

Browse files
authored
Merge pull request #223 from Guzz-T/issue/221/common
Restructure source code and imports
2 parents aa7667b + dcf3454 commit 548050a

29 files changed

Lines changed: 1139 additions & 1075 deletions

luxtronik/__init__.py

Lines changed: 14 additions & 252 deletions
Original file line numberDiff line numberDiff line change
@@ -5,269 +5,31 @@
55
from __future__ import annotations
66

77
import logging
8-
import socket
9-
import struct
10-
import time
118

129
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
1610
from luxtronik.discover import discover # noqa: F401
17-
from luxtronik.constants import (
11+
12+
from luxtronik.cfi import (
1813
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,
2519
)
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 (
3121
LUXTRONIK_DEFAULT_MODBUS_PORT,
22+
LuxtronikModbusTcpInterface,
23+
Holdings, # noqa: F401
24+
Inputs, # noqa: F401
25+
LuxtronikSmartHomeData,
26+
LuxtronikSmartHomeInterface,
27+
resolve_version,
3228
)
33-
3429
# endregion Imports
3530

3631
LOGGER = logging.getLogger("Luxtronik")
3732

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-
27133

27234
class LuxtronikAllData(LuxtronikData, LuxtronikSmartHomeData):
27335
"""

luxtronik/cfi/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
Python module for controlling a Luxtronik heat pump controller
3+
via the config interface.
4+
"""
5+
6+
from luxtronik.cfi.constants import (
7+
LUXTRONIK_DEFAULT_PORT, # noqa: F401
8+
)
9+
from luxtronik.cfi.calculations import CALCULATIONS_DEFINITIONS, Calculations # noqa: F401
10+
from luxtronik.cfi.parameters import PARAMETERS_DEFINITIONS, Parameters # noqa: F401
11+
from luxtronik.cfi.visibilities import VISIBILITIES_DEFINITIONS, Visibilities # noqa: F401
12+
from luxtronik.cfi.interface import LuxtronikData, LuxtronikSocketInterface # noqa: F401
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import logging
44
from typing import Final
55

6+
from luxtronik.definitions import LuxtronikDefinitionsList
67
from luxtronik.definitions.calculations import CALCULATIONS_DEFINITIONS_LIST, CALCULATIONS_OFFSET
78

8-
from luxtronik.constants import CALCULATIONS_FIELD_NAME
9-
from luxtronik.shi.definitions import LuxtronikDefinitionsList
9+
from luxtronik.cfi.constants import CALCULATIONS_FIELD_NAME
1010
from luxtronik.data_vector import DataVector
1111
from luxtronik.datatypes import Base
1212

luxtronik/cfi/constants.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Constants used throughout the Luxtronik config interface (CFI) module."""
2+
3+
# Default port to be used to connect to Luxtronik controller.
4+
LUXTRONIK_DEFAULT_PORT = 8889
5+
6+
LUXTRONIK_PARAMETERS_WRITE = 3002
7+
LUXTRONIK_PARAMETERS_READ = 3003
8+
LUXTRONIK_CALCULATIONS_READ = 3004
9+
LUXTRONIK_VISIBILITIES_READ = 3005
10+
11+
LUXTRONIK_SOCKET_READ_SIZE_PEEK = 16
12+
13+
LUXTRONIK_SOCKET_READ_SIZE_INTEGER = 4
14+
LUXTRONIK_SOCKET_READ_SIZE_CHAR = 1
15+
16+
# Identifier of calculation data-vectors and partial name for unknown calculation fields
17+
CALCULATIONS_FIELD_NAME = "calculation"
18+
19+
# Identifier of parameter data-vectors and partial name for unknown parameter fields
20+
PARAMETERS_FIELD_NAME = "parameter"
21+
22+
# Identifier of visibilities data-vectors and partial name for unknown visibility fields
23+
VISIBILITIES_FIELD_NAME = "visibility"

0 commit comments

Comments
 (0)