Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1328,6 +1328,10 @@ from `Unknown`to `Energy`
- Add `POWER_LIMIT_SWITCH` (index `1158`) of type `Unknown`
- Add `Unknown_Parameter_1159` (index `1159`) of type `Unknown`
- Add `POWER_LIMIT_VALUE` (index `1159`) of type `Unknown`
- Add `THERMAL_POWER_LIMIT_SWITCH` (index `1175`) of type `Unknown`
- Add `THERMAL_POWER_LIMIT_HEATING` (index `1176`) of type `Unknown`
- Add `THERMAL_POWER_LIMIT_WATER` (index `1177`) of type `Unknown`
- Add `THERMAL_POWER_LIMIT_COOLING` (index `1178`) of type `Unknown`

### Calculations

Expand Down Expand Up @@ -1372,6 +1376,7 @@ from `IPAddress`to `IPv4Address`
- Add `Unknown_Calculation_265` (index `265`) of type `Unknown`
- Add `Unknown_Calculation_266` (index `266`) of type `Unknown`
- Add `Desired_Room_Temperature` (index `267`) of type `Celsius`
- Add `Unknown_Calculation_268` (index `268`) of type `Unknown`
- Add `AC_Power_Input` (index `268`) of type `Power`

### Visibilities
Expand Down
2 changes: 1 addition & 1 deletion luxtronik/cfi/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

# Wait time (in seconds) after writing parameters to give controller
# some time to re-calculate values, etc.
WAIT_TIME_AFTER_PARAMETER_WRITE = 1
WAIT_TIME_AFTER_PARAMETER_WRITE: Final = 1

# The data from the config interface are transmitted in 32-bit chunks.
LUXTRONIK_CFI_REGISTER_BIT_SIZE: Final = 32
7 changes: 5 additions & 2 deletions luxtronik/cfi/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def _with_lock_and_connect(self, func, *args, **kwargs):
ret_val = None
with socket.create_connection((self._host, self._port)) as sock:
self._socket = sock
LOGGER.info("Connected to Luxtronik heat pump %s:%s", self._host, self._port)
LOGGER.info("Connected to CFI of Luxtronik heat pump %s:%s", self._host, self._port)
ret_val = func(*args, **kwargs)
except socket.gaierror as e:
LOGGER.error("Failed to connect to Luxtronik heat pump %s:%s. %s.",
Expand Down Expand Up @@ -166,6 +166,7 @@ def _write(self, parameters):
if not isinstance(parameters, Parameters):
LOGGER.error("Only parameters are writable!")
return
count = 0
for definition, field in parameters.items():
if field.write_pending:
field.write_pending = False
Expand All @@ -178,12 +179,14 @@ def _write(self, parameters):
value,
)
continue
LOGGER.info("%s: Parameter '%d' set to '%s'", self._host, definition.index, value)
LOGGER.debug("%s: Parameter '%d' set to '%s'", self._host, definition.index, value)
self._send_ints(LUXTRONIK_PARAMETERS_WRITE, definition.index, value)
cmd = self._read_int()
LOGGER.debug("%s: Command %s", self._host, cmd)
val = self._read_int()
LOGGER.debug("%s: Value %s", self._host, val)
count += 1
LOGGER.info("%s: Write %d parameters", self._host, count)
# Give the heatpump a short time to handle the value changes/calculations:
time.sleep(WAIT_TIME_AFTER_PARAMETER_WRITE)

Expand Down
2 changes: 1 addition & 1 deletion luxtronik/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@
LUXTRONIK_32BIT_FUNCTION_NOT_AVAILABLE: Final = 0x7FFFFFFF

# If True, preserve the last set field value on clear and assign `None` to raw
LUXTRONIK_PRESERVE_LAST_VALUE = True
LUXTRONIK_PRESERVE_LAST_VALUE: Final = True
30 changes: 17 additions & 13 deletions luxtronik/definitions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ def __init__(self, data_dict, type_name, offset, data_type=""):
that have been checked for correctness using pytest.
This eliminates the need for type tests here.
"""
self.report_successor = True
self.report_outdated_name = True
try:
data_dict = self.DEFAULT_DATA | data_dict
index = int(data_dict["index"])
Expand Down Expand Up @@ -194,11 +196,7 @@ def names(self):

@property
def successor(self):
# Clear the successor after first use,
# not to generate a lot of warnings
s = self._successor
self._successor = None
return s
return self._successor

@property
def name(self):
Expand Down Expand Up @@ -301,12 +299,12 @@ def get(self, name_or_idx, default=None):
if d is None:
LOGGER.debug(f"Definition for '{name_or_idx}' not found")
else:
# The successor is returned only once for each definition,
# not to generate a lot of warnings
successor = d.successor
if successor is not None:
LOGGER.warning(f"Definition for '{name_or_idx}' is outdated and will " \
+ f"be removed soon! Please use '{successor}' instead.")
# Report a successor only once per definition instance
# to avoid generating too many warnings
if d.successor is not None and d.report_successor:
d.report_successor = False
LOGGER.warning(f"'{d.type_name}' definition for '{name_or_idx}' is outdated and will " \
+ f"be removed soon! Please use '{d.successor}' instead.")
return d if d is not None else default

def _get(self, name_or_idx):
Expand Down Expand Up @@ -362,8 +360,14 @@ def _get_definition_by_name(self, name):
If multiple definitions added for the same name, the last added takes precedence.
"""
definition = self._name_dict.get(name.lower(), None)
if definition is not None and definition.valid and name.lower() != definition.name.lower():
LOGGER.warning(f"'{name}' is outdated! Use '{definition.name}' instead.")
# Report an outdated name only once per definition instance
# to avoid generating too many warnings
if definition is not None and definition.valid \
and name.lower() != definition.name.lower() \
and definition.report_outdated_name:
definition.report_outdated_name = False
LOGGER.warning(f"'{definition.type_name}' name '{name}' is outdated! " \
+ f"Use '{definition.name}' instead.")
return definition


Expand Down
28 changes: 28 additions & 0 deletions luxtronik/definitions/calculations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2558,6 +2558,17 @@
"unit": 'K/10',
"description": '',
},
{
"index": 244,
"count": 1,
"names": ['flow_temp_limit'],
"type": Celsius,
"writeable": False,
"datatype": 'UINT32',
"unit": '°C/10',
"since": "3.88.0",
"description": 'PRELIMINARY: Max supply (flow) temperature setpoint',
},
{
"index": 244,
"count": 1,
Expand Down Expand Up @@ -2638,6 +2649,17 @@
"unit": '',
"description": '',
},
{
"index": 252,
"count": 1,
"names": ['hot_gas_temp_limit'],
"type": Celsius,
"writeable": False,
"datatype": 'UINT32',
"unit": '°C/10',
"since": "3.88.0",
"description": 'PRELIMINARY: Max hot-gas (discharge) cut-off setpoint',
},
{
"index": 252,
"count": 1,
Expand Down Expand Up @@ -2804,6 +2826,12 @@
"unit": '°C/10',
"description": '',
},
{
"index": 268,
"names": ['Unknown_Calculation_268'],
"type": Unknown,
"successor": 'AC_Power_Input',
},
{
"index": 268,
"count": 1,
Expand Down
28 changes: 24 additions & 4 deletions luxtronik/definitions/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -11892,23 +11892,43 @@
},
{
"index": 1175,
"names": ['Unknown_Parameter_1175'],
"count": 1,
"names": ['THERMAL_POWER_LIMIT_SWITCH', 'Unknown_Parameter_1175'],
"type": Unknown,
"writeable": False,
"datatype": 'UINT32',
"unit": '',
"description": '',
},
{
"index": 1176,
"names": ['Unknown_Parameter_1176'],
"count": 1,
"names": ['THERMAL_POWER_LIMIT_HEATING', 'Unknown_Parameter_1176'],
"type": Unknown,
"writeable": False,
"datatype": 'UINT32',
"unit": '',
"description": '',
},
{
"index": 1177,
"names": ['Unknown_Parameter_1177'],
"count": 1,
"names": ['THERMAL_POWER_LIMIT_WATER', 'Unknown_Parameter_1177'],
"type": Unknown,
"writeable": False,
"datatype": 'UINT32',
"unit": '',
"description": '',
},
{
"index": 1178,
"names": ['Unknown_Parameter_1178'],
"count": 1,
"names": ['THERMAL_POWER_LIMIT_COOLING', 'Unknown_Parameter_1178'],
"type": Unknown,
"writeable": False,
"datatype": 'UINT32',
"unit": '',
"description": '',
},
{
"index": 1179,
Expand Down
19 changes: 19 additions & 0 deletions luxtronik/shi/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,25 @@ def _send_and_integrate(self, blocks_list):
# Send all telegrams. The retrieved data is returned within the telegrams
telegrams = [data[1] for data in telegrams_data]
success = self._interface.send(telegrams)
# Report send/received data
count = {'hr': 0, 'hw': 0, 'ir': 0, 'u': 0}
for t in telegrams:
if isinstance(t, LuxtronikSmartHomeReadHoldingsTelegram):
count['hr'] += t.count
elif isinstance(t, LuxtronikSmartHomeReadInputsTelegram):
count['ir'] += t.count
elif isinstance(t, LuxtronikSmartHomeWriteHoldingsTelegram):
count['hw'] += t.count
else:
count['u'] += t.count
if count['hr'] > 0:
LOGGER.info(f"{self._interface._host}: Read {count['hr']} holdings")
if count['ir'] > 0:
LOGGER.info(f"{self._interface._host}: Read {count['ir']} inputs")
if count['hw'] > 0:
LOGGER.info(f"{self._interface._host}: Write {count['hw']} holdings")
if count['u'] > 0:
LOGGER.info(f"{self._interface._host}: Write {count['u']} unknowns?")
# Transfer the data from the telegrams into the fields
success &= self._integrate_data(telegrams_data)
return success
Expand Down
7 changes: 5 additions & 2 deletions luxtronik/shi/modbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ def __init__(
self._lock = get_host_lock(host)

# Create the Modbus client (connection is not opened/closed automatically)
self._host = host
self._port = port
self._client = ModbusClient(
host=host,
port=port,
Expand Down Expand Up @@ -86,6 +88,8 @@ def _connect(self):
+ f"{self._client.last_error_as_txt}")
self._client.close()
return False
else:
LOGGER.info(f"Connected to SHI of Luxtronik heat pump {self._host}:{self._port}")

return True

Expand Down Expand Up @@ -295,8 +299,7 @@ def send(self, telegrams):

# Exit the function if no operation is necessary
if total_count <= 0:
LOGGER.warning("No data requested/provided. Abort operation.")
return False
return True

# Acquire lock, connect and read/write data. Disconnect afterwards.
success = False
Expand Down
2 changes: 2 additions & 0 deletions tests/fake/fake_modbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ class FakeModbus:
result = True

def __init__(self, host="", port="", timeout=0):
self._host = host
self._port = port
self._connected = False
self._blocking = False

Expand Down
12 changes: 6 additions & 6 deletions tests/shi/test_shi_modbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def test_no_holdings_read_data(self):

# Read zero holdings
result = self.modbus_interface.send(data_list)
assert not result
assert result # no error when there is no data
assert data_list[0].data == []
assert data_list[1].data == []

Expand All @@ -167,15 +167,15 @@ def test_no_holdings_write_data(self):

# Write zero holdings
result = self.modbus_interface.send(data_list)
assert not result
assert result # no error when there is no data


def test_no_inputs_read_data(self):
data_list = [LuxtronikSmartHomeReadInputsTelegram(0, 0), LuxtronikSmartHomeReadInputsTelegram(0, 0)]

# Read zero inputs
result = self.modbus_interface.send(data_list)
assert not result
assert result # no error when there is no data
assert data_list[0].data == []
assert data_list[1].data == []

Expand All @@ -185,7 +185,7 @@ def test_no_inputs_read_data(self):
[
(1, 2, True, [1, 2]),
(5, 3, True, [5, 6, 7]),
(0, 0, False, []),
(0, 0, True, []), # no error when there is no data
(1000, 2, False, None), # client has read error
(1001, 3, False, None), # client returns to less data
(1002, 4, False, None), # client returns to much data
Expand Down Expand Up @@ -213,7 +213,7 @@ def test_read_holdings(self, addr, count, valid, data):
[
(1, 2, True, [1, 2]),
(5, 3, True, [5, 6, 7]),
(0, 0, False, []),
(0, 0, True, []), # no error when there is no data
(1000, 2, False, None), # client has read error
(1001, 3, False, None), # client returns to less data
(1002, 4, False, None), # client returns to much data
Expand Down Expand Up @@ -241,7 +241,7 @@ def test_read_inputs(self, addr, count, valid, data):
[
(1, [1, 2], True),
(5, [5, 6, 7], True),
(0, [], False),
(0, [], True), # no error when there is no data
(1000, [8, 9], False), # Write error
(1001, [11], False), # Exception
]
Expand Down
8 changes: 8 additions & 0 deletions tests/test_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -1302,9 +1302,13 @@ def test_compatibilities(self):
"Unknown_Parameter_1173": (1173, Unknown),
"Unknown_Parameter_1174": (1174, Unknown),
"Unknown_Parameter_1175": (1175, Unknown),
"THERMAL_POWER_LIMIT_SWITCH": (1175, Unknown),
"Unknown_Parameter_1176": (1176, Unknown),
"THERMAL_POWER_LIMIT_HEATING": (1176, Unknown),
"Unknown_Parameter_1177": (1177, Unknown),
"THERMAL_POWER_LIMIT_WATER": (1177, Unknown),
"Unknown_Parameter_1178": (1178, Unknown),
"THERMAL_POWER_LIMIT_COOLING": (1178, Unknown),
"Unknown_Parameter_1179": (1179, Unknown),
"Unknown_Parameter_1180": (1180, Unknown),
"Unknown_Parameter_1181": (1181, Unknown),
Expand Down Expand Up @@ -1610,13 +1614,17 @@ def test_compatibilities(self):
"Unknown_Calculation_265": (265, Unknown),
"Unknown_Calculation_266": (266, Unknown),
"Desired_Room_Temperature": (267, Celsius),
"Unknown_Calculation_268": (268, Unknown),
"AC_Power_Input": (268, Power),
"Unknown_Calculation_269": (269, Unknown),
"Unknown_Calculation_270": (270, Unknown),
"Unknown_Calculation_271": (271, Unknown),
"Unknown_Calculation_272": (272, Unknown),
"Unknown_Calculation_273": (273, Unknown),
"Unknown_Calculation_274": (274, Unknown),
# New in 'main' branch (after next release):
"flow_temp_limit": (244, Celsius),
"hot_gas_temp_limit": (252, Celsius),
}

visis = {
Expand Down
1 change: 0 additions & 1 deletion tests/test_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ def test_init(self):
assert definition.names == names
assert definition.name == names[0]
assert definition.successor == 'abcd'
assert definition.successor is None
assert definition.data_type == 'INT16'
assert definition.valid
assert definition
Expand Down
Loading