Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
8659e02
Move the array-style access methods from the smart home data vector i…
Guzz-T Feb 4, 2026
f9eab13
Support bit-fields within registers
Guzz-T Feb 4, 2026
b75007e
Add bit fields for the heatpump status within the smart home inputs
Guzz-T Feb 4, 2026
df8fc76
Fix bug in the __contains__ method of the luxtronik definition
Guzz-T Feb 4, 2026
b1aafd0
Implement the bit-shift operation with integrate_data()
Guzz-T Feb 4, 2026
f4248f2
Consequently use LOGGER instead of self.logger
Guzz-T Feb 4, 2026
cb1f3b2
Remove all unknown names that were not already used in v0.3.14
Guzz-T Feb 4, 2026
dd736ca
Also check if all alternative names are added to the compatibility te…
Guzz-T Feb 4, 2026
7849cb1
Merge pull request #236 from Guzz-T/try/remove-unknown
kbabioch Feb 5, 2026
29699f3
Merge pull request #235 from Guzz-T/try/logger
kbabioch Feb 5, 2026
fae0789
Merge pull request #234 from Guzz-T/try/bit-fields
kbabioch Feb 5, 2026
118a662
Merge pull request #233 from Guzz-T/issue/221/array
kbabioch Feb 5, 2026
10086a4
wip
Guzz-T Jan 20, 2026
9a4a311
wip
Guzz-T Jan 20, 2026
db7a186
wip
Guzz-T Jan 11, 2026
393efab
wip
Guzz-T Jan 20, 2026
dbcb669
wip
Guzz-T Jan 21, 2026
85a4499
wip
Guzz-T Jan 21, 2026
e33bc45
wip
Guzz-T Jan 21, 2026
f85fb30
wip
Guzz-T Jan 21, 2026
9fdf239
wip
Guzz-T Jan 21, 2026
d61a5a6
wip
Guzz-T Jan 21, 2026
0d5f38e
wip
Guzz-T Jan 21, 2026
45e5aa4
wip
Guzz-T Jan 21, 2026
1aa06b8
wip
Guzz-T Jan 21, 2026
10eb6e8
wip
Guzz-T Jan 21, 2026
772ab17
wip
Guzz-T Jan 21, 2026
095fe1d
wip
Guzz-T Jan 22, 2026
0d36cb4
wip
Guzz-T Jan 22, 2026
dd31d90
wip
Guzz-T Jan 22, 2026
4035a8a
wip
Guzz-T Jan 22, 2026
6cee9e4
wip
Guzz-T Jan 22, 2026
4ab1d82
wip
Guzz-T Jan 22, 2026
f8fc434
wip
Guzz-T Jan 23, 2026
116a938
wip
Guzz-T Jan 23, 2026
8fc1518
wip last unify
Guzz-T Jan 23, 2026
0360b89
add v0.3.14 fields to test compatibility
Guzz-T Jan 23, 2026
9fc3e32
wip
Guzz-T Jan 24, 2026
53e9752
wip
Guzz-T Jan 24, 2026
7b51d0c
wip
Guzz-T Jan 24, 2026
8c51f1d
wip, multiple fields per register
Guzz-T Jan 24, 2026
d927770
wip
Guzz-T Jan 24, 2026
78173c1
wip
Guzz-T Jan 24, 2026
5aace32
wip
Guzz-T Jan 24, 2026
36b4a9c
wip
Guzz-T Jan 24, 2026
f8f982c
wip
Guzz-T Jan 24, 2026
9d5c182
wip
Guzz-T Jan 24, 2026
8196486
wip
Guzz-T Jan 25, 2026
a53fda2
wip
Guzz-T Jan 25, 2026
b7375a7
wip
Guzz-T Jan 25, 2026
6deef70
wip
Guzz-T Jan 25, 2026
f1c0592
wip
Guzz-T Jan 25, 2026
9dc7126
wip
Guzz-T Jan 25, 2026
f8188c5
wip
Guzz-T Jan 25, 2026
66d9e02
wip
Guzz-T Jan 25, 2026
af77880
wip
Guzz-T Jan 25, 2026
90d7d88
wip
Guzz-T Jan 27, 2026
3198750
wip
Guzz-T Jan 29, 2026
d9e4e17
wip
Guzz-T Feb 3, 2026
da31153
wip
Guzz-T Feb 3, 2026
2a326ca
wip
Guzz-T Feb 3, 2026
b522e79
preserve
Guzz-T Feb 3, 2026
5615fde
wip
Guzz-T Feb 3, 2026
6d8d2fd
wip
Guzz-T Feb 3, 2026
8140ed0
wip
Guzz-T Feb 5, 2026
1e1d451
wip
Guzz-T Feb 5, 2026
205d9b9
wip
Guzz-T Feb 5, 2026
0402f8d
wip
Guzz-T Feb 5, 2026
e8c0df2
wip
Guzz-T Feb 5, 2026
2b954a3
wip
Guzz-T Feb 5, 2026
9f3215f
wip
Guzz-T Feb 5, 2026
de3bfc1
wip
Guzz-T Feb 5, 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
24 changes: 6 additions & 18 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,29 +25,16 @@
CALCULATIONS_DEFAULT_DATA_TYPE
)

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

logger = LOGGER
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 All @@ -55,7 +43,7 @@ def _get_firmware_version(self):
def get(self, target):
"""Treats certain names specially. For all others, the function of the base class is called."""
if target == "ID_WEB_SoftStand":
self.logger.debug("The name 'ID_WEB_SoftStand' is obsolete! Use 'get_firmware_version()' instead.")
LOGGER.debug("The name 'ID_WEB_SoftStand' is obsolete! Use 'get_firmware_version()' instead.")
entry = Base("ID_WEB_SoftStand")
entry.raw = self._get_firmware_version()
return entry
Expand Down
44 changes: 24 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,24 @@ 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 = {}
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 +275,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
34 changes: 4 additions & 30 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,36 +24,9 @@
PARAMETERS_DEFAULT_DATA_TYPE
)

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

logger = LOGGER
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:
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:
self.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
16 changes: 4 additions & 12 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,18 +24,9 @@
VISIBILITIES_DEFAULT_DATA_TYPE,
)

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

logger = LOGGER
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
Loading