Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3440df6
Update smart home interface definitions according to (in)official doc…
Guzz-T Dec 25, 2025
f031c67
Refine lpc_mode description
Guzz-T Dec 25, 2025
6745492
Add some thermal energy definitions
Guzz-T Dec 26, 2025
00603fd
Report an error in case to_heatpump() returns None
Guzz-T Dec 27, 2025
a3050ba
Return None instead of Unknown_None from SelectionBase.from_heatpump()
Guzz-T Dec 27, 2025
8f23057
Only attempt to resolve "unknown" for string-based values
Guzz-T Dec 27, 2025
30a2b03
Allow the value of SelectionBase to be set using an integer or intege…
Guzz-T Dec 27, 2025
b505e30
Remove redundant datatype_class = "selection" assignment
Guzz-T Dec 27, 2025
9c9dc1a
Correct MixedCircuitMode description
Guzz-T Dec 27, 2025
156bad7
Handle contiguous values that are distributed across multiple registe…
Guzz-T Dec 27, 2025
4d43428
Enhance ScalingBase so that it can also handle non-4-byte values.
Guzz-T Dec 27, 2025
333c385
Add 16-bit temperature data types
Guzz-T Dec 27, 2025
956eeea
Add a buffer-type data-type
Guzz-T Dec 27, 2025
48b5e8d
Add "0" to data-type Errorcode to indicate "no error"
Guzz-T Dec 27, 2025
3d31e05
Add a BitMaskBase data-type
Guzz-T Dec 27, 2025
64d3af7
Use BitMaskBase as base class for the HeatPumpState data-type
Guzz-T Dec 27, 2025
9a48710
Further updates of the smart home interface definitions according to …
Guzz-T Dec 27, 2025
90576cb
Refactored "state" to "status" for shi data-types
Guzz-T Dec 27, 2025
24ca59b
Fix power data-type for shi register
Guzz-T Dec 28, 2025
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
208 changes: 148 additions & 60 deletions luxtronik/datatypes.py

Large diffs are not rendered by default.

329 changes: 242 additions & 87 deletions luxtronik/definitions/holdings.py

Large diffs are not rendered by default.

540 changes: 311 additions & 229 deletions luxtronik/definitions/inputs.py

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion luxtronik/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -1216,7 +1216,11 @@ def set(self, target, value):
index, parameter = self._lookup(target, with_index=True)
if index is not None:
if parameter.writeable or not self.safe:
self.queue[index] = int(parameter.to_heatpump(value))
raw = parameter.to_heatpump(value)
if isinstance(raw, int):
self.queue[index] = raw
else:
self.logger.error("Value '%s' for Parameter '%s' not valid!", value, parameter.name)
else:
self.logger.warning("Parameter '%s' not safe for writing!", parameter.name)
else:
Expand Down
3 changes: 3 additions & 0 deletions luxtronik/shi/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
# to give the controller time to recalculate values, etc.
LUXTRONIK_WAIT_TIME_AFTER_HOLDING_WRITE: Final = 1

# The data from the smart home interface are transmitted in 16-bit chunks.
LUXTRONIK_SHI_REGISTER_BIT_SIZE = 16

# Since version 3.92.0, all unavailable data fields
# have been returning this value (0x7FFF)
LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE: Final = 32767
Expand Down
68 changes: 68 additions & 0 deletions luxtronik/shi/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from luxtronik.datatypes import Unknown
from luxtronik.shi.constants import (
LUXTRONIK_DEFAULT_DEFINITION_OFFSET,
LUXTRONIK_SHI_REGISTER_BIT_SIZE,
LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE
)
from luxtronik.shi.common import (
Expand Down Expand Up @@ -549,6 +550,64 @@ def add(self, data_dict):
# Definition-Field-Pair methods
###############################################################################

VALUE_MASK = (1 << LUXTRONIK_SHI_REGISTER_BIT_SIZE) - 1

def pack_values(values, reverse=True):
"""
Packs a list of data chunks into one integer.

Args:
values (list[int]): raw data; distributed across multiple registers.
reverse (bool): Use big-endian/MSB-first if true,
otherwise use little-endian/LSB-first order.

Returns:
int: Packed raw data as a single integer value.

Note:
The smart home interface uses a chunk size of 16 bits.
"""
count = len(values)

result = 0
for idx, value in enumerate(values):
# normal: idx = 0..n-1
# reversed index: highest chunk first
bit_index = (count - 1 - idx) if reverse else idx

result |= (value & VALUE_MASK) << (LUXTRONIK_SHI_REGISTER_BIT_SIZE * bit_index)

return result

def unpack_values(packed, count, reverse=True):
"""
Unpacks 'count' values from a packed integer.

Args:
packed (int): Packed raw data as a single integer value.
count (int): Number of chunks to unpack.
reverse (bool): Use big-endian/MSB-first if true,
otherwise use little-endian/LSB-first order.

Returns:
list[int]: List of unpacked raw data values.

Note:
The smart home interface uses a chunk size of 16 bits.
"""
values = []

for idx in range(count):
# normal: idx = 0..n-1
# reversed: highest chunk first
bit_index = (count - 1 - idx) if reverse else idx

chunk = (packed >> (LUXTRONIK_SHI_REGISTER_BIT_SIZE * bit_index)) & VALUE_MASK
values.append(chunk)

return values


def get_data_arr(definition, field):
"""
Normalize the field's data to a list of the correct size.
Expand All @@ -562,6 +621,12 @@ def get_data_arr(definition, field):
or None if the data size does not match.
"""
data = field.raw
if data is None:
return None
if not isinstance(data, list) and definition.count > 1 \
and field.concatenate_multiple_data_chunks:
# Usually big-endian (reverse=True) is used
data = unpack_values(data, definition.count)
if not isinstance(data, list):
data = [data]
return data if len(data) == definition.count else None
Expand Down Expand Up @@ -600,4 +665,7 @@ def integrate_data(definition, field, raw_data, data_offset=-1):
raw = raw_data[data_offset : data_offset + definition.count]
raw = raw if len(raw) == definition.count and \
not any(data == LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE for data in raw) else None
if field.concatenate_multiple_data_chunks and raw is not None:
# Usually big-endian (reverse=True) is used
raw = pack_values(raw)
field.raw = raw
7 changes: 7 additions & 0 deletions tests/shi/test_contiguous.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
ContiguousDataBlockList,
)

"""
The test was originally written for "False".
Since "True" is already checked in "test_definitions.py",
we continue to use "False" consistently here.
"""
Base.concatenate_multiple_data_chunks = False


def_a1 = LuxtronikDefinition({
'index': 1,
Expand Down
49 changes: 49 additions & 0 deletions tests/shi/test_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ class TestDefinitionFieldPair:
def test_data_arr(self):
definition = LuxtronikDefinition.unknown(2, 'Foo', 30)
field = definition.create_field()
field.concatenate_multiple_data_chunks = False

# get from value
definition._count = 1
Expand Down Expand Up @@ -494,9 +495,33 @@ def test_data_arr(self):
assert arr is None
assert not check_data(definition, field)

field.concatenate_multiple_data_chunks = True

# get from array
definition._count = 2
field.raw = 0x0007_0003
arr = get_data_arr(definition, field)
assert arr == [7, 3]
assert check_data(definition, field)

# too much data
definition._count = 2
field.raw = 0x0004_0008_0001
arr = get_data_arr(definition, field)
assert arr == [8, 1]
assert check_data(definition, field)

# insufficient data
definition._count = 2
field.raw = 0x0009
arr = get_data_arr(definition, field)
assert arr == [0, 9]
assert check_data(definition, field)

def test_integrate(self):
definition = LuxtronikDefinition.unknown(2, 'Foo', 30)
field = definition.create_field()
field.concatenate_multiple_data_chunks = False

data = [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, 3, 4, 5, 6, 7]

Expand All @@ -520,4 +545,28 @@ def test_integrate(self):
integrate_data(definition, field, data, 9)
assert field.raw is None
integrate_data(definition, field, data, 1)
assert field.raw is None

field.concatenate_multiple_data_chunks = True

# set array
definition._count = 2
integrate_data(definition, field, data)
assert field.raw == 0x0003_0004
integrate_data(definition, field, data, 4)
assert field.raw == 0x0005_0006
integrate_data(definition, field, data, 7)
assert field.raw is None
integrate_data(definition, field, data, 0)
assert field.raw is None

# set value
definition._count = 1
integrate_data(definition, field, data)
assert field.raw == 0x0003
integrate_data(definition, field, data, 5)
assert field.raw == 0x0006
integrate_data(definition, field, data, 9)
assert field.raw is None
integrate_data(definition, field, data, 1)
assert field.raw is None
6 changes: 6 additions & 0 deletions tests/shi/test_vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
from luxtronik.shi.vector import DataVectorSmartHome
from luxtronik.shi.definitions import LuxtronikDefinitionsList

"""
The test was originally written for "False".
Since "True" is already checked in "test_definitions.py",
we continue to use "False" consistently here.
"""
Base.concatenate_multiple_data_chunks = False

###############################################################################
# Tests
Expand Down
Loading