Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
a532fdc
wip
Guzz-T Jan 20, 2026
a306c10
wip
Guzz-T Jan 20, 2026
577e3d9
wip
Guzz-T Jan 11, 2026
2e2f3f8
wip
Guzz-T Jan 20, 2026
48a7366
wip
Guzz-T Jan 21, 2026
14adb7f
wip
Guzz-T Jan 21, 2026
57a5c73
wip
Guzz-T Jan 21, 2026
7971ff4
wip
Guzz-T Jan 21, 2026
09f328d
wip
Guzz-T Jan 21, 2026
308ede2
wip
Guzz-T Jan 21, 2026
4633eab
wip
Guzz-T Jan 21, 2026
a043b1c
wip
Guzz-T Jan 21, 2026
1084b9f
wip
Guzz-T Jan 21, 2026
f77d4ee
wip
Guzz-T Jan 21, 2026
7c0e0d5
wip
Guzz-T Jan 21, 2026
d1441a7
wip
Guzz-T Jan 22, 2026
db46fba
wip
Guzz-T Jan 22, 2026
2237f72
wip
Guzz-T Jan 22, 2026
edae33c
wip
Guzz-T Jan 22, 2026
7317bb9
wip
Guzz-T Jan 22, 2026
d867fde
wip
Guzz-T Jan 22, 2026
a456ad9
wip
Guzz-T Jan 23, 2026
d3830a8
wip
Guzz-T Jan 23, 2026
6630180
wip last unify
Guzz-T Jan 23, 2026
dce9dc2
add v0.3.14 fields to test compatibility
Guzz-T Jan 23, 2026
b06e8a7
wip
Guzz-T Jan 24, 2026
2ec5f56
wip
Guzz-T Jan 24, 2026
4fbb959
wip
Guzz-T Jan 24, 2026
8f58d43
wip, multiple fields per register
Guzz-T Jan 24, 2026
767dbed
wip
Guzz-T Jan 24, 2026
3b8b1d1
wip
Guzz-T Jan 24, 2026
3c8fd2a
wip
Guzz-T Jan 24, 2026
447516e
wip
Guzz-T Jan 24, 2026
720355c
wip
Guzz-T Jan 24, 2026
270c4ce
wip
Guzz-T Jan 24, 2026
93991fe
wip
Guzz-T Jan 25, 2026
b4bd60c
wip
Guzz-T Jan 25, 2026
3f84acc
wip
Guzz-T Jan 25, 2026
1d63174
wip
Guzz-T Jan 25, 2026
d7184af
wip
Guzz-T Jan 25, 2026
0036df9
wip
Guzz-T Jan 25, 2026
ae0e609
wip
Guzz-T Jan 25, 2026
3d3315f
wip
Guzz-T Jan 25, 2026
c2f4447
wip
Guzz-T Jan 25, 2026
9ac333c
wip
Guzz-T Jan 27, 2026
61d8849
wip
Guzz-T Jan 29, 2026
bcfec0b
wip
Guzz-T Feb 3, 2026
def8505
wip
Guzz-T Feb 3, 2026
82dcc81
wip
Guzz-T Feb 3, 2026
e94f17f
preserve
Guzz-T Feb 3, 2026
83e6297
wip
Guzz-T Feb 3, 2026
06e0729
wip
Guzz-T Feb 3, 2026
3505fdd
wip
Guzz-T Feb 5, 2026
a97eef6
wip
Guzz-T Feb 5, 2026
02669ec
wip
Guzz-T Feb 5, 2026
1dfbc64
wip
Guzz-T Feb 5, 2026
3d8aa10
wip
Guzz-T Feb 5, 2026
6c31b33
wip
Guzz-T Feb 5, 2026
d28f6a2
wip
Guzz-T Feb 5, 2026
62a9308
wip
Guzz-T Feb 5, 2026
799fdf0
wip
Guzz-T Feb 16, 2026
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,8 @@ print(parameters.get("ID_Ba_Hz_akt").options()) # returns a list of possible val

# Now we increase the heating controller target temperature by 2 Kelvin
heating_offset = l.holdings.get(2) # Get an object for the offset
heating_offset.value = 2.0 # Set the desired value
l.holdings["heating_mode"] = "Offset" # Set the value to activate the offset mode
heating_offset.value = 2.0 # Queue the desired value by setting the field's value
l.holdings["heating_mode"] = "Offset" # Queue the value to activate the offset mode
l.write() # Write down the values to the heatpump
```

Expand Down
2 changes: 1 addition & 1 deletion luxtronik/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import logging

from luxtronik.common import get_host_lock
from luxtronik.common import LuxtronikSettings, get_host_lock # noqa: F401
from luxtronik.discover import discover # noqa: F401

from luxtronik.cfi import (
Expand Down
21 changes: 5 additions & 16 deletions luxtronik/cfi/calculations.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
CALCULATIONS_DEFINITIONS_LIST,
CALCULATIONS_OFFSET,
CALCULATIONS_DEFAULT_DATA_TYPE,
CALCULATIONS_OUTDATED,
)

from luxtronik.cfi.constants import CALCULATIONS_FIELD_NAME
from luxtronik.data_vector import DataVector
from luxtronik.cfi.vector import DataVectorConfig
from luxtronik.datatypes import Base


Expand All @@ -24,28 +25,16 @@
CALCULATIONS_DEFAULT_DATA_TYPE
)

class Calculations(DataVector):
class Calculations(DataVectorConfig):
"""Class that holds all calculations."""

name = CALCULATIONS_FIELD_NAME
definitions = CALCULATIONS_DEFINITIONS

_obsolete = {
"ID_WEB_SoftStand": "get_firmware_version()"
}

def __init__(self):
super().__init__()
for d in CALCULATIONS_DEFINITIONS:
self._data.add(d, d.create_field())

@property
def calculations(self):
return self._data
_outdated = CALCULATIONS_OUTDATED

def get_firmware_version(self):
"""Get the firmware version as string."""
return "".join([super(Calculations, self).get(i).value for i in range(81, 91)])
return "".join([str(super(Calculations, self).get(i).value) for i in range(81, 91)])

def _get_firmware_version(self):
"""Get the firmware version as string like in previous versions."""
Expand Down
47 changes: 27 additions & 20 deletions luxtronik/cfi/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import struct
import time

from luxtronik.common import get_host_lock
from luxtronik.common import LuxtronikSettings, get_host_lock
from luxtronik.cfi.constants import (
LUXTRONIK_DEFAULT_PORT,
LUXTRONIK_PARAMETERS_WRITE,
Expand Down Expand Up @@ -162,23 +162,27 @@ def _write_and_read(self, parameters, data):
return self._read(data)

def _write(self, parameters):
for index, value in parameters.queue.items():
if not isinstance(index, int) or not isinstance(value, int):
LOGGER.warning(
"%s: Parameter id '%s' or value '%s' invalid!",
self._host,
index,
value,
)
continue
LOGGER.info("%s: Parameter '%d' set to '%s'", self._host, index, value)
self._send_ints(LUXTRONIK_PARAMETERS_WRITE, 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)
# Flush queue after writing all values
parameters.queue = {}
if not isinstance(parameters, Parameters):
LOGGER.error(f"Only parameters are writable!"):
return
for definition, field in parameters.items():
if field.write_pending:
field.write_pending = False
value = field.raw
if not isinstance(definition.index, int) or not field.check_for_write(parameters.safe):
LOGGER.warning(
"%s: Parameter id '%s' or value '%s' invalid!",
self._host,
definition.index,
value,
)
continue
LOGGER.info("%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)
# Give the heatpump a short time to handle the value changes/calculations:
time.sleep(WAIT_TIME_AFTER_PARAMETER_WRITE)

Expand Down Expand Up @@ -274,17 +278,20 @@ def _parse(self, data_vector, raw_data):
undefined = {i for i in range(0, raw_len)}

# integrate the data into the fields
for pair in data_vector.data.pairs():
for pair in data_vector.data.items():
definition, field = pair
# skip this field if there are not enough data
next_idx = definition.index + definition.count
if next_idx > raw_len:
# not enough registers
field.raw = None
if not LuxtronikSettings.preserve_last_read_value_on_fail:
field.raw = None
continue
# remove all used indices from the list of undefined indices
for index in range(definition.index, next_idx):
undefined.discard(index)
# integrate_data() also resets the write_pending flag,
# intentionally only for read fields
pair.integrate_data(raw_data, LUXTRONIK_CFI_REGISTER_BIT_SIZE)

# create an unknown field for additional data
Expand Down
33 changes: 4 additions & 29 deletions luxtronik/cfi/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
PARAMETERS_DEFINITIONS_LIST,
PARAMETERS_OFFSET,
PARAMETERS_DEFAULT_DATA_TYPE,
PARAMETERS_OUTDATED,
)

from luxtronik.cfi.constants import PARAMETERS_FIELD_NAME
from luxtronik.data_vector import DataVector
from luxtronik.cfi.vector import DataVectorConfig


LOGGER = logging.getLogger(__name__)
Expand All @@ -23,35 +24,9 @@
PARAMETERS_DEFAULT_DATA_TYPE
)

class Parameters(DataVector):
class Parameters(DataVectorConfig):
"""Class that holds all parameters."""

name = PARAMETERS_FIELD_NAME
definitions = PARAMETERS_DEFINITIONS

def __init__(self, safe=True):
"""Initialize parameters class."""
super().__init__()
self.safe = safe
self.queue = {}
for d in PARAMETERS_DEFINITIONS:
self._data.add(d, d.create_field())

@property
def parameters(self):
return self._data

def set(self, target, value):
"""Set parameter to new value."""
index, parameter = self._lookup(target, with_index=True)
if index is not None:
if parameter.writeable or not self.safe:
raw = parameter.to_heatpump(value)
if isinstance(raw, int):
self.queue[index] = raw
else:
LOGGER.error("Value '%s' for Parameter '%s' not valid!", value, parameter.name)
else:
LOGGER.warning("Parameter '%s' not safe for writing!", parameter.name)
else:
LOGGER.warning("Parameter '%s' not found", target)
_outdated = PARAMETERS_OUTDATED
83 changes: 83 additions & 0 deletions luxtronik/cfi/vector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@

import logging

from luxtronik.data_vector import DataVector


LOGGER = logging.getLogger(__name__)

###############################################################################
# Configuration interface data-vector
###############################################################################

class DataVectorConfig(DataVector):
"""Specialized DataVector for Luxtronik configuration fields."""

def _init_instance(self, safe):
"""Re-usable method to initialize all instance variables."""
super()._init_instance(safe)

def __init__(self, safe=True):
"""
Initialize the data-vector instance.
Creates field objects for definitions and stores them in the data vector.

Args:
safe (bool): If true, prevent fields marked as
not secure from being written to.
"""
self._init_instance(safe)

# Add all available fields
for d in self.definitions:
self._data.add(d, d.create_field())

@classmethod
def empty(cls, safe=True):
"""
Initialize the data-vector instance without any fields.

Args:
safe (bool): If true, prevent fields marked as
not secure from being written to.
"""
obj = cls.__new__(cls) # this don't call __init__()
obj._init_instance(safe)
return obj

def add(self, def_field_name_or_idx, alias=None):
"""
Adds an additional field to this data vector.
Mainly used for data vectors created via `empty()`
to read/write individual fields. Existing fields will not be overwritten.

Args:
def_field_name_or_idx (LuxtronikDefinition | Base | str | int):
Field to add. Either by definition, name or index, or the field itself.
alias (Hashable | None): Alias, which can be used to access the field again.

Returns:
Base | None: The added field object if this could be added or
the existing field, otherwise None. In case a field

Note:
It is not possible to add fields which are not defined.
To add custom fields, add them to the used `LuxtronikDefinitionsList`
(`cls.definitions`) first.
If multiple fields added for the same index/name, the last added takes precedence.
"""
# Look-up the related definition
definition, field = self._get_definition(def_field_name_or_idx, True)
if definition is None:
return None

# Check if the field already exists
existing_field = self._data.get(definition, None)
if existing_field is not None:
return existing_field

# Add a (new) field
if field is None:
field = definition.create_field()
self._data.add_sorted(definition, field, alias)
return field
15 changes: 4 additions & 11 deletions luxtronik/cfi/visibilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
VISIBILITIES_DEFINITIONS_LIST,
VISIBILITIES_OFFSET,
VISIBILITIES_DEFAULT_DATA_TYPE,
VISIBILITIES_OUTDATED,
)

from luxtronik.cfi.constants import VISIBILITIES_FIELD_NAME
from luxtronik.data_vector import DataVector
from luxtronik.cfi.vector import DataVectorConfig


LOGGER = logging.getLogger(__name__)
Expand All @@ -23,17 +24,9 @@
VISIBILITIES_DEFAULT_DATA_TYPE,
)

class Visibilities(DataVector):
class Visibilities(DataVectorConfig):
"""Class that holds all visibilities."""

name = VISIBILITIES_FIELD_NAME
definitions = VISIBILITIES_DEFINITIONS

def __init__(self):
super().__init__()
for d in VISIBILITIES_DEFINITIONS:
self._data.add(d, d.create_field())

@property
def visibilities(self):
return self._data
_outdated = VISIBILITIES_OUTDATED
36 changes: 11 additions & 25 deletions luxtronik/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,7 @@ def integrate_data(self, raw_data, num_bits, data_offset=-1):

class LuxtronikFieldsDictionary:
"""
Dictionary that behaves like the earlier data vector dictionaries (index-field-dictionary),
with the addition that obsolete fields are also supported and can be addressed by name.
Dictionary that maps definitions, names or indices to added fields.
Aliases are also supported.
"""

Expand All @@ -228,15 +227,12 @@ def __getitem__(self, def_field_name_or_idx):
return self.get(def_field_name_or_idx)

def __len__(self):
return len(self._def_lookup._index_dict)
"""Return the number of added fields."""
return len(self._pairs)

def __iter__(self):
"""
Iterate over all non-obsolete indices. If an index is assigned multiple times,
only the index of the preferred definition will be output.
"""
all_related_defs = self._def_lookup._index_dict.values()
return iter([d.index for d in self._pairs if d in all_related_defs])
"""Return the iterator over all definitions related to the added fields."""
return iter([d for d, _ in self._pairs])

def __contains__(self, def_field_name_or_idx):
"""
Expand Down Expand Up @@ -265,26 +261,16 @@ def __contains__(self, def_field_name_or_idx):
return def_field_name_or_idx in self._def_lookup

def values(self):
"""
Iterator for all added non-obsolete fields. If an index is assigned multiple times,
only the field of the preferred definition will be output.
"""
all_related_defs = self._def_lookup._index_dict.values()
return iter([f for d, f in self._pairs if d in all_related_defs])
"""Return the iterator over all added fields."""
return iter([f for _, f in self._pairs])

def items(self):
"""
Iterator for all non-obsolete index-field-pairs (list of tuples with
0: index, 1: field) contained herein. If an index is assigned multiple times,
only the index-field-pair of the preferred definition will be output.
"""
all_related_defs = self._def_lookup._index_dict.values()
return iter([(d.index, f) for d, f in self._pairs if d in all_related_defs])
"""Return the iterator over all added definition-field-pairs."""
return iter(self._pairs)

@property
def pairs(self):
"""
Return all definition-field-pairs contained herein.
"""
"""Return all definition-field-pairs contained herein."""
return self._pairs

@property
Expand Down
10 changes: 10 additions & 0 deletions luxtronik/common.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@

from threading import RLock

###############################################################################
# User adjust-able settings class
###############################################################################

class LuxtronikSettings:

# If False, overwrite existing values with None in case of a transmission error.
# Otherwise, leave the previous value unchanged.
preserve_last_read_value_on_fail = True

###############################################################################
# Multi-threading lock mechanism
###############################################################################
Expand Down
4 changes: 0 additions & 4 deletions luxtronik/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,3 @@
# Since version 3.92.0, all unavailable 16 bit signed data fields
# have been returning this value (0x7FFF)
LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE: Final = 32767

LUXTRONIK_NAME_CHECK_NONE: Final = "none"
LUXTRONIK_NAME_CHECK_PREFERRED: Final = "preferred"
LUXTRONIK_NAME_CHECK_OBSOLETE: Final = "obsolete"
Loading
Loading