From a532fdc1e6d1c1cb88a722fed8c6647e9ef90fd1 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Tue, 20 Jan 2026 20:22:23 +0100 Subject: [PATCH 01/61] wip --- README.md | 4 +- luxtronik/cfi/calculations.py | 9 +- luxtronik/cfi/interface.py | 36 +- luxtronik/cfi/parameters.py | 28 +- luxtronik/cfi/visibilities.py | 4 +- luxtronik/constants.py | 4 + luxtronik/data_vector.py | 607 ++++++++++++++++++++++++++---- luxtronik/definitions/__init__.py | 52 +-- luxtronik/shi/contiguous.py | 7 +- luxtronik/shi/interface.py | 9 +- luxtronik/shi/vector.py | 198 +--------- tests/cfi/test_cfi_parameters.py | 23 +- tests/shi/test_shi_contiguous.py | 35 +- tests/shi/test_shi_definitions.py | 121 ++++++ tests/test_compatibility.py | 33 +- tests/test_definition_list.py | 5 +- tests/test_socket_interaction.py | 40 +- 17 files changed, 786 insertions(+), 429 deletions(-) create mode 100644 tests/shi/test_shi_definitions.py diff --git a/README.md b/README.md index f40a9c36..96ab3409 100755 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/luxtronik/cfi/calculations.py b/luxtronik/cfi/calculations.py index 784f9be3..51d98785 100644 --- a/luxtronik/cfi/calculations.py +++ b/luxtronik/cfi/calculations.py @@ -11,7 +11,7 @@ ) 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 @@ -24,7 +24,7 @@ CALCULATIONS_DEFAULT_DATA_TYPE ) -class Calculations(DataVector): +class Calculations(DataVectorConfig): """Class that holds all calculations.""" name = CALCULATIONS_FIELD_NAME @@ -34,11 +34,6 @@ class Calculations(DataVector): "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 diff --git a/luxtronik/cfi/interface.py b/luxtronik/cfi/interface.py index 0d030f3b..7ddcc662 100644 --- a/luxtronik/cfi/interface.py +++ b/luxtronik/cfi/interface.py @@ -162,23 +162,25 @@ 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 index, field in parameters.items(): + if field.write_pending: + value = field.raw + if not isinstance(index, int) or not isinstance(value, int): + LOGGER.warning( + "%s: Parameter id '%s' or value '%s' invalid!", + self._host, + index, + value, + ) + field.write_pending = False + 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) + field.write_pending = False # Give the heatpump a short time to handle the value changes/calculations: time.sleep(WAIT_TIME_AFTER_PARAMETER_WRITE) diff --git a/luxtronik/cfi/parameters.py b/luxtronik/cfi/parameters.py index 345b7d1b..f0976028 100644 --- a/luxtronik/cfi/parameters.py +++ b/luxtronik/cfi/parameters.py @@ -11,7 +11,7 @@ ) from luxtronik.cfi.constants import PARAMETERS_FIELD_NAME -from luxtronik.data_vector import DataVector +from luxtronik.cfi.vector import DataVectorConfig LOGGER = logging.getLogger(__name__) @@ -23,35 +23,13 @@ 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: - 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) diff --git a/luxtronik/cfi/visibilities.py b/luxtronik/cfi/visibilities.py index 79d5f603..ec87c3bf 100644 --- a/luxtronik/cfi/visibilities.py +++ b/luxtronik/cfi/visibilities.py @@ -11,7 +11,7 @@ ) from luxtronik.cfi.constants import VISIBILITIES_FIELD_NAME -from luxtronik.data_vector import DataVector +from luxtronik.cfi.vector import DataVectorConfig LOGGER = logging.getLogger(__name__) @@ -23,7 +23,7 @@ VISIBILITIES_DEFAULT_DATA_TYPE, ) -class Visibilities(DataVector): +class Visibilities(DataVectorConfig): """Class that holds all visibilities.""" name = VISIBILITIES_FIELD_NAME diff --git a/luxtronik/constants.py b/luxtronik/constants.py index 894b3d31..35ddd4a7 100644 --- a/luxtronik/constants.py +++ b/luxtronik/constants.py @@ -21,3 +21,7 @@ LUXTRONIK_NAME_CHECK_NONE: Final = "none" LUXTRONIK_NAME_CHECK_PREFERRED: Final = "preferred" LUXTRONIK_NAME_CHECK_OBSOLETE: Final = "obsolete" + +# Since version 3.92.0, all unavailable 16 bit data fields +# have been returning this value (0x7FFF) +LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE: Final = 32767 diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index 97512c9b..902a3167 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -5,121 +5,580 @@ from luxtronik.constants import ( LUXTRONIK_NAME_CHECK_PREFERRED, LUXTRONIK_NAME_CHECK_OBSOLETE, + LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, ) -from luxtronik.collections import LuxtronikFieldsDictionary +from luxtronik.datatypes import Base +from luxtronik.definitions import LuxtronikDefinition, LuxtronikDefinitionsDictionary LOGGER = logging.getLogger(__name__) +############################################################################### +# Common functions +############################################################################### + +def pack_values(values, num_bits, reverse=True): + """ + Packs a list of data chunks into one integer. + + Args: + values (list[int]): raw data; distributed across multiple registers. + num_bits (int): Number of bits per chunk. + 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) + mask = (1 << num_bits) - 1 + + 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 & mask) << (num_bits * bit_index) + + return result + +def unpack_values(packed, count, num_bits, reverse=True): + """ + Unpacks 'count' chunks from a packed integer. + + Args: + packed (int): Packed raw data as a single integer value. + count (int): Number of chunks to unpack. + num_bits (int): Number of bits per chunk. + 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 = [] + mask = (1 << num_bits) - 1 + + 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 >> (num_bits * bit_index)) & mask + values.append(chunk) + + return values + +def integrate_data(definition, field, raw_data, data_offset=-1): + """ + Integrate raw values from a data array into the field. + + Args: + definition (LuxtronikDefinition): Meta-data of the field. + field (Base): Field object where to integrate the data. + raw_data (list): Source array of bytes/words. + data_offset (int): Optional offset. Defaults to `definition.index`. + """ + # Use data_offset if provided, otherwise the index + data_offset = data_offset if data_offset >= 0 else definition.index + # Use the information of the definition to extract the raw-value + if (data_offset + definition.count - 1) >= len(raw_data): + raw = None + elif definition.count == 1: + raw = raw_data[data_offset] + else: + raw = raw_data[data_offset : data_offset + definition.count] + raw = raw if len(raw) == definition.count else None + if field.concatenate_multiple_data_chunks and raw is not None: + # Usually big-endian (reverse=True) is used + raw = pack_values(raw, definition.reg_bits) + raw = raw if definition.check_raw_not_none(raw) else None + field.raw = raw + +def get_data_arr(definition, field): + """ + Normalize the field's data to a list of the correct size. + + Args: + definition (LuxtronikDefinition): Meta-data of the field. + field (Base): Field object that contains data to get. + + Returns: + list[int] | None: List of length `definition.count`, + 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, definition.reg_bits) + if not isinstance(data, list): + data = [data] + return data if len(data) == definition.count else None + +############################################################################### +# Definition / field pair +############################################################################### + +class LuxtronikDefFieldPair: + """ + Combines a definition and a field into a single iterable object. + """ + + def __init__(self, definition, field): + """ + Initialize a definition-field-pair. + + Args: + field (Base): The field object. + definition (LuxtronikDefinition): The definition for this field. + """ + self.field = field + self.definition = definition + + def __iter__(self): + yield self.definition + yield self.field + + @property + def index(self): + return self.definition.index + + @property + def addr(self): + return self.definition.addr + + @property + def count(self): + return self.definition.count + + def get_data_arr(self): + """ + Normalize the field's data to a list of the correct size. + + Returns: + list[int] | None: List of length `definition.count`, or None if insufficient. + """ + return get_data_arr(self.definition, self.field) + + def integrate_data(self, raw_data, data_offset=-1): + """ + Integrate the related parts of the `raw_data` into the field + + Args: + raw_data (list): Source array of bytes/words. + data_offset (int): Optional offset. Defaults to `definition.index`. + """ + integrate_data(self.definition, self.field, raw_data, data_offset) + +############################################################################### +# Field dictionary for data vectors +############################################################################### + +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. + Aliases are also supported. + """ + + def __init__(self): + # There may be several names or alias that points to one definition. + # So in order to spare memory we split the name/index-to-field-lookup + # into a name/index-to-definition-lookup and a definition-to-field-lookup + self._def_lookup = LuxtronikDefinitionsDictionary() + self._field_lookup = {} + # Furthermore stores the definition-to-field-lookup separate from the + # field-definition pairs to keep the index-sorted order when adding new entries + self._pairs = [] # list of LuxtronikDefFieldPair + + def __getitem__(self, def_field_name_or_idx): + return self.get(def_field_name_or_idx) + + def __setitem__(self, def_name_or_idx, value): + assert False, "__setitem__ not implemented." + + def __len__(self): + return len(self._def_lookup._index_dict) + + 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]) + + def __contains__(self, def_field_name_or_idx): + """ + Check whether the data vector contains a name, index, + or definition matching an added field, or the field itself. + + Args: + def_field_name_or_idx (LuxtronikDefinition | Base | str | int): + Definition object, field object, field name or register index. + + Returns: + True if the searched element was found, otherwise False. + """ + if isinstance(def_field_name_or_idx, Base): + return any(def_field_name_or_idx is field for field in self._field_lookup.values()) + elif isinstance(def_field_name_or_idx, LuxtronikDefinition): + # speed-up the look-up by search only the name-dict + return def_field_name_or_idx.name in self._def_lookup._name_dict + else: + 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]) + + 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]) + + def pairs(self): + """ + Return all definition-field-pairs contained herein. + """ + return self._pairs + + @property + def def_dict(self): + """Return the internal definition dictionary""" + return self._def_lookup + + def add(self, definition, field, alias=None): + """ + Add a definition-field-pair to the internal dictionaries. + + Args: + definition (LuxtronikDefinition): Definition related to the field. + field (Base): Field to add. + alias (Hashable | None): Alias, which can be used to access the field again. + """ + if definition.valid: + self._def_lookup.add(definition, alias) + self._field_lookup[definition] = field + self._pairs.append(LuxtronikDefFieldPair(definition, field)) + + def add_sorted(self, definition, field, alias=None): + if definition.valid: + self.add(definition, field, alias) + # sort _pairs by definition.index + self._pairs.sort(key=lambda pair: pair.definition.index) + + def register_alias(self, def_field_name_or_idx, alias): + """ + Add an alternative name (or anything hashable else) + that can be used to access a specific field. + + Args: + def_field_name_or_idx (LuxtronikDefinition | Base | str | int): + Field to which the alias is to be added. + Either by definition, name, register index, or the field itself. + alias (Hashable): Alias, which can be used to access the field again. + + Returns: + Base | None: The field to which the alias was added, + or None if not possible + """ + # Resolve a field input + def_name_or_idx = def_field_name_or_idx + if isinstance(def_name_or_idx, Base): + def_name_or_idx = def_name_or_idx.name + # register alias + definition = self._def_lookup.register_alias(def_name_or_idx, alias) + if definition is None: + return None + return self._field_lookup.get(definition, None) + + def get(self, def_field_name_or_idx, default=None): + """ + Retrieve a field by definition, name or register index. + + Args: + def_field_name_or_idx (LuxtronikDefinition | str | int): + Definition, name, or register index to be used to search for the field. + + Returns: + Base | None: The field found or the provided default if not found. + + Note: + If multiple fields added for the same index/name, + the last added takes precedence. + """ + def_name_or_idx = def_field_name_or_idx + if isinstance(def_name_or_idx, Base): + def_name_or_idx = def_name_or_idx.name + if isinstance(def_name_or_idx, LuxtronikDefinition): + definition = def_name_or_idx + else: + definition = self._def_lookup.get(def_name_or_idx) + if definition is not None: + return self._field_lookup.get(definition, default) + return default + ############################################################################### # Base class for all luxtronik data vectors ############################################################################### class DataVector: - """Class that holds a vector of data entries.""" + """ + Class that holds a vector of data entries. + + Provides access to fields by name, index or alias. + To use aliases, they must first be registered here (locally = only valid + for this vector) or directly in the `LuxtronikDefinitionsList` + (globally = valid for all newly created vector). + """ + logger = LOGGER name = "DataVector" + # DataVector specific list of definitions as `LuxtronikDefinitionsList` + definitions = None # override this + _obsolete = {} + +# Field construction methods ################################################## + + @classmethod + def create_unknown_field(cls, idx): + """ + Create an unknown field object. + Be careful! The used controller firmware + may not support this field. + + Args: + idx (int): Register index. + + Returns: + Unknown: A field instance of type 'Unknown'. + """ + return Unknown(f"unknown_{cls.name}_{idx}", False) + + @classmethod + def create_any_field(cls, name_or_idx): + """ + Create a field object from an available definition + (= included in class variable `cls.definitions`). + Be careful! The used controller firmware + may not support this field. + + Args: + name_or_idx (str | int): Field name or register index. + + Returns: + Base | None: The created field, or None if not found or not valid. + """ + # The definitions object hold all available definitions + definition = cls.definitions.get(name_or_idx) + if definition is not None and definition.valid: + return definition.create_field() + return None + + def create_field(self, name_or_idx): + """ + Create a field object from a version-dependent definition (= included in + class variable `cls.definitions` and is valid for `self.version`). + + Args: + name_or_idx (str | int): Field name or register index. + + Returns: + Base | None: The created field, or None if not found or not valid. + """ + definition, _ = self._get_definition(name_or_idx, False) + if definition is not None and definition.valid: + return definition.create_field() + return None + + +# constructor, magic methods and iterators #################################### + def __init__(self): """Initialize DataVector class.""" self._data = LuxtronikFieldsDictionary() - def __iter__(self): - """Iterator for the data entries.""" - return iter(self._data.items()) - @property def data(self): - """ - Return the internal `LuxtronikFieldsDictionary`. - Please check its documentation. - """ return self._data def __getitem__(self, def_name_or_idx): - """ - Array-style access to method `get`. - Please check its documentation. - """ return self.get(def_name_or_idx) def __setitem__(self, def_name_or_idx, value): - """ - Array-style access to method `set`. - Please check its documentation. - """ return self.set(def_name_or_idx, value) + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._data) def __contains__(self, def_field_name_or_idx): """ - Forward the `LuxtronikFieldsDictionary.__contains__` method. - Please check its documentation. + Check whether the data vector contains a name, index, + or definition matching an added field, or the field itself. + + Args: + def_field_name_or_idx (LuxtronikDefinition | Base | str | int): + Definition object, field object, field name or register index. + + Returns: + True if the searched element was found, otherwise False. """ return def_field_name_or_idx in self._data - def _name_lookup(self, name): + def values(self): + return iter(self._data.values()) + + def items(self): + return iter(self._data.items()) + + +# Alias methods ############################################################### + + def register_alias(self, def_field_name_or_idx, alias): """ - Try to find the index using the given field name. + Add an alternative name (or anything hashable else) + that can be used to access a specific field. Args: - name (string): Field name. + def_field_name_or_idx (LuxtronikDefinition | Base | str | int): + Field to which the alias is to be added. + Either by definition, name, register index, or the field itself. + alias (Hashable): Alias, which can be used to access the field again. Returns: - tuple[int | None, str | None]: - 0: Index found or None - 1: New preferred name, if available, otherwise None + Base | None: The field to which the alias was added, + or None if not possible + """ + return self._data.register_alias(def_field_name_or_idx, alias) + + +# Parse methods ############################################################### + + def parse(self, raw_data): """ - obsolete_entry = self._obsolete.get(name, None) + Parse raw data into the corresponding fields. + + Args: + raw_data (list[int]): List of raw register values. + The raw data must start at register index 0. + """ + raw_len = len(raw_data) + undefined = {i for i in range(0, raw_len)} + for definition, field in self._data.pairs(): + next_idx = definition.index + definition.count + if next_idx >= raw_len: + # not enough registers + continue + for index in range(definition.index, next_idx): + undefined.discard(index) + integrate_data(definition, field, raw_data) + field.write_pending = False + # create an unknown field for additional data + for index in undefined: + # self.logger.warning(f"Entry '%d' not in list of {self.name}", index) + definition = LuxtronikDefinition.unknown(index, self.name, self.definitions.offset) + field = definition.create_field() + field.raw = raw_data[index] + self._data.add_sorted(definition, field) + + +# Get and set methods ######################################################### + + def _get_definition(self, def_field_name_or_idx, all_not_version_dependent): + """ + Look-up a definition by name, index, a field instance or by the definition itself. + + Args: + def_field_name_or_idx (LuxtronikDefinition | Base | str | int): + Definition object, field object, field name or register index. + all_not_version_dependent (bool): If true, look up the definition + within the `cls.definitions` otherwise within `self._data` (which + contain all definitions related to all added fields) + + Returns: + tuple[LuxtronikDefinition | None, Base | None]: + A definition-field-pair tuple: + Index 0: Return the found or given definitions, otherwise None + Index 1: Return the given field, otherwise None + """ + definition = def_field_name_or_idx + field = None + if isinstance(def_field_name_or_idx, Base): + definition = def_field_name_or_idx.name + field = def_field_name_or_idx + if not isinstance(def_field_name_or_idx, LuxtronikDefinition): + if all_not_version_dependent: + definition = self.definitions.get(definition) + else: + # def_dict contains only valid and addable definitions + definition = self._data.def_dict.get(definition) + return definition, field + + def get(self, def_name_or_idx, default=None): + """ + Retrieve a field by definition, name or register index. + + Args: + def_name_or_idx (LuxtronikDefinition | str | int): + Definition, name, or register index to be used to search for the field. + + Returns: + Base | None: The field found or the provided default if not found. + + Note: + If multiple fields added for the same index/name, + the last added takes precedence. + """ + obsolete_entry = self._obsolete.get(def_name_or_idx, None) if obsolete_entry: - return None, obsolete_entry - for index, entry in self._data.items(): - check_result = entry.check_name(name) - if check_result == LUXTRONIK_NAME_CHECK_PREFERRED: - return index, None - elif check_result == LUXTRONIK_NAME_CHECK_OBSOLETE: - return index, entry.name - return None, None - - def _lookup(self, target, with_index=False): - """ - Lookup an entry - - "target" could either be its id or its name. - - In case "with_index" is set, also the index is returned. - """ - if isinstance(target, str): - try: - # Try to get entry by id - target_index = int(target) - except ValueError: - # Get entry by name - target_index, new_name = self._name_lookup(target) - if new_name is not None: - raise KeyError(f"The name '{target}' is obsolete! Use '{new_name}' instead.") - elif isinstance(target, int): - # Get entry by id - target_index = target - else: - target_index = None - - target_entry = self._data.get(target_index, None) - if target_entry is None: - LOGGER.warning("entry '%s' not found", target) - if with_index: - return target_index, target_entry - return target_entry - - def get(self, target): - """Get entry by id or name.""" - entry = self._lookup(target) - return entry - - def set(self, target, value): - "TODO: Placeholder for future changes" - pass + raise KeyError(f"The name '{def_name_or_idx}' is obsolete! Use '{obsolete_entry}' instead.") + field = self._data.get(def_name_or_idx, default) + if field is None: + self.logger.warning(f"entry '{def_name_or_idx}' not found") + return field + + def set(self, def_field_name_or_idx, value): + """ + Set field to new value. + + The value is set, even if the field marked as non-writeable. + No data validation is performed either. + + Args: + def_field_name_or_idx (LuxtronikDefinition | Base | int | str): + Definition, name, or register index to be used to search for the field. + It is also possible to pass the field itself. + value (int | List[int]): Value to set + """ + field = def_field_name_or_idx + if not isinstance(field, Base): + field = self.get(def_field_name_or_idx) + if field is not None: + field.value = value \ No newline at end of file diff --git a/luxtronik/definitions/__init__.py b/luxtronik/definitions/__init__.py index 9afa9318..c5b1924f 100644 --- a/luxtronik/definitions/__init__.py +++ b/luxtronik/definitions/__init__.py @@ -38,17 +38,10 @@ class LuxtronikDefinition: "names": [], "since": "", "until": "", - "datatype": "", - "bit_offset": None, - "bit_count": None, + "datatype": "UINT32", "description": "", } - # It is permissible not to specify a data type. - # In this case, all functions based on it will be disabled. - VALID_DATA_TYPES = ("", "UINT16", "UINT32", "UINT64", "INT16", "INT32", "INT64") - - def __init__(self, data_dict, type_name, offset): """ Initialize a definition from a data-dictionary. @@ -90,23 +83,14 @@ def __init__(self, data_dict, type_name, offset): self._offset = int(offset) self._addr = self._offset + self._index self._data_type = data_dict["datatype"] - data_type_valid = self._data_type in self.VALID_DATA_TYPES - self._valid &= data_type_valid - data_type_valid &= self._data_type != "" - self._bit_offset = data_dict["bit_offset"] - bit_count = data_dict["bit_count"] - if bit_count: - self._num_bits = bit_count - else: - self._num_bits = int(self._data_type.replace('U', '').replace('INT', '')) \ - if data_type_valid else 0 + self._num_bits = int(self._data_type.replace('U', '').replace('INT', '')) except Exception as e: self._valid = False self._index = 0 LOGGER.error(f"Failed to create LuxtronikDefinition: '{e}' with {data_dict}") @classmethod - def unknown(cls, index, type_name, offset, data_type=""): + def unknown(cls, index, type_name, offset): """ Create an "unknown" definition. @@ -114,15 +98,13 @@ def unknown(cls, index, type_name, offset, data_type=""): index (int): The register index of the "unknown" definition. type_name (str): The type name e.g. 'holding', 'input', ... . offset (str): Offset of the address from the specified index. - data_type (str): Data type of the field (see VALID_DATA_TYPES). Returns: LuxtronikDefinition: A definition marked as unknown. """ return cls({ "index": index, - "names": [f"unknown_{type_name.lower()}_{index}"], - "datatype": data_type, + "names": [f"unknown_{type_name.lower()}_{index}"] }, type_name, offset) def __bool__(self): @@ -167,18 +149,15 @@ def field_type(self): def writeable(self): return self._writeable - @property - def data_type(self): - return self._data_type - - @property - def bit_offset(self): - return self._bit_offset - @property def num_bits(self): return self._num_bits + @property + def reg_bits(self): + """Return the number of bits per register.""" + return self._num_bits // self._count + @property def names(self): return self._names @@ -249,7 +228,7 @@ def __getitem__(self, name_or_idx): def __contains__(self, def_name_or_idx): if isinstance(def_name_or_idx, LuxtronikDefinition): - return any(def_name_or_idx is d for d in self._name_dict.values()) + return any(def_name_or_idx is d for d in self._index_dict.values()) return self._get(def_name_or_idx) is not None def _add_alias(self, definition, alias): @@ -437,17 +416,16 @@ class LuxtronikDefinitionsList: (locally = only valid for that dictionary). """ - def _init_instance(self, name, offset, default_data_type, version): + def _init_instance(self, name, offset, version): """Re-usable method to initialize all instance variables.""" self._name = name self._offset = offset - self._default_data_type = default_data_type self._version = version # sorted list of all definitions self._definitions = [] self._lookup = LuxtronikDefinitionsDictionary() - def __init__(self, definitions_list, name, offset, default_data_type): + def __init__(self, definitions_list, name, offset): """ Initialize the (by index sorted) definitions list. @@ -464,7 +442,7 @@ def __init__(self, definitions_list, name, offset, default_data_type): - The value of count must always be greater than or equal to 1 - All names should be unique """ - self._init_instance(name, offset, default_data_type, None) + self._init_instance(name, offset, None) # Add definition objects only for valid items. # The correct sorting has already been ensured by the pytest @@ -486,7 +464,7 @@ def filtered(cls, definitions, version): If None is passed, all available fields are added. """ obj = cls.__new__(cls) # this don't call __init__() - obj._init_instance(definitions.name, definitions.offset, definitions._default_data_type, version) + obj._init_instance(definitions.name, definitions.offset, version) for d in definitions: if d.valid and version_in_range(obj._version, d.since, d.until): @@ -520,7 +498,7 @@ def create_unknown_definition(self, index): Returns: LuxtronikDefinition: A definition marked as unknown. """ - return LuxtronikDefinition.unknown(index, self._name, self._offset, self._default_data_type) + return LuxtronikDefinition.unknown(index, self._name, self._offset) def register_alias(self, def_name_or_idx, alias): """ diff --git a/luxtronik/shi/contiguous.py b/luxtronik/shi/contiguous.py index fcb6c500..7c598a82 100644 --- a/luxtronik/shi/contiguous.py +++ b/luxtronik/shi/contiguous.py @@ -6,8 +6,7 @@ import logging -from luxtronik.collections import LuxtronikDefFieldPair -from luxtronik.shi.constants import LUXTRONIK_SHI_REGISTER_BIT_SIZE +from luxtronik.data_vector import LuxtronikDefFieldPair LOGGER = logging.getLogger(__name__) @@ -171,7 +170,7 @@ def integrate_data(self, data_arr): first = self.first_index for part in self._parts: data_offset = part.index - first - part.integrate_data(data_arr, LUXTRONIK_SHI_REGISTER_BIT_SIZE, data_offset) + part.integrate_data(data_arr, data_offset) return True @@ -192,7 +191,7 @@ def get_data_arr(self): valid = True for part in self._parts: data_offset = part.index - first - data = part.get_data_arr(LUXTRONIK_SHI_REGISTER_BIT_SIZE) + data = part.get_data_arr() if data is None: valid = False diff --git a/luxtronik/shi/interface.py b/luxtronik/shi/interface.py index 025bbc0c..b924a84c 100644 --- a/luxtronik/shi/interface.py +++ b/luxtronik/shi/interface.py @@ -2,17 +2,14 @@ import logging -from luxtronik.collections import get_data_arr from luxtronik.common import classproperty, version_in_range from luxtronik.datatypes import Base +from luxtronik.data_vector import get_data_arr from luxtronik.definitions import ( LuxtronikDefinition, LuxtronikDefinitionsList, ) -from luxtronik.shi.constants import ( - LUXTRONIK_LATEST_SHI_VERSION, - LUXTRONIK_SHI_REGISTER_BIT_SIZE -) +from luxtronik.shi.constants import LUXTRONIK_LATEST_SHI_VERSION from luxtronik.shi.common import ( LuxtronikSmartHomeReadHoldingsTelegram, LuxtronikSmartHomeReadInputsTelegram, @@ -390,7 +387,7 @@ def _prepare_write_field(self, definition, field, safe, data): field.value = data # Abort if insufficient data is provided - if not get_data_arr(definition, field, LUXTRONIK_SHI_REGISTER_BIT_SIZE): + if not get_data_arr(definition, field): LOGGER.warning("Data error / insufficient data provided: " \ + f"name={definition.name}, data={field.raw}") return False diff --git a/luxtronik/shi/vector.py b/luxtronik/shi/vector.py index 36e7d39b..33882480 100644 --- a/luxtronik/shi/vector.py +++ b/luxtronik/shi/vector.py @@ -2,8 +2,7 @@ import logging from luxtronik.common import version_in_range -from luxtronik.collections import LuxtronikFieldsDictionary -from luxtronik.data_vector import DataVector +from luxtronik.data_vector import LuxtronikFieldsDictionary, DataVector from luxtronik.datatypes import Base, Unknown from luxtronik.definitions import LuxtronikDefinition @@ -18,73 +17,7 @@ ############################################################################### class DataVectorSmartHome(DataVector): - """ - Specialized DataVector for Luxtronik smart home fields. - - Provides access to fields by name, index or alias. - To use aliases, they must first be registered here (locally = only valid - for this vector) or directly in the `LuxtronikDefinitionsList` - (globally = valid for all newly created vector). - """ - - # DataVector specific list of definitions as `LuxtronikDefinitionsList` - definitions = None # override this - -# Field construction methods ################################################## - - @classmethod - def create_unknown_field(cls, idx): - """ - Create an unknown field object. - Be careful! The used controller firmware - may not support this field. - - Args: - idx (int): Register index. - - Returns: - Unknown: A field instance of type 'Unknown'. - """ - return Unknown(f"unknown_{cls.name}_{idx}", False) - - @classmethod - def create_any_field(cls, name_or_idx): - """ - Create a field object from an available definition - (= included in class variable `cls.definitions`). - Be careful! The used controller firmware - may not support this field. - - Args: - name_or_idx (str | int): Field name or register index. - - Returns: - Base | None: The created field, or None if not found or not valid. - """ - # The definitions object hold all available definitions - definition = cls.definitions.get(name_or_idx) - if definition is not None and definition.valid: - return definition.create_field() - return None - - def create_field(self, name_or_idx): - """ - Create a field object from a version-dependent definition (= included in - class variable `cls.definitions` and is valid for `self.version`). - - Args: - name_or_idx (str | int): Field name or register index. - - Returns: - Base | None: The created field, or None if not found or not valid. - """ - definition, _ = self._get_definition(name_or_idx, False) - if definition is not None and definition.valid: - return definition.create_field() - return None - - -# Constructors and magic methods ############################################## + """Specialized DataVector for Luxtronik smart home fields.""" def _init_instance(self, version, safe): """Re-usable method to initialize all instance variables.""" @@ -143,57 +76,22 @@ def empty(cls, version=LUXTRONIK_LATEST_SHI_VERSION, safe=True): obj._init_instance(version, safe) return obj - def __len__(self): - return len(self._data.pairs()) - - def __iter__(self): - return iter([definition for definition, _ in self._data.pairs()]) - - -# properties and access methods ############################################### - @property def version(self): return self._version - def values(self): - return iter([field for _, field in self._data.pairs()]) - - def items(self): - return iter(self._data.pairs()) - - -# Find, add and alias methods ################################################# - - def _get_definition(self, def_field_name_or_idx, all_not_version_dependent): + def update_read_blocks(self): """ - Look-up a definition by name, index, a field instance or by the definition itself. - - Args: - def_field_name_or_idx (LuxtronikDefinition | Base | str | int): - Definition object, field object, field name or register index. - all_not_version_dependent (bool): If true, look up the definition - within the `cls.definitions` otherwise within `self.def_dict` (which - contain all definitions related to all added fields) + (Re-)Create the data block list (`ContiguousDataBlockList`) for read-operations. - Returns: - tuple[LuxtronikDefinition | None, Base | None]: - A definition-field-pair tuple: - Index 0: Return the found or given definitions, otherwise None - Index 1: Return the given field, otherwise None + Since the data blocks do not change as long as no new fields are added, + it is sufficient to regenerate them only when a change occurs. """ - definition = def_field_name_or_idx - field = None - if isinstance(def_field_name_or_idx, Base): - definition = def_field_name_or_idx.name - field = def_field_name_or_idx - if not isinstance(def_field_name_or_idx, LuxtronikDefinition): - if all_not_version_dependent: - definition = self.definitions.get(definition) - else: - # def_dict contains only valid and addable definitions - definition = self._data.def_dict.get(definition) - return definition, field + if not self._read_blocks_up_to_date: + self._read_blocks.clear() + for definition, field in self._data.pairs(): + self._read_blocks.collect(definition, field) + self._read_blocks_up_to_date = True def add(self, def_field_name_or_idx, alias=None): """ @@ -234,76 +132,4 @@ def add(self, def_field_name_or_idx, alias=None): self._read_blocks_up_to_date = False self._data.add_sorted(definition, field, alias) return field - return None - - def register_alias(self, def_field_name_or_idx, alias): - """ - Add an alternative name (or anything hashable else) - that can be used to access a specific field. - - Args: - def_field_name_or_idx (LuxtronikDefinition | Base | str | int): - Field to which the alias is to be added. - Either by definition, name, register index, or the field itself. - alias (Hashable): Alias, which can be used to access the field again. - - Returns: - Base | None: The field to which the alias was added, - or None if not possible - """ - return self._data.register_alias(def_field_name_or_idx, alias) - - -# Data-blocks methods ######################################################### - - def update_read_blocks(self): - """ - (Re-)Create the data block list (`ContiguousDataBlockList`) for read-operations. - - Since the data blocks do not change as long as no new fields are added, - it is sufficient to regenerate them only when a change occurs. - """ - if not self._read_blocks_up_to_date: - self._read_blocks.clear() - for definition, field in self._data.pairs(): - self._read_blocks.collect(definition, field) - self._read_blocks_up_to_date = True - - -# Data and access methods ##################################################### - - def get(self, def_name_or_idx, default=None): - """ - Retrieve a field by definition, name or register index. - - Args: - def_name_or_idx (LuxtronikDefinition | str | int): - Definition, name, or register index to be used to search for the field. - - Returns: - Base | None: The field found or the provided default if not found. - - Note: - If multiple fields added for the same index/name, - the last added takes precedence. - """ - return self._data.get(def_name_or_idx, default) - - def set(self, def_field_name_or_idx, value): - """ - Set field to new value. - - The value is set, even if the field marked as non-writeable. - No data validation is performed either. - - Args: - def_field_name_or_idx (LuxtronikDefinition | Base | int | str): - Definition, name, or register index to be used to search for the field. - It is also possible to pass the field itself. - value (int | List[int]): Value to set - """ - field = def_field_name_or_idx - if not isinstance(field, Base): - field = self.get(def_field_name_or_idx) - if field is not None: - field.value = value \ No newline at end of file + return None \ No newline at end of file diff --git a/tests/cfi/test_cfi_parameters.py b/tests/cfi/test_cfi_parameters.py index 417e146c..dc2f55c9 100644 --- a/tests/cfi/test_cfi_parameters.py +++ b/tests/cfi/test_cfi_parameters.py @@ -15,11 +15,9 @@ def test_init(self): assert parameters.name == "parameter" assert parameters.parameters == parameters._data assert parameters.safe - assert len(parameters.queue) == 0 parameters = Parameters(False) assert not parameters.safe - assert len(parameters.queue) == 0 def test_data(self): """Test cases for the data dictionary""" @@ -84,18 +82,21 @@ def test_set(self): # Set something which does not exist parameters.set("BarFoo", 0) - assert len(parameters.queue) == 0 + assert paramters["BarFoo"] is not None - # Set something which is not allowed to be set - parameters.set("ID_Transfert_LuxNet", 0) - assert len(parameters.queue) == 0 + # Set something which was previously not allowed to be set + parameters.set("ID_Transfert_LuxNet", 1) + assert paramters["ID_Transfert_LuxNet"].raw == 1 + assert paramters["ID_Transfert_LuxNet"].write_pending # Set something which is allowed to be set - parameters.set("ID_Einst_WK_akt", 0) - assert len(parameters.queue) == 1 + parameters.set("ID_Einst_WK_akt", 2) + assert paramters["ID_Einst_WK_akt"].raw == 2 + assert paramters["ID_Einst_WK_akt"].write_pending parameters = Parameters(safe=False) - # Set something which is not allowed to be set, but we are brave. - parameters.set("ID_Transfert_LuxNet", 0) - assert len(parameters.queue) == 1 + # Set something which was previously not allowed to be set, but we are brave. + parameters.set("ID_Transfert_LuxNet", 4) + assert paramters["ID_Transfert_LuxNet"].raw == 4 + assert paramters["ID_Transfert_LuxNet"].write_pending diff --git a/tests/shi/test_shi_contiguous.py b/tests/shi/test_shi_contiguous.py index 62a27d50..888ff675 100644 --- a/tests/shi/test_shi_contiguous.py +++ b/tests/shi/test_shi_contiguous.py @@ -2,7 +2,6 @@ from luxtronik.constants import LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE from luxtronik.datatypes import Base from luxtronik.definitions import LuxtronikDefinition -from luxtronik.shi.constants import LUXTRONIK_SHI_REGISTER_BIT_SIZE from luxtronik.shi.contiguous import ( ContiguousDataPart, ContiguousDataBlock, @@ -20,32 +19,26 @@ def_a1 = LuxtronikDefinition({ 'index': 1, 'count': 1, - 'datatype': 'INT16', }, 'test', 100) def_a = LuxtronikDefinition({ 'index': 1, 'count': 2, - 'datatype': 'INT16', }, 'test', 100) def_b = LuxtronikDefinition({ 'index': 3, 'count': 1, - 'datatype': 'INT16', }, 'test', 100) def_c = LuxtronikDefinition({ 'index': 4, 'count': 3, - 'datatype': 'INT16', }, 'test', 100) def_c1 = LuxtronikDefinition({ 'index': 4, 'count': 1, - 'datatype': 'INT16', }, 'test', 100) def_c2 = LuxtronikDefinition({ 'index': 5, 'count': 1, - 'datatype': 'INT16', }, 'test', 100) defs = [] @@ -74,49 +67,49 @@ def test_repr(self): def test_get_data(self): part = ContiguousDataPart(def_a, field_a) field_a.raw = [4, 2] - assert part.get_data_arr(LUXTRONIK_SHI_REGISTER_BIT_SIZE) == [4, 2] + assert part.get_data_arr() == [4, 2] field_a.raw = [1, 3, 5] - assert part.get_data_arr(LUXTRONIK_SHI_REGISTER_BIT_SIZE) is None + assert part.get_data_arr() is None field_a.raw = [9] - assert part.get_data_arr(LUXTRONIK_SHI_REGISTER_BIT_SIZE) is None + assert part.get_data_arr() is None part = ContiguousDataPart(def_a1, field_a1) field_a1.raw = [8] - assert part.get_data_arr(LUXTRONIK_SHI_REGISTER_BIT_SIZE) == [8] + assert part.get_data_arr() == [8] field_a1.raw = 7 - assert part.get_data_arr(LUXTRONIK_SHI_REGISTER_BIT_SIZE) == [7] + assert part.get_data_arr() == [7] def test_integrate_data(self): part = ContiguousDataPart(def_a, field_a) - part.integrate_data([1, 5, 7, 9], LUXTRONIK_SHI_REGISTER_BIT_SIZE, 0) + part.integrate_data([1, 5, 7, 9], 0) assert part.field.raw == [1, 5] - part.integrate_data([1, 5, 7, 9], LUXTRONIK_SHI_REGISTER_BIT_SIZE) + part.integrate_data([1, 5, 7, 9]) assert part.field.raw == [5, 7] - part.integrate_data([1, 5, 7, 9], LUXTRONIK_SHI_REGISTER_BIT_SIZE, 2) + part.integrate_data([1, 5, 7, 9], 2) assert part.field.raw == [7, 9] - part.integrate_data([1, 5, 7, 9], LUXTRONIK_SHI_REGISTER_BIT_SIZE, 3) + part.integrate_data([1, 5, 7, 9], 3) assert part.field.raw is None - part.integrate_data([1, 5, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, 9], LUXTRONIK_SHI_REGISTER_BIT_SIZE, 1) - assert part.field.raw == [5, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] + part.integrate_data([1, 5, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, 9], 1) + assert part.field.raw is None part = ContiguousDataPart(def_c1, field_c1) - part.integrate_data([2, 4, 6], LUXTRONIK_SHI_REGISTER_BIT_SIZE, 1) + part.integrate_data([2, 4, 6], 1) assert part.field.raw == 4 - part.integrate_data([2, 4, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE], LUXTRONIK_SHI_REGISTER_BIT_SIZE, 2) + part.integrate_data([2, 4, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE], 2) assert part.field.raw is None - part.integrate_data([2, 4, 6], LUXTRONIK_SHI_REGISTER_BIT_SIZE, 5) + part.integrate_data([2, 4, 6], 5) assert part.field.raw is None diff --git a/tests/shi/test_shi_definitions.py b/tests/shi/test_shi_definitions.py new file mode 100644 index 00000000..d4297420 --- /dev/null +++ b/tests/shi/test_shi_definitions.py @@ -0,0 +1,121 @@ +from luxtronik.constants import LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE +from luxtronik.definitions import LuxtronikDefinition +from luxtronik.data_vector import ( + get_data_arr, + integrate_data, +) + +############################################################################### +# Tests +############################################################################### + +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 + field.raw = 5 + arr = get_data_arr(definition, field) + assert arr == [5] + assert check_data(definition, field) + + # get from array + definition._count = 2 + field.raw = [7, 3] + arr = get_data_arr(definition, field) + assert arr == [7, 3] + assert check_data(definition, field) + + # too much data + definition._count = 2 + field.raw = [4, 8, 1] + arr = get_data_arr(definition, field) + assert arr is None + assert not check_data(definition, field) + + # insufficient data + definition._count = 2 + field.raw = [9] + arr = get_data_arr(definition, field) + 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] + + # set array + definition._count = 2 + integrate_data(definition, field, data) + assert field.raw == [3, 4] + integrate_data(definition, field, data, 4) + assert field.raw == [5, 6] + 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 == 3 + integrate_data(definition, field, data, 5) + assert field.raw == 6 + 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 \ No newline at end of file diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index 4a0f80c3..a6eeb019 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -1204,9 +1204,7 @@ def test_compatibilities(self): "Unknown_Parameter_1155": 1155, "Unknown_Parameter_1156": 1156, "Unknown_Parameter_1157": 1157, - "Unknown_Parameter_1158": 1158, "POWER_LIMIT_SWITCH": 1158, - "Unknown_Parameter_1159": 1159, "POWER_LIMIT_VALUE": 1159, } @@ -1913,12 +1911,12 @@ def test_compatibilities(self): # First, we check if we can find all entries of the above dicts. ok = True - for mapping, data_vector, caption in values: + for mapping, obj, caption in values: print_caption = True for old_name, old_idx in mapping.items(): - cur_idx, new_name = data_vector._name_lookup(old_name) - cur_name = data_vector.get(old_idx).name - if cur_idx != old_idx or (new_name is not None and new_name != cur_name): + def_by_name = obj.definitions.get(old_name) + def_by_idx = obj.definitions.get(old_idx) + if (def_by_name.index != old_idx) or (def_by_idx.name != def_by_name.name): # We do not use assert here, in order to catch all incompatibilities at once. if print_caption: print(f"### Incompatibilities - {caption}:") @@ -1929,16 +1927,15 @@ def test_compatibilities(self): # Second, we check if all names are present in the above dicts. ok = True - for mapping, data_vector, caption in values: + for mapping, obj, caption in values: print_caption = True - for definition in data_vector.definitions: - for name in definition.names: - if name not in mapping: - # We do not use assert here, in order to catch all incompatibilities at once. - # The output can be copied to the dicts above - if print_caption: - print(f"### Missing - {caption}:") - print_caption = False - print(f'"{name}": {definition.index},') - ok = False - assert ok, "Found missing entries. Please consider to add them to the test suite." \ No newline at end of file + for cur_idx, entry in obj: + if entry.name not in mapping: + # We do not use assert here, in order to catch all incompatibilities at once. + # The output can be copied to the dicts above + if print_caption: + print(f"### Missing - {caption}:") + print_caption = False + print(f'"{entry.name}": {cur_idx},') + ok = False + assert ok, f"Found missing {obj.name}. Please consider to add them to the test suite." \ No newline at end of file diff --git a/tests/test_definition_list.py b/tests/test_definition_list.py index a84c7f06..67fff156 100644 --- a/tests/test_definition_list.py +++ b/tests/test_definition_list.py @@ -2,7 +2,6 @@ from luxtronik.common import parse_version from luxtronik.datatypes import Base -from luxtronik.definitions import LuxtronikDefinition from luxtronik.definitions.calculations import CALCULATIONS_DEFINITIONS_LIST from luxtronik.definitions.holdings import HOLDINGS_DEFINITIONS_LIST from luxtronik.definitions.inputs import INPUTS_DEFINITIONS_LIST @@ -25,6 +24,8 @@ KEY_UNTIL = "until" KEY_DESC = "description" +VALID_DATA_TYPES = ("UINT16", "UINT32", "INT16", "INT32") + class RunTestDefinitionList: @@ -185,7 +186,7 @@ def test_data_type(self): for definition in self.definitions: if KEY_DATATYPE in definition: data_type = definition[KEY_DATATYPE] - assert data_type in LuxtronikDefinition.VALID_DATA_TYPES, \ + assert data_type in VALID_DATA_TYPES, \ f"Datatype must be set correctly: {definition}" def test_since(self): diff --git a/tests/test_socket_interaction.py b/tests/test_socket_interaction.py index bf0a1577..c6e50dbb 100644 --- a/tests/test_socket_interaction.py +++ b/tests/test_socket_interaction.py @@ -3,7 +3,6 @@ import unittest.mock as mock from luxtronik import Luxtronik, LuxtronikSocketInterface, Parameters, Calculations, Visibilities -from luxtronik.collections import integrate_data from tests.fake import ( fake_create_connection, fake_parameter_value, @@ -17,7 +16,6 @@ @mock.patch("socket.create_connection", fake_create_connection) @mock.patch("luxtronik.LuxtronikModbusTcpInterface", FakeModbus) class TestSocketInteraction: - def check_luxtronik_data(self, lux, check_for_true=True): cp = self.check_data_vector(lux.parameters) cc = self.check_data_vector(lux.calculations) @@ -34,12 +32,8 @@ def check_data_vector(self, data_vector): fct = fake_calculation_value elif type(data_vector) is Visibilities: fct = fake_visibility_value - for d, f in data_vector.data.pairs(): - # get raw data - raw = [fct(idx) for idx in range(d.index, d.index + d.count)] - temp_field = d.create_field() - integrate_data(d, temp_field, raw, 32, 0) - if f.raw != temp_field.raw: + for idx, entry in data_vector.items(): + if entry.raw != fct(idx): return False return True @@ -49,7 +43,7 @@ def clear_luxtronik_data(self, lux): self.clear_data_vector(lux.visibilities) def clear_data_vector(self, data_vector): - for idx, entry in data_vector: + for idx, entry in data_vector.items(): entry.raw = 0 def test_luxtronik_socket_interface(self): @@ -96,21 +90,29 @@ def test_luxtronik_socket_interface(self): # Finally, writing p = Parameters() - p.queue = {0: 100, 1: 200} + p[0].raw = 100 + p[0].write_pending = True + p[1].raw = 200 + p[1].write_pending = True lux.write(p) s = FakeSocket.last_instance assert s.written_values[0] == 100 assert s.written_values[1] == 200 - assert len(p.queue) == 0 + assert not p[0].write_pending + assert not p[1].write_pending p = Parameters() - p.queue = {2: 300, 3: "test"} + p[2].raw = 300 + p[2].write_pending = True + p[3].raw = "test" + p[3].write_pending = True d = lux.write_and_read(p) s = FakeSocket.last_instance assert s.written_values[2] == 300 # Make sure that the non-int value is not written: assert 3 not in s.written_values - assert len(p.queue) == 0 + assert not p[2].write_pending + assert not p[3].write_pending assert self.check_luxtronik_data(d) def test_luxtronik(self): @@ -141,13 +143,15 @@ def test_luxtronik(self): ########################## # Test the write routine # ########################## - lux.parameters.queue = {0: 500} + lux.parameters[0].raw = 500 + lux.parameters[0].write_pending = True lux.write() s = FakeSocket.last_instance assert s.written_values[0] == 500 p = Parameters() - p.queue = {1: 501} + p[1].raw = 501 + p[1].write_pending = True lux.write(p) s = FakeSocket.last_instance assert s.written_values[1] == 501 @@ -158,7 +162,8 @@ def test_luxtronik(self): ################################### # Test the write_and_read routine # ################################### - lux.parameters.queue = {2: 502} + lux.parameters[2].raw = 502 + lux.parameters[2].write_pending = True lux.write_and_read() # Currently write_and_read triggers two separate connections/operations s = FakeSocket.prev_instance @@ -169,7 +174,8 @@ def test_luxtronik(self): self.clear_luxtronik_data(lux) - p.queue = {3: 503} + p[3].raw = 503 + p[3].write_pending = True lux.write_and_read(p) # Currently write_and_read triggers two separate connections/operations s = FakeSocket.prev_instance From a306c103cbc4b43a80e58949e03ef0e560d56e91 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Tue, 20 Jan 2026 21:09:01 +0100 Subject: [PATCH 02/61] wip --- luxtronik/data_vector.py | 1 + tests/shi/test_shi_contiguous.py | 8 +- tests/shi/test_shi_definitions.py | 79 +++++++++++++-- tests/test_compatibility.py | 8 +- tests/test_data_vector.py | 163 +++++++++++++++++++++++++++++- 5 files changed, 244 insertions(+), 15 deletions(-) diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index 902a3167..248ab5f4 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -101,6 +101,7 @@ def integrate_data(definition, field, raw_data, data_offset=-1): if field.concatenate_multiple_data_chunks and raw is not None: # Usually big-endian (reverse=True) is used raw = pack_values(raw, definition.reg_bits) + raw = raw if definition.check_raw_not_none(raw) else None field.raw = raw diff --git a/tests/shi/test_shi_contiguous.py b/tests/shi/test_shi_contiguous.py index 888ff675..f74f5bf1 100644 --- a/tests/shi/test_shi_contiguous.py +++ b/tests/shi/test_shi_contiguous.py @@ -19,26 +19,32 @@ def_a1 = LuxtronikDefinition({ 'index': 1, 'count': 1, + 'datatype': 'INT16', }, 'test', 100) def_a = LuxtronikDefinition({ 'index': 1, 'count': 2, + 'datatype': 'INT16', }, 'test', 100) def_b = LuxtronikDefinition({ 'index': 3, 'count': 1, + 'datatype': 'INT16', }, 'test', 100) def_c = LuxtronikDefinition({ 'index': 4, 'count': 3, + 'datatype': 'INT16', }, 'test', 100) def_c1 = LuxtronikDefinition({ 'index': 4, 'count': 1, + 'datatype': 'INT16', }, 'test', 100) def_c2 = LuxtronikDefinition({ 'index': 5, 'count': 1, + 'datatype': 'INT16', }, 'test', 100) defs = [] @@ -99,7 +105,7 @@ def test_integrate_data(self): assert part.field.raw is None part.integrate_data([1, 5, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, 9], 1) - assert part.field.raw is None + assert part.field.raw == [5, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] part = ContiguousDataPart(def_c1, field_c1) diff --git a/tests/shi/test_shi_definitions.py b/tests/shi/test_shi_definitions.py index d4297420..592fca6f 100644 --- a/tests/shi/test_shi_definitions.py +++ b/tests/shi/test_shi_definitions.py @@ -21,28 +21,24 @@ def test_data_arr(self): field.raw = 5 arr = get_data_arr(definition, field) assert arr == [5] - assert check_data(definition, field) # get from array definition._count = 2 field.raw = [7, 3] arr = get_data_arr(definition, field) assert arr == [7, 3] - assert check_data(definition, field) # too much data definition._count = 2 field.raw = [4, 8, 1] arr = get_data_arr(definition, field) assert arr is None - assert not check_data(definition, field) # insufficient data definition._count = 2 field.raw = [9] arr = get_data_arr(definition, field) assert arr is None - assert not check_data(definition, field) field.concatenate_multiple_data_chunks = True @@ -51,21 +47,18 @@ def test_data_arr(self): 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) @@ -76,6 +69,8 @@ def test_integrate(self): # set array definition._count = 2 + definition._num_bits = 64 + definition._data_type = 'INT64' integrate_data(definition, field, data) assert field.raw == [3, 4] integrate_data(definition, field, data, 4) @@ -83,10 +78,70 @@ def test_integrate(self): integrate_data(definition, field, data, 7) assert field.raw is None integrate_data(definition, field, data, 0) + assert field.raw == [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] + + # set value + definition._count = 1 + definition._num_bits = 32 + definition._data_type = 'INT32' + integrate_data(definition, field, data) + assert field.raw == 3 + integrate_data(definition, field, data, 5) + assert field.raw == 6 + integrate_data(definition, field, data, 9) + assert field.raw is None + integrate_data(definition, field, data, 1) + # Currently there is no magic "not available" value for 32 bit values -> not None + # This applies also to similar lines below + assert field.raw == LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE + + field.concatenate_multiple_data_chunks = True + + # set array + definition._count = 2 + definition._num_bits = 64 + definition._data_type = 'INT64' + integrate_data(definition, field, data) + assert field.raw == 0x00000003_00000004 + integrate_data(definition, field, data, 4) + assert field.raw == 0x00000005_00000006 + integrate_data(definition, field, data, 7) assert field.raw is None + integrate_data(definition, field, data, 0) + assert field.raw == 0x00000001_00007FFF # set value definition._count = 1 + definition._num_bits = 32 + definition._data_type = 'INT32' + integrate_data(definition, field, data) + assert field.raw == 0x00000003 + integrate_data(definition, field, data, 5) + assert field.raw == 0x00000006 + integrate_data(definition, field, data, 9) + assert field.raw is None + integrate_data(definition, field, data, 1) + assert field.raw == 0x00007FFF + + field.concatenate_multiple_data_chunks = False + + # set array + definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' + integrate_data(definition, field, data) + assert field.raw == [3, 4] + integrate_data(definition, field, data, 4) + assert field.raw == [5, 6] + integrate_data(definition, field, data, 7) + assert field.raw is None + integrate_data(definition, field, data, 0) + assert field.raw == [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] + + # set value + definition._count = 1 + definition._num_bits = 16 + definition._data_type = 'INT16' integrate_data(definition, field, data) assert field.raw == 3 integrate_data(definition, field, data, 5) @@ -100,6 +155,8 @@ def test_integrate(self): # set array definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' integrate_data(definition, field, data) assert field.raw == 0x0003_0004 integrate_data(definition, field, data, 4) @@ -107,10 +164,12 @@ def test_integrate(self): integrate_data(definition, field, data, 7) assert field.raw is None integrate_data(definition, field, data, 0) - assert field.raw is None + assert field.raw == 0x0001_7FFF # set value definition._count = 1 + definition._num_bits = 16 + definition._data_type = 'INT16' integrate_data(definition, field, data) assert field.raw == 0x0003 integrate_data(definition, field, data, 5) @@ -118,4 +177,6 @@ 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 \ No newline at end of file + assert field.raw is None + + field.concatenate_multiple_data_chunks = False diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index a6eeb019..49e58b15 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -1915,8 +1915,12 @@ def test_compatibilities(self): print_caption = True for old_name, old_idx in mapping.items(): def_by_name = obj.definitions.get(old_name) - def_by_idx = obj.definitions.get(old_idx) - if (def_by_name.index != old_idx) or (def_by_idx.name != def_by_name.name): + field_by_name = obj.get(old_name) + if (def_by_name is None) \ + or (field_by_name is None) \ + or (def_by_name.index != old_idx) \ + or (def_by_name.name != field_by_name.name) \ + or (old_name not in def_by_name.names): # We do not use assert here, in order to catch all incompatibilities at once. if print_caption: print(f"### Incompatibilities - {caption}:") diff --git a/tests/test_data_vector.py b/tests/test_data_vector.py index 9a13632e..e88e0bd8 100644 --- a/tests/test_data_vector.py +++ b/tests/test_data_vector.py @@ -3,9 +3,166 @@ # pylint: disable=too-few-public-methods,invalid-name,too-many-lines import pytest +from luxtronik.definitions import LuxtronikDefinition, LuxtronikDefinitionsDictionary +from luxtronik.data_vector import LuxtronikFieldsDictionary, DataVector -from luxtronik.data_vector import DataVector -from luxtronik.datatypes import Base +from luxtronik.datatypes import ( + Base, + Unknown, +) + + +class TestLuxtronikFieldsDictionary: + + def test_init(self): + d = LuxtronikFieldsDictionary() + + assert type(d._def_lookup) is LuxtronikDefinitionsDictionary + assert type(d._field_lookup) is dict + assert len(d._field_lookup.values()) == 0 + assert type(d._pairs) is list + assert len(d._pairs) == 0 + + def test_add(self): + d = LuxtronikFieldsDictionary() + assert len(d) == 0 + assert len(d._pairs) == 0 + + u = LuxtronikDefinition.unknown(1, "test", 0) + f = u.create_field() + d.add(u, f) + assert len(d) == 1 + assert len(d._pairs) == 1 + assert d._pairs[0].definition is u + assert d._pairs[0].field is f + + u = LuxtronikDefinition.unknown(2, "test", 0) + f = u.create_field() + d.add(u, f) + assert len(d) == 2 + assert len(d._pairs) == 2 + assert d._pairs[1].definition is u + assert d._pairs[1].field is f + + u = LuxtronikDefinition.unknown(0, "test", 0) + f = u.create_field() + d.add_sorted(u, f) + assert len(d) == 3 + assert len(d._pairs) == 3 + assert d._pairs[0].definition is u + assert d._pairs[0].field is f + + def create_instance(self): + d = LuxtronikFieldsDictionary() + u = LuxtronikDefinition.unknown(1, "test", 0) + d.add(u, u.create_field()) + u = LuxtronikDefinition.unknown(2, "test", 0) + d.add(u, u.create_field()) + b = LuxtronikDefinition({ + "index": 2, + "type": Base, + "names": ["base2"], + }, "test", 0) + f = b.create_field() + d.add(b, f) + b = LuxtronikDefinition({ + "index": 3, + "type": Base, + "names": ["base3"], + }, "test", 0) + d.add(b, b.create_field(), "base4") + return d, u, f + + def test_len(self): + d, _, _ = self.create_instance() + # 3 different indices + assert len(d) == 3 + assert len(d._pairs) == 4 + + def test_get_contains(self): + d, u, f = self.create_instance() + assert "1" in d + assert d["1"].name == "unknown_test_1" + assert "unknown_test_1" in d + assert d["unknown_test_1"].name == "unknown_test_1" + assert 2 in d + assert d[2].name == "base2" + assert "unknown_test_2" in d + assert d["unknown_test_2"].name == "unknown_test_2" + assert "base2" in d + assert d["base2"].name == "base2" + assert "base3" in d + assert d.get("base3").name == "base3" + assert "base4" in d + assert d.get("base4").name == "base3" + assert u in d + assert d[u].name == "unknown_test_2" + assert f in d + assert d[f].name == "base2" + assert 4 not in d + + def test_iter(self): + d, _, _ = self.create_instance() + for idx, key in enumerate(d): + if idx == 0: + assert key == 1 + if idx == 1: + assert key == 2 + if idx == 2: + assert key == 3 + + def test_values(self): + d, _, _ = self.create_instance() + for idx, value in enumerate(d.values()): + if idx == 0: + assert type(value) is Unknown + assert value.name == "unknown_test_1" + if idx == 1: + assert type(value) is Base + assert value.name == "base2" + if idx == 2: + assert type(value) is Base + assert value.name == "base3" + + def test_pairs(self): + d, _, _ = self.create_instance() + for idx, (key, value) in enumerate(d.items()): + if idx == 0: + assert key == 1 + assert type(value) is Unknown + assert value.name == "unknown_test_1" + if idx == 1: + assert key == 2 + assert type(value) is Base + assert value.name == "base2" + if idx == 2: + assert key == 3 + assert type(value) is Base + assert value.name == "base3" + + class MyTestClass: + pass + + def test_alias(self): + d, u, f = self.create_instance() + my = self.MyTestClass() + + d.register_alias(0, "abc") + assert d["abc"] is d[0] + + field = d.register_alias("unknown_test_1", 6) + assert d[6] is field + + field = d.register_alias(u, my) + assert d[my] is d[u] + + d.register_alias(f, my) + assert d[my] is not d[u] + assert d[my] is d[f] + + field = d.register_alias(9, my) + assert field is None + assert d[my] is d[f] class ObsoleteDataVector(DataVector): @@ -28,7 +185,7 @@ class TestDataVector: @pytest.mark.parametrize("name, exception_expected", [ ("foo", False), - ("bar", True), + ("bar", False), ("baz", True), ("qux", False), ]) From 577e3d9c8353df4f173af7e236522c3bb3aa4ae0 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sun, 11 Jan 2026 20:12:48 +0100 Subject: [PATCH 03/61] wip --- tests/shi/test_shi_vector.py | 41 +++++++++++++++--------------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/tests/shi/test_shi_vector.py b/tests/shi/test_shi_vector.py index a232c32d..8a4b734a 100644 --- a/tests/shi/test_shi_vector.py +++ b/tests/shi/test_shi_vector.py @@ -91,8 +91,8 @@ def test_create(self): # create versioned data vector data_vector = DataVectorTest(parse_version("1.2")) assert data_vector.version == (1, 2, 0, 0) - assert len(data_vector) == 3 - assert len(data_vector._data.pairs()) == 3 + assert len(data_vector) == 2 + assert len(data_vector._data._items) == 3 assert not data_vector._read_blocks_up_to_date assert len(data_vector._read_blocks) == 0 @@ -215,8 +215,8 @@ def test_add(self): def_9a = data_vector.definitions['field_9a'] field = data_vector.add(def_9a) assert def_9a in data_vector - assert len(data_vector) == 3 - assert len(data_vector.data._pairs) == 3 + assert len(data_vector) == 2 + assert len(data_vector.data._items) == 3 assert field.name == 'field_9a' # Get via index (last added) @@ -249,38 +249,31 @@ def test_iter(self): data_vector.add(5) data_vector.add(9) - for index, definition in enumerate(data_vector): + for index, idx in enumerate(data_vector): if index == 0: - assert definition.index == 5 - assert definition.name == 'field_5' + assert idx == 5 if index == 1: - assert definition.index == 9 - assert definition.name == 'field_9a' + assert idx == 9 # field_9 if index == 2: - assert definition.index == 9 - assert definition.name == 'field_9' + assert False for index, field in enumerate(data_vector.values()): if index == 0: assert field.name == 'field_5' if index == 1: - assert field.name == 'field_9a' - if index == 2: assert field.name == 'field_9' + if index == 2: + assert False - for index, (definition, field) in enumerate(data_vector.items()): + for index, (idx, field) in enumerate(data_vector.items()): if index == 0: - assert definition.index == 5 - assert definition.name == 'field_5' + assert idx == 5 assert field.name == 'field_5' if index == 1: - assert definition.index == 9 - assert definition.name == 'field_9a' - assert field.name == 'field_9a' - if index == 2: - assert definition.index == 9 - assert definition.name == 'field_9' + assert idx == 9 assert field.name == 'field_9' + if index == 2: + assert False def test_set(self): data_vector = DataVectorTest(parse_version("1.1.2")) @@ -405,8 +398,8 @@ def test_version_none(self): data_vector.add("field_invalid") assert len(data_vector) == 3 data_vector.add(10) # field_9a alias - assert len(data_vector) == 4 - assert len(data_vector._data._pairs) == 4 + assert len(data_vector) == 3 + assert len(data_vector._data._items) == 4 class TestHoldings: From 2e2f3f86084cd471cf21003d33ba5b90c0659116 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Tue, 20 Jan 2026 22:20:09 +0100 Subject: [PATCH 04/61] wip --- luxtronik/data_vector.py | 14 ++++++----- luxtronik/datatypes.py | 41 +++++++++++++++++++++++++------ luxtronik/definitions/__init__.py | 30 +++++++++++++++------- luxtronik/shi/holdings.py | 2 +- tests/cfi/test_cfi_parameters.py | 34 ++++++++++--------------- tests/shi/test_shi_vector.py | 6 ++--- tests/test_definition_list.py | 2 -- 7 files changed, 80 insertions(+), 49 deletions(-) diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index 248ab5f4..e7574528 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -8,7 +8,7 @@ LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, ) -from luxtronik.datatypes import Base +from luxtronik.datatypes import Base, Unknown from luxtronik.definitions import LuxtronikDefinition, LuxtronikDefinitionsDictionary @@ -98,7 +98,9 @@ def integrate_data(definition, field, raw_data, data_offset=-1): else: raw = raw_data[data_offset : data_offset + definition.count] raw = raw if len(raw) == definition.count else None - if field.concatenate_multiple_data_chunks and raw is not None: + should_pack = field.concatenate_multiple_data_chunks \ + and definition.reg_bits > 0 # and definition.count > 1 + if should_pack and raw is not None : # Usually big-endian (reverse=True) is used raw = pack_values(raw, definition.reg_bits) @@ -120,8 +122,9 @@ def get_data_arr(definition, field): data = field.raw if data is None: return None - if not isinstance(data, list) and definition.count > 1 \ - and field.concatenate_multiple_data_chunks: + should_unpack = field.concatenate_multiple_data_chunks \ + and definition.reg_bits > 0 and definition.count > 1 + if should_unpack and not isinstance(data, list): # Usually big-endian (reverse=True) is used data = unpack_values(data, definition.count, definition.reg_bits) if not isinstance(data, list): @@ -500,11 +503,10 @@ def parse(self, raw_data): for index in range(definition.index, next_idx): undefined.discard(index) integrate_data(definition, field, raw_data) - field.write_pending = False # create an unknown field for additional data for index in undefined: # self.logger.warning(f"Entry '%d' not in list of {self.name}", index) - definition = LuxtronikDefinition.unknown(index, self.name, self.definitions.offset) + definition = self.definitions.create_unknown_definition(index) field = definition.create_field() field.raw = raw_data[index] self._data.add_sorted(definition, field) diff --git a/luxtronik/datatypes.py b/luxtronik/datatypes.py index f8431c4b..7f34e169 100755 --- a/luxtronik/datatypes.py +++ b/luxtronik/datatypes.py @@ -44,11 +44,15 @@ def __init__(self, names, writeable=False): @classmethod def to_heatpump(cls, value): """Converts value into heatpump units.""" + if not isinstance(value, int): + return None return value @classmethod def from_heatpump(cls, value): """Converts value from heatpump units.""" + if not isinstance(value, int): + return None return value @classproperty @@ -165,7 +169,7 @@ def sanitize_option(cls, option): @classmethod def from_heatpump(cls, value): - if value is None: + if not isinstance(value, int): return None if value in cls.codes: return cls.codes.get(value) @@ -319,11 +323,16 @@ class Bool(Base): @classmethod def from_heatpump(cls, value): + if not isinstance(value, int): + return None return bool(value) @classmethod def to_heatpump(cls, value): - return int(value) + try: + return int(bool(value)) + except Exception: + return None class Frequency(Base): @@ -347,10 +356,14 @@ class IPv4Address(Base): @classmethod def from_heatpump(cls, value): + if not isinstance(value, int): + return None return socket.inet_ntoa(struct.pack(">i", value)) @classmethod def to_heatpump(cls, value): + if not isinstance(value, str): + return None return struct.unpack(">i", socket.inet_aton(value))[0] @@ -361,7 +374,7 @@ class Timestamp(Base): @classmethod def from_heatpump(cls, value): - if value is None: + if not isinstance(value, int): return None if value <= 0: return datetime.datetime.fromtimestamp(0) @@ -369,6 +382,8 @@ def from_heatpump(cls, value): @classmethod def to_heatpump(cls, value): + if not isinstance(value, float): + return None return datetime.datetime.timestamp(value) @@ -560,12 +575,14 @@ class Hours2(Base): @classmethod def from_heatpump(cls, value): - if value is None: + if not isinstance(value, int): return None return 1 + value / 2 @classmethod def to_heatpump(cls, value): + if not isinstance(value, int): + return None return round((value - 1) * 2) @@ -602,6 +619,8 @@ class Character(Base): @classmethod def from_heatpump(cls, value): + if not isinstance(value, int): + return None if value == 0: return "" return chr(value) @@ -614,6 +633,8 @@ class MajorMinorVersion(Base): @classmethod def from_heatpump(cls, value): + if not isinstance(value, int): + return None if value > 0: major = value // 100 minor = value % 100 @@ -932,7 +953,7 @@ class TimeOfDay(Base): @classmethod def from_heatpump(cls, value): - if value is None: + if not isinstance(value, int): return None hours = value // 3600 minutes = (value // 60) % 60 @@ -942,6 +963,8 @@ def from_heatpump(cls, value): @classmethod def to_heatpump(cls, value): + if not isinstance(value, str): + return None d = [int(v) for v in value.split(":")] val = d[0] * 3600 + d[1] * 60 @@ -958,7 +981,7 @@ class TimeOfDay2(Base): @classmethod def from_heatpump(cls, value): - if value is None: + if not isinstance(value, int): return None value_low = value & 0xFFFF @@ -972,6 +995,8 @@ def from_heatpump(cls, value): @classmethod def to_heatpump(cls, value): + if not isinstance(value, str): + return None d = value.split("-") low = [int(v) for v in d[0].split(":")] high = [int(v) for v in d[1].split(":")] @@ -1087,7 +1112,9 @@ class FullVersion(Base): @classmethod def from_heatpump(cls, value): - if isinstance(value, list) and len(value) >= 3: + if not isinstance(value, list): + return None + if len(value) >= 3: return f"{value[0]}.{value[1]}.{value[2]}" else: return "0" diff --git a/luxtronik/definitions/__init__.py b/luxtronik/definitions/__init__.py index c5b1924f..d92090c4 100644 --- a/luxtronik/definitions/__init__.py +++ b/luxtronik/definitions/__init__.py @@ -38,10 +38,15 @@ class LuxtronikDefinition: "names": [], "since": "", "until": "", - "datatype": "UINT32", + "datatype": "", "description": "", } + # It is permissible not to specify a data type. + # In this case, all functions based on it will be disabled. + VALID_DATA_TYPES = ("", "UINT16", "UINT32", "UINT64", "INT16", "INT32", "INT64") + + def __init__(self, data_dict, type_name, offset): """ Initialize a definition from a data-dictionary. @@ -83,14 +88,18 @@ def __init__(self, data_dict, type_name, offset): self._offset = int(offset) self._addr = self._offset + self._index self._data_type = data_dict["datatype"] - self._num_bits = int(self._data_type.replace('U', '').replace('INT', '')) + data_type_valid = self._data_type in self.VALID_DATA_TYPES + self._valid &= data_type_valid + data_type_valid &= self._data_type != "" + self._num_bits = int(self._data_type.replace('U', '').replace('INT', '')) \ + if data_type_valid else 0 except Exception as e: self._valid = False self._index = 0 LOGGER.error(f"Failed to create LuxtronikDefinition: '{e}' with {data_dict}") @classmethod - def unknown(cls, index, type_name, offset): + def unknown(cls, index, type_name, offset, data_type=""): """ Create an "unknown" definition. @@ -98,13 +107,15 @@ def unknown(cls, index, type_name, offset): index (int): The register index of the "unknown" definition. type_name (str): The type name e.g. 'holding', 'input', ... . offset (str): Offset of the address from the specified index. + data_type (str): Data type of the field (see VALID_DATA_TYPES). Returns: LuxtronikDefinition: A definition marked as unknown. """ return cls({ "index": index, - "names": [f"unknown_{type_name.lower()}_{index}"] + "names": [f"unknown_{type_name.lower()}_{index}"], + "datatype": data_type, }, type_name, offset) def __bool__(self): @@ -416,16 +427,17 @@ class LuxtronikDefinitionsList: (locally = only valid for that dictionary). """ - def _init_instance(self, name, offset, version): + def _init_instance(self, name, offset, default_data_type, version): """Re-usable method to initialize all instance variables.""" self._name = name self._offset = offset + self._default_data_type = default_data_type self._version = version # sorted list of all definitions self._definitions = [] self._lookup = LuxtronikDefinitionsDictionary() - def __init__(self, definitions_list, name, offset): + def __init__(self, definitions_list, name, offset, default_data_type): """ Initialize the (by index sorted) definitions list. @@ -442,7 +454,7 @@ def __init__(self, definitions_list, name, offset): - The value of count must always be greater than or equal to 1 - All names should be unique """ - self._init_instance(name, offset, None) + self._init_instance(name, offset, default_data_type, None) # Add definition objects only for valid items. # The correct sorting has already been ensured by the pytest @@ -464,7 +476,7 @@ def filtered(cls, definitions, version): If None is passed, all available fields are added. """ obj = cls.__new__(cls) # this don't call __init__() - obj._init_instance(definitions.name, definitions.offset, version) + obj._init_instance(definitions.name, definitions.offset, definitions._default_data_type, version) for d in definitions: if d.valid and version_in_range(obj._version, d.since, d.until): @@ -498,7 +510,7 @@ def create_unknown_definition(self, index): Returns: LuxtronikDefinition: A definition marked as unknown. """ - return LuxtronikDefinition.unknown(index, self._name, self._offset) + return LuxtronikDefinition.unknown(index, self._name, self._offset, self._default_data_type) def register_alias(self, def_name_or_idx, alias): """ diff --git a/luxtronik/shi/holdings.py b/luxtronik/shi/holdings.py index 1df95e68..53f6e0d4 100644 --- a/luxtronik/shi/holdings.py +++ b/luxtronik/shi/holdings.py @@ -7,7 +7,7 @@ from luxtronik.definitions.holdings import ( HOLDINGS_DEFINITIONS_LIST, HOLDINGS_OFFSET, - HOLDINGS_DEFAULT_DATA_TYPE, + HOLDINGS_DEFAULT_DATA_TYPE ) from luxtronik.shi.constants import HOLDINGS_FIELD_NAME diff --git a/tests/cfi/test_cfi_parameters.py b/tests/cfi/test_cfi_parameters.py index dc2f55c9..9a033f24 100644 --- a/tests/cfi/test_cfi_parameters.py +++ b/tests/cfi/test_cfi_parameters.py @@ -43,26 +43,18 @@ def test_get(self): assert parameters.get("0").name == s assert parameters.get(s).name == s - def test__lookup(self): - """Test cases for _lookup""" - parameters = Parameters() - s = "ID_Transfert_LuxNet" - assert parameters._lookup(0).name == s - assert parameters._lookup("0").name == s - assert parameters._lookup(s).name == s - - p0 = parameters._lookup(0) - assert parameters._lookup(0, True) == (0, p0) - assert parameters._lookup("0", True) == (0, p0) - assert parameters._lookup(s, True) == (0, p0) + p0 = parameters.get(0) + assert parameters[0] is p0 + assert parameters["0"] is p0 + assert parameters[s] is p0 # Look for a name which does not exist s = "ID_BarFoo" - assert parameters._lookup(s, True)[0] is None + assert parameters.get(s) is None # Look for something which is not an int and not a string j = 0.0 - assert parameters._lookup(j) is None + assert parameters.get(j) is None def test___iter__(self): """Test cases for __iter__""" @@ -82,21 +74,21 @@ def test_set(self): # Set something which does not exist parameters.set("BarFoo", 0) - assert paramters["BarFoo"] is not None + assert parameters["BarFoo"] is None # Set something which was previously not allowed to be set parameters.set("ID_Transfert_LuxNet", 1) - assert paramters["ID_Transfert_LuxNet"].raw == 1 - assert paramters["ID_Transfert_LuxNet"].write_pending + assert parameters["ID_Transfert_LuxNet"].raw == 1 + assert parameters["ID_Transfert_LuxNet"].write_pending # Set something which is allowed to be set parameters.set("ID_Einst_WK_akt", 2) - assert paramters["ID_Einst_WK_akt"].raw == 2 - assert paramters["ID_Einst_WK_akt"].write_pending + assert parameters["ID_Einst_WK_akt"].raw == 20 + assert parameters["ID_Einst_WK_akt"].write_pending parameters = Parameters(safe=False) # Set something which was previously not allowed to be set, but we are brave. parameters.set("ID_Transfert_LuxNet", 4) - assert paramters["ID_Transfert_LuxNet"].raw == 4 - assert paramters["ID_Transfert_LuxNet"].write_pending + assert parameters["ID_Transfert_LuxNet"].raw == 4 + assert parameters["ID_Transfert_LuxNet"].write_pending diff --git a/tests/shi/test_shi_vector.py b/tests/shi/test_shi_vector.py index 8a4b734a..b1bd5af3 100644 --- a/tests/shi/test_shi_vector.py +++ b/tests/shi/test_shi_vector.py @@ -92,7 +92,7 @@ def test_create(self): data_vector = DataVectorTest(parse_version("1.2")) assert data_vector.version == (1, 2, 0, 0) assert len(data_vector) == 2 - assert len(data_vector._data._items) == 3 + assert len(data_vector._data._pairs) == 3 assert not data_vector._read_blocks_up_to_date assert len(data_vector._read_blocks) == 0 @@ -216,7 +216,7 @@ def test_add(self): field = data_vector.add(def_9a) assert def_9a in data_vector assert len(data_vector) == 2 - assert len(data_vector.data._items) == 3 + assert len(data_vector.data._pairs) == 3 assert field.name == 'field_9a' # Get via index (last added) @@ -399,7 +399,7 @@ def test_version_none(self): assert len(data_vector) == 3 data_vector.add(10) # field_9a alias assert len(data_vector) == 3 - assert len(data_vector._data._items) == 4 + assert len(data_vector._data._pairs) == 4 class TestHoldings: diff --git a/tests/test_definition_list.py b/tests/test_definition_list.py index 67fff156..b0cfef34 100644 --- a/tests/test_definition_list.py +++ b/tests/test_definition_list.py @@ -24,8 +24,6 @@ KEY_UNTIL = "until" KEY_DESC = "description" -VALID_DATA_TYPES = ("UINT16", "UINT32", "INT16", "INT32") - class RunTestDefinitionList: From 48a736651e4b53d26282eebcf9079c7747939758 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Wed, 21 Jan 2026 20:26:17 +0100 Subject: [PATCH 05/61] wip --- luxtronik/cfi/parameters.py | 1 - luxtronik/cfi/vector.py | 56 ++++++ luxtronik/cfi/visibilities.py | 5 - luxtronik/data_vector.py | 350 ++-------------------------------- luxtronik/datatypes.py | 4 - luxtronik/shi/contiguous.py | 2 +- luxtronik/shi/interface.py | 2 +- luxtronik/shi/vector.py | 9 +- 8 files changed, 76 insertions(+), 353 deletions(-) create mode 100644 luxtronik/cfi/vector.py diff --git a/luxtronik/cfi/parameters.py b/luxtronik/cfi/parameters.py index f0976028..d3906369 100644 --- a/luxtronik/cfi/parameters.py +++ b/luxtronik/cfi/parameters.py @@ -26,7 +26,6 @@ class Parameters(DataVectorConfig): """Class that holds all parameters.""" - logger = LOGGER name = PARAMETERS_FIELD_NAME definitions = PARAMETERS_DEFINITIONS diff --git a/luxtronik/cfi/vector.py b/luxtronik/cfi/vector.py new file mode 100644 index 00000000..4f8b5206 --- /dev/null +++ b/luxtronik/cfi/vector.py @@ -0,0 +1,56 @@ + +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__(self, safe=True): + """Initialize config interface data-vector class.""" + super()._init_instance(safe) + + # Add all available fields + for d in self.definitions: + self._data.add(d, d.create_field()) + + def add(self, def_field_name_or_idx, alias=None): + """ + Adds an additional field to this data vector. + + 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 \ No newline at end of file diff --git a/luxtronik/cfi/visibilities.py b/luxtronik/cfi/visibilities.py index ec87c3bf..99ceb3e2 100644 --- a/luxtronik/cfi/visibilities.py +++ b/luxtronik/cfi/visibilities.py @@ -29,11 +29,6 @@ class Visibilities(DataVectorConfig): 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 diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index e7574528..2a057e33 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -7,6 +7,7 @@ LUXTRONIK_NAME_CHECK_OBSOLETE, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, ) +from luxtronik.collections import LuxtronikFieldsDictionary from luxtronik.datatypes import Base, Unknown from luxtronik.definitions import LuxtronikDefinition, LuxtronikDefinitionsDictionary @@ -15,334 +16,6 @@ LOGGER = logging.getLogger(__name__) -############################################################################### -# Common functions -############################################################################### - -def pack_values(values, num_bits, reverse=True): - """ - Packs a list of data chunks into one integer. - - Args: - values (list[int]): raw data; distributed across multiple registers. - num_bits (int): Number of bits per chunk. - 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) - mask = (1 << num_bits) - 1 - - 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 & mask) << (num_bits * bit_index) - - return result - -def unpack_values(packed, count, num_bits, reverse=True): - """ - Unpacks 'count' chunks from a packed integer. - - Args: - packed (int): Packed raw data as a single integer value. - count (int): Number of chunks to unpack. - num_bits (int): Number of bits per chunk. - 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 = [] - mask = (1 << num_bits) - 1 - - 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 >> (num_bits * bit_index)) & mask - values.append(chunk) - - return values - -def integrate_data(definition, field, raw_data, data_offset=-1): - """ - Integrate raw values from a data array into the field. - - Args: - definition (LuxtronikDefinition): Meta-data of the field. - field (Base): Field object where to integrate the data. - raw_data (list): Source array of bytes/words. - data_offset (int): Optional offset. Defaults to `definition.index`. - """ - # Use data_offset if provided, otherwise the index - data_offset = data_offset if data_offset >= 0 else definition.index - # Use the information of the definition to extract the raw-value - if (data_offset + definition.count - 1) >= len(raw_data): - raw = None - elif definition.count == 1: - raw = raw_data[data_offset] - else: - raw = raw_data[data_offset : data_offset + definition.count] - raw = raw if len(raw) == definition.count else None - should_pack = field.concatenate_multiple_data_chunks \ - and definition.reg_bits > 0 # and definition.count > 1 - if should_pack and raw is not None : - # Usually big-endian (reverse=True) is used - raw = pack_values(raw, definition.reg_bits) - - raw = raw if definition.check_raw_not_none(raw) else None - field.raw = raw - -def get_data_arr(definition, field): - """ - Normalize the field's data to a list of the correct size. - - Args: - definition (LuxtronikDefinition): Meta-data of the field. - field (Base): Field object that contains data to get. - - Returns: - list[int] | None: List of length `definition.count`, - or None if the data size does not match. - """ - data = field.raw - if data is None: - return None - should_unpack = field.concatenate_multiple_data_chunks \ - and definition.reg_bits > 0 and definition.count > 1 - if should_unpack and not isinstance(data, list): - # Usually big-endian (reverse=True) is used - data = unpack_values(data, definition.count, definition.reg_bits) - if not isinstance(data, list): - data = [data] - return data if len(data) == definition.count else None - -############################################################################### -# Definition / field pair -############################################################################### - -class LuxtronikDefFieldPair: - """ - Combines a definition and a field into a single iterable object. - """ - - def __init__(self, definition, field): - """ - Initialize a definition-field-pair. - - Args: - field (Base): The field object. - definition (LuxtronikDefinition): The definition for this field. - """ - self.field = field - self.definition = definition - - def __iter__(self): - yield self.definition - yield self.field - - @property - def index(self): - return self.definition.index - - @property - def addr(self): - return self.definition.addr - - @property - def count(self): - return self.definition.count - - def get_data_arr(self): - """ - Normalize the field's data to a list of the correct size. - - Returns: - list[int] | None: List of length `definition.count`, or None if insufficient. - """ - return get_data_arr(self.definition, self.field) - - def integrate_data(self, raw_data, data_offset=-1): - """ - Integrate the related parts of the `raw_data` into the field - - Args: - raw_data (list): Source array of bytes/words. - data_offset (int): Optional offset. Defaults to `definition.index`. - """ - integrate_data(self.definition, self.field, raw_data, data_offset) - -############################################################################### -# Field dictionary for data vectors -############################################################################### - -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. - Aliases are also supported. - """ - - def __init__(self): - # There may be several names or alias that points to one definition. - # So in order to spare memory we split the name/index-to-field-lookup - # into a name/index-to-definition-lookup and a definition-to-field-lookup - self._def_lookup = LuxtronikDefinitionsDictionary() - self._field_lookup = {} - # Furthermore stores the definition-to-field-lookup separate from the - # field-definition pairs to keep the index-sorted order when adding new entries - self._pairs = [] # list of LuxtronikDefFieldPair - - def __getitem__(self, def_field_name_or_idx): - return self.get(def_field_name_or_idx) - - def __setitem__(self, def_name_or_idx, value): - assert False, "__setitem__ not implemented." - - def __len__(self): - return len(self._def_lookup._index_dict) - - 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]) - - def __contains__(self, def_field_name_or_idx): - """ - Check whether the data vector contains a name, index, - or definition matching an added field, or the field itself. - - Args: - def_field_name_or_idx (LuxtronikDefinition | Base | str | int): - Definition object, field object, field name or register index. - - Returns: - True if the searched element was found, otherwise False. - """ - if isinstance(def_field_name_or_idx, Base): - return any(def_field_name_or_idx is field for field in self._field_lookup.values()) - elif isinstance(def_field_name_or_idx, LuxtronikDefinition): - # speed-up the look-up by search only the name-dict - return def_field_name_or_idx.name in self._def_lookup._name_dict - else: - 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]) - - 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]) - - def pairs(self): - """ - Return all definition-field-pairs contained herein. - """ - return self._pairs - - @property - def def_dict(self): - """Return the internal definition dictionary""" - return self._def_lookup - - def add(self, definition, field, alias=None): - """ - Add a definition-field-pair to the internal dictionaries. - - Args: - definition (LuxtronikDefinition): Definition related to the field. - field (Base): Field to add. - alias (Hashable | None): Alias, which can be used to access the field again. - """ - if definition.valid: - self._def_lookup.add(definition, alias) - self._field_lookup[definition] = field - self._pairs.append(LuxtronikDefFieldPair(definition, field)) - - def add_sorted(self, definition, field, alias=None): - if definition.valid: - self.add(definition, field, alias) - # sort _pairs by definition.index - self._pairs.sort(key=lambda pair: pair.definition.index) - - def register_alias(self, def_field_name_or_idx, alias): - """ - Add an alternative name (or anything hashable else) - that can be used to access a specific field. - - Args: - def_field_name_or_idx (LuxtronikDefinition | Base | str | int): - Field to which the alias is to be added. - Either by definition, name, register index, or the field itself. - alias (Hashable): Alias, which can be used to access the field again. - - Returns: - Base | None: The field to which the alias was added, - or None if not possible - """ - # Resolve a field input - def_name_or_idx = def_field_name_or_idx - if isinstance(def_name_or_idx, Base): - def_name_or_idx = def_name_or_idx.name - # register alias - definition = self._def_lookup.register_alias(def_name_or_idx, alias) - if definition is None: - return None - return self._field_lookup.get(definition, None) - - def get(self, def_field_name_or_idx, default=None): - """ - Retrieve a field by definition, name or register index. - - Args: - def_field_name_or_idx (LuxtronikDefinition | str | int): - Definition, name, or register index to be used to search for the field. - - Returns: - Base | None: The field found or the provided default if not found. - - Note: - If multiple fields added for the same index/name, - the last added takes precedence. - """ - def_name_or_idx = def_field_name_or_idx - if isinstance(def_name_or_idx, Base): - def_name_or_idx = def_name_or_idx.name - if isinstance(def_name_or_idx, LuxtronikDefinition): - definition = def_name_or_idx - else: - definition = self._def_lookup.get(def_name_or_idx) - if definition is not None: - return self._field_lookup.get(definition, default) - return default - ############################################################################### # Base class for all luxtronik data vectors ############################################################################### @@ -357,7 +30,6 @@ class DataVector: (globally = valid for all newly created vector). """ - logger = LOGGER name = "DataVector" # DataVector specific list of definitions as `LuxtronikDefinitionsList` @@ -422,9 +94,16 @@ class variable `cls.definitions` and is valid for `self.version`). # constructor, magic methods and iterators #################################### + def _init_instance(self, safe): + """Re-usable method to initialize all instance variables.""" + self.safe = safe + + # Dictionary that holds all fields + self._data = LuxtronikFieldsDictionary() + def __init__(self): """Initialize DataVector class.""" - self._data = LuxtronikFieldsDictionary() + self._init_instance(True) @property def data(self): @@ -495,17 +174,18 @@ def parse(self, raw_data): """ raw_len = len(raw_data) undefined = {i for i in range(0, raw_len)} - for definition, field in self._data.pairs(): + for pair in self._data.pairs(): + definition, field = pair next_idx = definition.index + definition.count if next_idx >= raw_len: # not enough registers continue for index in range(definition.index, next_idx): undefined.discard(index) - integrate_data(definition, field, raw_data) + pair.integrate_data(raw_data) # create an unknown field for additional data for index in undefined: - # self.logger.warning(f"Entry '%d' not in list of {self.name}", index) + # LOGGER.warning(f"Entry '%d' not in list of {self.name}", index) definition = self.definitions.create_unknown_definition(index) field = definition.create_field() field.raw = raw_data[index] @@ -564,7 +244,7 @@ def get(self, def_name_or_idx, default=None): raise KeyError(f"The name '{def_name_or_idx}' is obsolete! Use '{obsolete_entry}' instead.") field = self._data.get(def_name_or_idx, default) if field is None: - self.logger.warning(f"entry '{def_name_or_idx}' not found") + LOGGER.warning(f"entry '{def_name_or_idx}' not found") return field def set(self, def_field_name_or_idx, value): @@ -581,7 +261,9 @@ def set(self, def_field_name_or_idx, value): value (int | List[int]): Value to set """ field = def_field_name_or_idx + print(field) if not isinstance(field, Base): field = self.get(def_field_name_or_idx) + print(field) if field is not None: field.value = value \ No newline at end of file diff --git a/luxtronik/datatypes.py b/luxtronik/datatypes.py index 7f34e169..74dc3d16 100755 --- a/luxtronik/datatypes.py +++ b/luxtronik/datatypes.py @@ -44,15 +44,11 @@ def __init__(self, names, writeable=False): @classmethod def to_heatpump(cls, value): """Converts value into heatpump units.""" - if not isinstance(value, int): - return None return value @classmethod def from_heatpump(cls, value): """Converts value from heatpump units.""" - if not isinstance(value, int): - return None return value @classproperty diff --git a/luxtronik/shi/contiguous.py b/luxtronik/shi/contiguous.py index 7c598a82..52ec1f8d 100644 --- a/luxtronik/shi/contiguous.py +++ b/luxtronik/shi/contiguous.py @@ -6,7 +6,7 @@ import logging -from luxtronik.data_vector import LuxtronikDefFieldPair +from luxtronik.collections import LuxtronikDefFieldPair LOGGER = logging.getLogger(__name__) diff --git a/luxtronik/shi/interface.py b/luxtronik/shi/interface.py index b924a84c..66977abc 100644 --- a/luxtronik/shi/interface.py +++ b/luxtronik/shi/interface.py @@ -3,8 +3,8 @@ import logging from luxtronik.common import classproperty, version_in_range +from luxtronik.collections import get_data_arr from luxtronik.datatypes import Base -from luxtronik.data_vector import get_data_arr from luxtronik.definitions import ( LuxtronikDefinition, LuxtronikDefinitionsList, diff --git a/luxtronik/shi/vector.py b/luxtronik/shi/vector.py index 33882480..2b23f298 100644 --- a/luxtronik/shi/vector.py +++ b/luxtronik/shi/vector.py @@ -2,9 +2,7 @@ import logging from luxtronik.common import version_in_range -from luxtronik.data_vector import LuxtronikFieldsDictionary, DataVector -from luxtronik.datatypes import Base, Unknown -from luxtronik.definitions import LuxtronikDefinition +from luxtronik.data_vector import DataVector from luxtronik.shi.constants import LUXTRONIK_LATEST_SHI_VERSION from luxtronik.shi.contiguous import ContiguousDataBlockList @@ -21,12 +19,9 @@ class DataVectorSmartHome(DataVector): def _init_instance(self, version, safe): """Re-usable method to initialize all instance variables.""" - self.safe = safe + super()._init_instance(safe) self._version = version - # Dictionary that holds all fields - self._data = LuxtronikFieldsDictionary() - # Instead of re-create the block-list on every read, we just update it # on first time used or on next time used if some fields are added. self._read_blocks_up_to_date = False From 14adb7f43056c633b7007d17e2659e3ead2e304f Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Wed, 21 Jan 2026 20:29:07 +0100 Subject: [PATCH 06/61] wip --- luxtronik/collections.py | 167 ++++++++++++++------------------------- 1 file changed, 60 insertions(+), 107 deletions(-) diff --git a/luxtronik/collections.py b/luxtronik/collections.py index 9190bdf7..67389dcf 100644 --- a/luxtronik/collections.py +++ b/luxtronik/collections.py @@ -2,7 +2,13 @@ import logging -from luxtronik.datatypes import Base +from luxtronik.constants import ( + LUXTRONIK_NAME_CHECK_PREFERRED, + LUXTRONIK_NAME_CHECK_OBSOLETE, + LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, +) + +from luxtronik.datatypes import Base, Unknown from luxtronik.definitions import LuxtronikDefinition, LuxtronikDefinitionsDictionary @@ -10,7 +16,7 @@ ############################################################################### -# Common methods +# Common functions ############################################################################### def pack_values(values, num_bits, reverse=True): @@ -72,50 +78,19 @@ def unpack_values(packed, count, num_bits, reverse=True): return values -def get_data_arr(definition, field, num_bits): - """ - Normalize the field's data to a list of the correct size. - - Args: - definition (LuxtronikDefinition): Meta-data of the field. - field (Base): Field object that contains data to get. - num_bits (int): Number of bits per register. - - Returns: - list[int] | None: List of length `definition.count`, - or None if the data size does not match. - """ - data = field.raw - if data is None: - return None - # Currently, no read-modify-write function is implemented. - # For this reason, we cannot write (and retrieve the data to write) - # from a field with a bit_offset. - # -> no additional code here like in `integrate_data` - should_unpack = field.concatenate_multiple_data_chunks \ - and definition.count > 1 - if should_unpack and not isinstance(data, list): - # Usually big-endian (reverse=True) is used - data = unpack_values(data, definition.count, num_bits) - if not isinstance(data, list): - data = [data] - return data if len(data) == definition.count else None - -def integrate_data(definition, field, raw_data, num_bits, data_offset=-1): +def integrate_data(definition, field, raw_data, data_offset=-1): """ - Integrate the related parts of the `raw_data` into the field. + Integrate raw values from a data array into the field. Args: definition (LuxtronikDefinition): Meta-data of the field. field (Base): Field object where to integrate the data. - raw_data (list): Source array of register values. - num_bits (int): Number of bits per register. + raw_data (list): Source array of bytes/words. data_offset (int): Optional offset. Defaults to `definition.index`. """ # Use data_offset if provided, otherwise the index data_offset = data_offset if data_offset >= 0 else definition.index # Use the information of the definition to extract the raw-value - use_bit_offset = definition.bit_offset and definition.num_bits if (data_offset + definition.count - 1) >= len(raw_data): raw = None elif definition.count == 1: @@ -123,17 +98,39 @@ def integrate_data(definition, field, raw_data, num_bits, data_offset=-1): else: raw = raw_data[data_offset : data_offset + definition.count] raw = raw if len(raw) == definition.count else None - should_pack = field.concatenate_multiple_data_chunks + should_pack = field.concatenate_multiple_data_chunks \ + and definition.reg_bits > 0 # and definition.count > 1 if should_pack and raw is not None : # Usually big-endian (reverse=True) is used - raw = pack_values(raw, num_bits) + raw = pack_values(raw, definition.reg_bits) raw = raw if definition.check_raw_not_none(raw) else None - # Perform bit shift operations - if use_bit_offset and isinstance(raw, int): - raw = (raw >> definition.bit_offset) & ((1 << definition.num_bits) - 1) field.raw = raw +def get_data_arr(definition, field): + """ + Normalize the field's data to a list of the correct size. + + Args: + definition (LuxtronikDefinition): Meta-data of the field. + field (Base): Field object that contains data to get. + + Returns: + list[int] | None: List of length `definition.count`, + or None if the data size does not match. + """ + data = field.raw + if data is None: + return None + should_unpack = field.concatenate_multiple_data_chunks \ + and definition.reg_bits > 0 and definition.count > 1 + if should_unpack and not isinstance(data, list): + # Usually big-endian (reverse=True) is used + data = unpack_values(data, definition.count, definition.reg_bits) + if not isinstance(data, list): + data = [data] + return data if len(data) == definition.count else None + ############################################################################### # Definition / field pair ############################################################################### @@ -155,49 +152,39 @@ def __init__(self, definition, field): self.definition = definition def __iter__(self): - """ - Yield the definition and the field to unpack the object like `d, f = pair`. - """ yield self.definition yield self.field @property def index(self): - """ - Forward the `LuxtronikDefinition.index` property. - Please check its documentation. - """ return self.definition.index @property def addr(self): - """ - Forward the `LuxtronikDefinition.addr` property. - Please check its documentation. - """ return self.definition.addr @property def count(self): - """ - Forward the `LuxtronikDefinition.count` property. - Please check its documentation. - """ return self.definition.count - def get_data_arr(self, num_bits): + def get_data_arr(self): """ - Forward the `get_data_arr` method with the stored objects. - Please check its documentation. + Normalize the field's data to a list of the correct size. + + Returns: + list[int] | None: List of length `definition.count`, or None if insufficient. """ - return get_data_arr(self.definition, self.field, num_bits) + return get_data_arr(self.definition, self.field) - def integrate_data(self, raw_data, num_bits, data_offset=-1): + def integrate_data(self, raw_data, data_offset=-1): """ - Forward the `integrate_data` method with the stored objects. - Please check its documentation. + Integrate the related parts of the `raw_data` into the field + + Args: + raw_data (list): Source array of bytes/words. + data_offset (int): Optional offset. Defaults to `definition.index`. """ - integrate_data(self.definition, self.field, raw_data, num_bits, data_offset) + integrate_data(self.definition, self.field, raw_data, data_offset) ############################################################################### # Field dictionary for data vectors @@ -211,7 +198,7 @@ class LuxtronikFieldsDictionary: """ def __init__(self): - # There may be several names or alias that points to one definition and field. + # There may be several names or alias that points to one definition. # So in order to spare memory we split the name/index-to-field-lookup # into a name/index-to-definition-lookup and a definition-to-field-lookup self._def_lookup = LuxtronikDefinitionsDictionary() @@ -221,12 +208,11 @@ def __init__(self): self._pairs = [] # list of LuxtronikDefFieldPair def __getitem__(self, def_field_name_or_idx): - """ - Array-style access to method `get`. - Please check its documentation. - """ return self.get(def_field_name_or_idx) + def __setitem__(self, def_name_or_idx, value): + assert False, "__setitem__ not implemented." + def __len__(self): return len(self._def_lookup._index_dict) @@ -243,12 +229,6 @@ def __contains__(self, def_field_name_or_idx): Check whether the data vector contains a name, index, or definition matching an added field, or the field itself. - If `def_field_name_or_idx` - - is a definition -> check whether a field with this definition has been added - - is a field -> check whether this field has been added - - is a name -> check whether a field with this name has been added - - is a idx -> check whether a field with this index has been added - Args: def_field_name_or_idx (LuxtronikDefinition | Base | str | int): Definition object, field object, field name or register index. @@ -289,20 +269,9 @@ def pairs(self): @property def def_dict(self): - """ - Return the internal definition dictionary, - containing all definitions related to the added fields. - """ + """Return the internal definition dictionary""" return self._def_lookup - @property - def field_dict(self): - """ - Return the internal field dictionary, - containing all added fields. - """ - return self._field_lookup - def add(self, definition, field, alias=None): """ Add a definition-field-pair to the internal dictionaries. @@ -311,8 +280,6 @@ def add(self, definition, field, alias=None): definition (LuxtronikDefinition): Definition related to the field. field (Base): Field to add. alias (Hashable | None): Alias, which can be used to access the field again. - - Note: Only use this method if the definitions order is already correct. """ if definition.valid: self._def_lookup.add(definition, alias) @@ -320,14 +287,6 @@ def add(self, definition, field, alias=None): self._pairs.append(LuxtronikDefFieldPair(definition, field)) def add_sorted(self, definition, field, alias=None): - """ - Behaves like the normal `add` but then sorts the pairs. - - Args: - definition (LuxtronikDefinition): Definition related to the field. - field (Base): Field to add. - alias (Hashable | None): Alias, which can be used to access the field again. - """ if definition.valid: self.add(definition, field, alias) # sort _pairs by definition.index @@ -348,8 +307,8 @@ def register_alias(self, def_field_name_or_idx, alias): Base | None: The field to which the alias was added, or None if not possible """ + # Resolve a field input def_name_or_idx = def_field_name_or_idx - # Resolve a field argument if isinstance(def_name_or_idx, Base): def_name_or_idx = def_name_or_idx.name # register alias @@ -360,17 +319,11 @@ def register_alias(self, def_field_name_or_idx, alias): def get(self, def_field_name_or_idx, default=None): """ - Retrieve an added field by definition, name or register index, or the field itself. - - If `def_field_name_or_idx` - - is a definition -> lookup the field by the definition - - is a field -> lookup the field by the field's name - - is a name -> lookup the field by the name - - is a idx -> lookup the field by the index + Retrieve a field by definition, name or register index. Args: - def_field_name_or_idx (LuxtronikDefinition | Base | str | int): - Definition, field, name, or register index to be used to search for the field. + def_field_name_or_idx (LuxtronikDefinition | str | int): + Definition, name, or register index to be used to search for the field. Returns: Base | None: The field found or the provided default if not found. From 57a5c7317bc5df035efa5ce57fe0a51014da2c0c Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Wed, 21 Jan 2026 20:41:13 +0100 Subject: [PATCH 07/61] wip --- tests/shi/test_shi_definitions.py | 119 +++++++++++++++++++----------- 1 file changed, 77 insertions(+), 42 deletions(-) diff --git a/tests/shi/test_shi_definitions.py b/tests/shi/test_shi_definitions.py index 592fca6f..eb68142c 100644 --- a/tests/shi/test_shi_definitions.py +++ b/tests/shi/test_shi_definitions.py @@ -1,9 +1,9 @@ from luxtronik.constants import LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE -from luxtronik.definitions import LuxtronikDefinition -from luxtronik.data_vector import ( +from luxtronik.collections import ( get_data_arr, integrate_data, ) +from luxtronik.definitions import LuxtronikDefinition ############################################################################### # Tests @@ -14,28 +14,53 @@ 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 + definition._num_bits = 32 + definition._data_type = 'INT32' + field.raw = 5 + arr = get_data_arr(definition, field) + assert arr == [5] + + # get from value + definition._count = 1 + definition._num_bits = 16 + definition._data_type = 'INT16' field.raw = 5 arr = get_data_arr(definition, field) assert arr == [5] # get from array definition._count = 2 + definition._num_bits = 64 + definition._data_type = 'INT64' + field.raw = [7, 3] + arr = get_data_arr(definition, field) + assert arr == [7, 3] + + # get from array + definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' field.raw = [7, 3] arr = get_data_arr(definition, field) assert arr == [7, 3] # too much data definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' field.raw = [4, 8, 1] arr = get_data_arr(definition, field) assert arr is None # insufficient data definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' field.raw = [9] arr = get_data_arr(definition, field) assert arr is None @@ -44,18 +69,32 @@ def test_data_arr(self): # get from array definition._count = 2 + definition._num_bits = 64 + definition._data_type = 'INT64' + field.raw = 0x00000007_00000003 + arr = get_data_arr(definition, field) + assert arr == [7, 3] + + # get from array + definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' field.raw = 0x0007_0003 arr = get_data_arr(definition, field) assert arr == [7, 3] # too much data definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' field.raw = 0x0004_0008_0001 arr = get_data_arr(definition, field) assert arr == [8, 1] # insufficient data definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' field.raw = 0x0009 arr = get_data_arr(definition, field) assert arr == [0, 9] @@ -63,10 +102,10 @@ def test_data_arr(self): 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] + field.concatenate_multiple_data_chunks = False + # set array definition._count = 2 definition._num_bits = 64 @@ -80,6 +119,19 @@ def test_integrate(self): integrate_data(definition, field, data, 0) assert field.raw == [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] + # set array + definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' + integrate_data(definition, field, data) + assert field.raw == [3, 4] + integrate_data(definition, field, data, 4) + assert field.raw == [5, 6] + integrate_data(definition, field, data, 7) + assert field.raw is None + integrate_data(definition, field, data, 0) + assert field.raw == [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] + # set value definition._count = 1 definition._num_bits = 32 @@ -95,6 +147,19 @@ def test_integrate(self): # This applies also to similar lines below assert field.raw == LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE + # set value + definition._count = 1 + definition._num_bits = 16 + definition._data_type = 'INT16' + integrate_data(definition, field, data) + assert field.raw == 3 + integrate_data(definition, field, data, 5) + assert field.raw == 6 + 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 @@ -110,61 +175,31 @@ def test_integrate(self): integrate_data(definition, field, data, 0) assert field.raw == 0x00000001_00007FFF - # set value - definition._count = 1 - definition._num_bits = 32 - definition._data_type = 'INT32' - integrate_data(definition, field, data) - assert field.raw == 0x00000003 - integrate_data(definition, field, data, 5) - assert field.raw == 0x00000006 - integrate_data(definition, field, data, 9) - assert field.raw is None - integrate_data(definition, field, data, 1) - assert field.raw == 0x00007FFF - - field.concatenate_multiple_data_chunks = False - # set array definition._count = 2 definition._num_bits = 32 definition._data_type = 'INT32' integrate_data(definition, field, data) - assert field.raw == [3, 4] + assert field.raw == 0x0003_0004 integrate_data(definition, field, data, 4) - assert field.raw == [5, 6] + 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 == [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] + assert field.raw == 0x0001_7FFF # set value definition._count = 1 - definition._num_bits = 16 - definition._data_type = 'INT16' + definition._num_bits = 32 + definition._data_type = 'INT32' integrate_data(definition, field, data) - assert field.raw == 3 + assert field.raw == 0x00000003 integrate_data(definition, field, data, 5) - assert field.raw == 6 + assert field.raw == 0x00000006 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 - definition._num_bits = 32 - definition._data_type = 'INT32' - 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 == 0x0001_7FFF + assert field.raw == 0x00007FFF # set value definition._count = 1 From 7971ff4782aa228a8741a4cd8037eb7c049ff209 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Wed, 21 Jan 2026 21:31:10 +0100 Subject: [PATCH 08/61] wip --- luxtronik/datatypes.py | 6 ++---- tests/test_datatypes.py | 8 ++++---- tests/test_definition_list.py | 3 ++- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/luxtronik/datatypes.py b/luxtronik/datatypes.py index 74dc3d16..be3a1d17 100755 --- a/luxtronik/datatypes.py +++ b/luxtronik/datatypes.py @@ -378,7 +378,7 @@ def from_heatpump(cls, value): @classmethod def to_heatpump(cls, value): - if not isinstance(value, float): + if not isinstance(value, (int, float, datetime.datetime)): return None return datetime.datetime.timestamp(value) @@ -1108,12 +1108,10 @@ class FullVersion(Base): @classmethod def from_heatpump(cls, value): - if not isinstance(value, list): + if not isinstance(value, list) or len(value) <= 2: return None if len(value) >= 3: return f"{value[0]}.{value[1]}.{value[2]}" - else: - return "0" class Unknown(Base): diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index 5da5cc7e..b3852fa0 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -620,7 +620,7 @@ def test_from_heatpump(self): # pylint: disable=fixme # TODO Consider to drop microseconds when dealing with this datatype? b = datetime.datetime.now() - assert a.from_heatpump(datetime.datetime.timestamp(b)) == b + assert a.from_heatpump(datetime.datetime.timestamp(b)) is None def test_to_heatpump(self): """Test cases for to_heatpump function""" @@ -1009,9 +1009,9 @@ def test_init(self): def test_from_heatpump(self): """Test cases for from_heatpump function""" - assert FullVersion.from_heatpump(112) == "0" - assert FullVersion.from_heatpump(0) == "0" - assert FullVersion.from_heatpump([0, 12]) == "0" + assert FullVersion.from_heatpump(112) is None + assert FullVersion.from_heatpump(0) is None + assert FullVersion.from_heatpump([0, 12]) is None assert FullVersion.from_heatpump([0, 12, 3]) == "0.12.3" class TestIcon: diff --git a/tests/test_definition_list.py b/tests/test_definition_list.py index b0cfef34..a84c7f06 100644 --- a/tests/test_definition_list.py +++ b/tests/test_definition_list.py @@ -2,6 +2,7 @@ from luxtronik.common import parse_version from luxtronik.datatypes import Base +from luxtronik.definitions import LuxtronikDefinition from luxtronik.definitions.calculations import CALCULATIONS_DEFINITIONS_LIST from luxtronik.definitions.holdings import HOLDINGS_DEFINITIONS_LIST from luxtronik.definitions.inputs import INPUTS_DEFINITIONS_LIST @@ -184,7 +185,7 @@ def test_data_type(self): for definition in self.definitions: if KEY_DATATYPE in definition: data_type = definition[KEY_DATATYPE] - assert data_type in VALID_DATA_TYPES, \ + assert data_type in LuxtronikDefinition.VALID_DATA_TYPES, \ f"Datatype must be set correctly: {definition}" def test_since(self): From 09f328d016504e47c37fb075219922c0760d5eab Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Wed, 21 Jan 2026 21:34:54 +0100 Subject: [PATCH 09/61] wip --- luxtronik/collections.py | 8 +------- luxtronik/data_vector.py | 8 +------- tests/test_compatibility.py | 2 +- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/luxtronik/collections.py b/luxtronik/collections.py index 67389dcf..0766f990 100644 --- a/luxtronik/collections.py +++ b/luxtronik/collections.py @@ -2,13 +2,7 @@ import logging -from luxtronik.constants import ( - LUXTRONIK_NAME_CHECK_PREFERRED, - LUXTRONIK_NAME_CHECK_OBSOLETE, - LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, -) - -from luxtronik.datatypes import Base, Unknown +from luxtronik.datatypes import Base from luxtronik.definitions import LuxtronikDefinition, LuxtronikDefinitionsDictionary diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index 2a057e33..ff1cb6ab 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -2,15 +2,9 @@ import logging -from luxtronik.constants import ( - LUXTRONIK_NAME_CHECK_PREFERRED, - LUXTRONIK_NAME_CHECK_OBSOLETE, - LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, -) from luxtronik.collections import LuxtronikFieldsDictionary - from luxtronik.datatypes import Base, Unknown -from luxtronik.definitions import LuxtronikDefinition, LuxtronikDefinitionsDictionary +from luxtronik.definitions import LuxtronikDefinition LOGGER = logging.getLogger(__name__) diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index 49e58b15..d7b6c941 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -1925,7 +1925,7 @@ def test_compatibilities(self): if print_caption: print(f"### Incompatibilities - {caption}:") print_caption = False - print(f'"{old_name}" is not registered for {old_idx}: "{cur_name}",') + print(f'"{old_name}" is not registered for {old_idx}: "{def_by_name.name}",') ok = False assert ok, "Found incompatibilities. Please consider to add them to compatibilities.py" From 308ede27d71261d0023f1af20e604e792e3be5f9 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Wed, 21 Jan 2026 21:43:21 +0100 Subject: [PATCH 10/61] wip --- luxtronik/collections.py | 5 +---- luxtronik/data_vector.py | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/luxtronik/collections.py b/luxtronik/collections.py index 0766f990..fe467937 100644 --- a/luxtronik/collections.py +++ b/luxtronik/collections.py @@ -204,9 +204,6 @@ def __init__(self): def __getitem__(self, def_field_name_or_idx): return self.get(def_field_name_or_idx) - def __setitem__(self, def_name_or_idx, value): - assert False, "__setitem__ not implemented." - def __len__(self): return len(self._def_lookup._index_dict) @@ -262,7 +259,7 @@ def pairs(self): return self._pairs @property - def def_dict(self): + def definitions(self): """Return the internal definition dictionary""" return self._def_lookup diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index ff1cb6ab..5e915e5e 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -214,8 +214,8 @@ def _get_definition(self, def_field_name_or_idx, all_not_version_dependent): if all_not_version_dependent: definition = self.definitions.get(definition) else: - # def_dict contains only valid and addable definitions - definition = self._data.def_dict.get(definition) + # _data.definitions contains only valid and previously added definitions + definition = self._data.definitions.get(definition) return definition, field def get(self, def_name_or_idx, default=None): From 4633eab99dbefd76aa2feaa224a1256d53031b8f Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Wed, 21 Jan 2026 22:06:02 +0100 Subject: [PATCH 11/61] wip --- tests/test_data_vector.py | 161 +------------------------------------- 1 file changed, 2 insertions(+), 159 deletions(-) diff --git a/tests/test_data_vector.py b/tests/test_data_vector.py index e88e0bd8..f12ea2f5 100644 --- a/tests/test_data_vector.py +++ b/tests/test_data_vector.py @@ -3,166 +3,9 @@ # pylint: disable=too-few-public-methods,invalid-name,too-many-lines import pytest -from luxtronik.definitions import LuxtronikDefinition, LuxtronikDefinitionsDictionary -from luxtronik.data_vector import LuxtronikFieldsDictionary, DataVector -from luxtronik.datatypes import ( - Base, - Unknown, -) - - -class TestLuxtronikFieldsDictionary: - - def test_init(self): - d = LuxtronikFieldsDictionary() - - assert type(d._def_lookup) is LuxtronikDefinitionsDictionary - assert type(d._field_lookup) is dict - assert len(d._field_lookup.values()) == 0 - assert type(d._pairs) is list - assert len(d._pairs) == 0 - - def test_add(self): - d = LuxtronikFieldsDictionary() - assert len(d) == 0 - assert len(d._pairs) == 0 - - u = LuxtronikDefinition.unknown(1, "test", 0) - f = u.create_field() - d.add(u, f) - assert len(d) == 1 - assert len(d._pairs) == 1 - assert d._pairs[0].definition is u - assert d._pairs[0].field is f - - u = LuxtronikDefinition.unknown(2, "test", 0) - f = u.create_field() - d.add(u, f) - assert len(d) == 2 - assert len(d._pairs) == 2 - assert d._pairs[1].definition is u - assert d._pairs[1].field is f - - u = LuxtronikDefinition.unknown(0, "test", 0) - f = u.create_field() - d.add_sorted(u, f) - assert len(d) == 3 - assert len(d._pairs) == 3 - assert d._pairs[0].definition is u - assert d._pairs[0].field is f - - def create_instance(self): - d = LuxtronikFieldsDictionary() - u = LuxtronikDefinition.unknown(1, "test", 0) - d.add(u, u.create_field()) - u = LuxtronikDefinition.unknown(2, "test", 0) - d.add(u, u.create_field()) - b = LuxtronikDefinition({ - "index": 2, - "type": Base, - "names": ["base2"], - }, "test", 0) - f = b.create_field() - d.add(b, f) - b = LuxtronikDefinition({ - "index": 3, - "type": Base, - "names": ["base3"], - }, "test", 0) - d.add(b, b.create_field(), "base4") - return d, u, f - - def test_len(self): - d, _, _ = self.create_instance() - # 3 different indices - assert len(d) == 3 - assert len(d._pairs) == 4 - - def test_get_contains(self): - d, u, f = self.create_instance() - assert "1" in d - assert d["1"].name == "unknown_test_1" - assert "unknown_test_1" in d - assert d["unknown_test_1"].name == "unknown_test_1" - assert 2 in d - assert d[2].name == "base2" - assert "unknown_test_2" in d - assert d["unknown_test_2"].name == "unknown_test_2" - assert "base2" in d - assert d["base2"].name == "base2" - assert "base3" in d - assert d.get("base3").name == "base3" - assert "base4" in d - assert d.get("base4").name == "base3" - assert u in d - assert d[u].name == "unknown_test_2" - assert f in d - assert d[f].name == "base2" - assert 4 not in d - - def test_iter(self): - d, _, _ = self.create_instance() - for idx, key in enumerate(d): - if idx == 0: - assert key == 1 - if idx == 1: - assert key == 2 - if idx == 2: - assert key == 3 - - def test_values(self): - d, _, _ = self.create_instance() - for idx, value in enumerate(d.values()): - if idx == 0: - assert type(value) is Unknown - assert value.name == "unknown_test_1" - if idx == 1: - assert type(value) is Base - assert value.name == "base2" - if idx == 2: - assert type(value) is Base - assert value.name == "base3" - - def test_pairs(self): - d, _, _ = self.create_instance() - for idx, (key, value) in enumerate(d.items()): - if idx == 0: - assert key == 1 - assert type(value) is Unknown - assert value.name == "unknown_test_1" - if idx == 1: - assert key == 2 - assert type(value) is Base - assert value.name == "base2" - if idx == 2: - assert key == 3 - assert type(value) is Base - assert value.name == "base3" - - class MyTestClass: - pass - - def test_alias(self): - d, u, f = self.create_instance() - my = self.MyTestClass() - - d.register_alias(0, "abc") - assert d["abc"] is d[0] - - field = d.register_alias("unknown_test_1", 6) - assert d[6] is field - - field = d.register_alias(u, my) - assert d[my] is d[u] - - d.register_alias(f, my) - assert d[my] is not d[u] - assert d[my] is d[f] - - field = d.register_alias(9, my) - assert field is None - assert d[my] is d[f] +from luxtronik.data_vector import DataVector +from luxtronik.datatypes import Base class ObsoleteDataVector(DataVector): From a043b1c4ac1a4b64b3620307c1f7d186415a3c72 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Wed, 21 Jan 2026 22:06:15 +0100 Subject: [PATCH 12/61] wip --- tests/test_collections.py | 223 +------------------------------------- 1 file changed, 6 insertions(+), 217 deletions(-) diff --git a/tests/test_collections.py b/tests/test_collections.py index a1f41adc..a9966743 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -1,223 +1,12 @@ -from luxtronik.collections import ( - get_data_arr, - integrate_data, - LuxtronikDefFieldPair, - LuxtronikFieldsDictionary, -) +import pytest + +from luxtronik.collections import LuxtronikFieldsDictionary from luxtronik.definitions import LuxtronikDefinition, LuxtronikDefinitionsDictionary from luxtronik.datatypes import ( Base, Unknown, ) -from luxtronik.constants import LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE - - -############################################################################### -# Tests -############################################################################### - -class TestDefinitionFieldPair: - - def test_init(self): - definition = LuxtronikDefinition.unknown(2, 'Foo', 30) - field = definition.create_field() - pair = LuxtronikDefFieldPair(definition, field) - - assert pair.definition is definition - assert pair.field is field - d, f = pair - assert d is definition - assert f is field - - def test_data_arr(self): - definition = LuxtronikDefinition.unknown(2, 'Foo', 30) - field = definition.create_field() - pair = LuxtronikDefFieldPair(definition, field) - - field.concatenate_multiple_data_chunks = False - - # get from value - definition._count = 1 - field.raw = 5 - arr = get_data_arr(definition, field, 32) - assert arr == [5] - assert arr == pair.get_data_arr(32) - - # get from value - definition._count = 1 - field.raw = 5 - arr = get_data_arr(definition, field, 16) - assert arr == [5] - assert arr == pair.get_data_arr(16) - - # get from array - definition._count = 2 - field.raw = [7, 3] - arr = get_data_arr(definition, field, 32) - assert arr == [7, 3] - assert arr == pair.get_data_arr(32) - - # get from array - definition._count = 2 - field.raw = [7, 3] - arr = get_data_arr(definition, field, 16) - assert arr == [7, 3] - assert arr == pair.get_data_arr(16) - - # too much data - definition._count = 2 - field.raw = [4, 8, 1] - arr = get_data_arr(definition, field, 16) - assert arr is None - assert arr == pair.get_data_arr(16) - - # insufficient data - definition._count = 2 - field.raw = [9] - arr = get_data_arr(definition, field, 16) - assert arr is None - assert arr == pair.get_data_arr(16) - - field.concatenate_multiple_data_chunks = True - - # get from array - definition._count = 2 - field.raw = 0x00000007_00000003 - arr = get_data_arr(definition, field, 32) - assert arr == [7, 3] - assert arr == pair.get_data_arr(32) - - # get from array - definition._count = 2 - field.raw = 0x0007_0003 - arr = get_data_arr(definition, field, 16) - assert arr == [7, 3] - assert arr == pair.get_data_arr(16) - - # too much data - definition._count = 2 - field.raw = 0x0004_0008_0001 - arr = get_data_arr(definition, field, 16) - assert arr == [8, 1] - assert arr == pair.get_data_arr(16) - - # insufficient data - definition._count = 2 - field.raw = 0x0009 - arr = get_data_arr(definition, field, 16) - assert arr == [0, 9] - assert arr == pair.get_data_arr(16) - - def test_integrate(self): - definition = LuxtronikDefinition.unknown(2, 'Foo', 30) - field = definition.create_field() - pair = LuxtronikDefFieldPair(definition, field) - data = [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, 3, 4, 5, 6, 7] - - field.concatenate_multiple_data_chunks = False - - # set array - definition._count = 2 - definition._data_type = 'INT64' - integrate_data(definition, field, data, 32) - assert field.raw == [3, 4] - pair.integrate_data(data, 32, 4) - assert field.raw == [5, 6] - integrate_data(definition, field, data, 32, 7) - assert field.raw is None - pair.integrate_data(data, 32, 0) - assert field.raw == [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] - - # set array - definition._count = 2 - definition._data_type = 'INT32' - integrate_data(definition, field, data, 16) - assert field.raw == [3, 4] - pair.integrate_data(data, 16, 4) - assert field.raw == [5, 6] - integrate_data(definition, field, data, 16, 7) - assert field.raw is None - pair.integrate_data(data, 16, 0) - assert field.raw == [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] - - # set value - definition._count = 1 - definition._data_type = 'INT32' - integrate_data(definition, field, data, 32) - assert field.raw == 3 - pair.integrate_data(data, 32, 5) - assert field.raw == 6 - integrate_data(definition, field, data, 32, 9) - assert field.raw is None - pair.integrate_data(data, 32, 1) - # Currently there is no magic "not available" value for 32 bit values -> not None - # This applies also to similar lines below - assert field.raw == LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE - - # set value - definition._count = 1 - definition._data_type = 'INT16' - integrate_data(definition, field, data, 16) - assert field.raw == 3 - pair.integrate_data(data, 16, 5) - assert field.raw == 6 - integrate_data(definition, field, data, 16, 9) - assert field.raw is None - pair.integrate_data(data, 16, 1) - assert field.raw is None - - field.concatenate_multiple_data_chunks = True - - # set array - definition._count = 2 - definition._data_type = 'INT64' - integrate_data(definition, field, data, 32) - assert field.raw == 0x00000003_00000004 - pair.integrate_data(data, 32, 4) - assert field.raw == 0x00000005_00000006 - integrate_data(definition, field, data, 32, 7) - assert field.raw is None - pair.integrate_data(data, 32, 0) - assert field.raw == 0x00000001_00007FFF - - # set array - definition._count = 2 - definition._data_type = 'INT32' - integrate_data(definition, field, data, 16) - assert field.raw == 0x0003_0004 - pair.integrate_data(data, 16, 4) - assert field.raw == 0x0005_0006 - integrate_data(definition, field, data, 16, 7) - assert field.raw is None - pair.integrate_data(data, 16, 0) - assert field.raw == 0x0001_7FFF - - # set value - definition._count = 1 - definition._data_type = 'INT32' - integrate_data(definition, field, data, 32) - assert field.raw == 0x00000003 - pair.integrate_data(data, 32, 5) - assert field.raw == 0x00000006 - integrate_data(definition, field, data, 32, 9) - assert field.raw is None - pair.integrate_data(data, 32, 1) - assert field.raw == 0x00007FFF - - # set value - definition._count = 1 - definition._data_type = 'INT16' - integrate_data(definition, field, data, 16) - assert field.raw == 0x0003 - pair.integrate_data(data, 16, 5) - assert field.raw == 0x0006 - integrate_data(definition, field, data, 16, 9) - assert field.raw is None - pair.integrate_data(data, 16, 1) - assert field.raw is None - - field.concatenate_multiple_data_chunks = False class TestLuxtronikFieldsDictionary: @@ -234,7 +23,7 @@ def test_init(self): def test_add(self): d = LuxtronikFieldsDictionary() assert len(d) == 0 - assert len(d.pairs()) == 0 + assert len(d._pairs) == 0 u = LuxtronikDefinition.unknown(1, "test", 0) f = u.create_field() @@ -285,7 +74,7 @@ def test_len(self): d, _, _ = self.create_instance() # 3 different indices assert len(d) == 3 - assert len(d.pairs()) == 4 + assert len(d._pairs) == 4 def test_get_contains(self): d, u, f = self.create_instance() @@ -332,7 +121,7 @@ def test_values(self): assert type(value) is Base assert value.name == "base3" - def test_items(self): + def test_pairs(self): d, _, _ = self.create_instance() for idx, (key, value) in enumerate(d.items()): if idx == 0: From 1084b9f1c93d7e6389ce6b039260ff0b324cab9e Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Wed, 21 Jan 2026 22:28:22 +0100 Subject: [PATCH 13/61] wip --- luxtronik/collections.py | 18 +++++++++++++----- luxtronik/data_vector.py | 4 ++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/luxtronik/collections.py b/luxtronik/collections.py index fe467937..f71c4d01 100644 --- a/luxtronik/collections.py +++ b/luxtronik/collections.py @@ -259,8 +259,8 @@ def pairs(self): return self._pairs @property - def definitions(self): - """Return the internal definition dictionary""" + def def_dict(self): + """Return the internal definition dictionary, containing all added definitions""" return self._def_lookup def add(self, definition, field, alias=None): @@ -278,6 +278,14 @@ def add(self, definition, field, alias=None): self._pairs.append(LuxtronikDefFieldPair(definition, field)) def add_sorted(self, definition, field, alias=None): + """ + Behaves like the normal `add` but then sorts the pairs. + + Args: + definition (LuxtronikDefinition): Definition related to the field. + field (Base): Field to add. + alias (Hashable | None): Alias, which can be used to access the field again. + """ if definition.valid: self.add(definition, field, alias) # sort _pairs by definition.index @@ -310,11 +318,11 @@ def register_alias(self, def_field_name_or_idx, alias): def get(self, def_field_name_or_idx, default=None): """ - Retrieve a field by definition, name or register index. + Retrieve a field by definition, name or register index, or the field itself. Args: - def_field_name_or_idx (LuxtronikDefinition | str | int): - Definition, name, or register index to be used to search for the field. + def_field_name_or_idx (LuxtronikDefinition | Base | str | int): + Definition, field, name, or register index to be used to search for the field. Returns: Base | None: The field found or the provided default if not found. diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index 5e915e5e..f8023726 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -214,8 +214,8 @@ def _get_definition(self, def_field_name_or_idx, all_not_version_dependent): if all_not_version_dependent: definition = self.definitions.get(definition) else: - # _data.definitions contains only valid and previously added definitions - definition = self._data.definitions.get(definition) + # _data.def_dict contains only valid and previously added definitions + definition = self._data.def_dict.get(definition) return definition, field def get(self, def_name_or_idx, default=None): From f77d4ee940d5a52a3679ec61eff5af81b7ab8623 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Wed, 21 Jan 2026 23:24:31 +0100 Subject: [PATCH 14/61] wip --- tests/shi/test_shi_definitions.py | 217 ------------------------------ tests/test_collections.py | 217 ++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 217 deletions(-) delete mode 100644 tests/shi/test_shi_definitions.py diff --git a/tests/shi/test_shi_definitions.py b/tests/shi/test_shi_definitions.py deleted file mode 100644 index eb68142c..00000000 --- a/tests/shi/test_shi_definitions.py +++ /dev/null @@ -1,217 +0,0 @@ -from luxtronik.constants import LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE -from luxtronik.collections import ( - get_data_arr, - integrate_data, -) -from luxtronik.definitions import LuxtronikDefinition - -############################################################################### -# Tests -############################################################################### - -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 - definition._num_bits = 32 - definition._data_type = 'INT32' - field.raw = 5 - arr = get_data_arr(definition, field) - assert arr == [5] - - # get from value - definition._count = 1 - definition._num_bits = 16 - definition._data_type = 'INT16' - field.raw = 5 - arr = get_data_arr(definition, field) - assert arr == [5] - - # get from array - definition._count = 2 - definition._num_bits = 64 - definition._data_type = 'INT64' - field.raw = [7, 3] - arr = get_data_arr(definition, field) - assert arr == [7, 3] - - # get from array - definition._count = 2 - definition._num_bits = 32 - definition._data_type = 'INT32' - field.raw = [7, 3] - arr = get_data_arr(definition, field) - assert arr == [7, 3] - - # too much data - definition._count = 2 - definition._num_bits = 32 - definition._data_type = 'INT32' - field.raw = [4, 8, 1] - arr = get_data_arr(definition, field) - assert arr is None - - # insufficient data - definition._count = 2 - definition._num_bits = 32 - definition._data_type = 'INT32' - field.raw = [9] - arr = get_data_arr(definition, field) - assert arr is None - - field.concatenate_multiple_data_chunks = True - - # get from array - definition._count = 2 - definition._num_bits = 64 - definition._data_type = 'INT64' - field.raw = 0x00000007_00000003 - arr = get_data_arr(definition, field) - assert arr == [7, 3] - - # get from array - definition._count = 2 - definition._num_bits = 32 - definition._data_type = 'INT32' - field.raw = 0x0007_0003 - arr = get_data_arr(definition, field) - assert arr == [7, 3] - - # too much data - definition._count = 2 - definition._num_bits = 32 - definition._data_type = 'INT32' - field.raw = 0x0004_0008_0001 - arr = get_data_arr(definition, field) - assert arr == [8, 1] - - # insufficient data - definition._count = 2 - definition._num_bits = 32 - definition._data_type = 'INT32' - field.raw = 0x0009 - arr = get_data_arr(definition, field) - assert arr == [0, 9] - - def test_integrate(self): - definition = LuxtronikDefinition.unknown(2, 'Foo', 30) - field = definition.create_field() - data = [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, 3, 4, 5, 6, 7] - - field.concatenate_multiple_data_chunks = False - - # set array - definition._count = 2 - definition._num_bits = 64 - definition._data_type = 'INT64' - integrate_data(definition, field, data) - assert field.raw == [3, 4] - integrate_data(definition, field, data, 4) - assert field.raw == [5, 6] - integrate_data(definition, field, data, 7) - assert field.raw is None - integrate_data(definition, field, data, 0) - assert field.raw == [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] - - # set array - definition._count = 2 - definition._num_bits = 32 - definition._data_type = 'INT32' - integrate_data(definition, field, data) - assert field.raw == [3, 4] - integrate_data(definition, field, data, 4) - assert field.raw == [5, 6] - integrate_data(definition, field, data, 7) - assert field.raw is None - integrate_data(definition, field, data, 0) - assert field.raw == [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] - - # set value - definition._count = 1 - definition._num_bits = 32 - definition._data_type = 'INT32' - integrate_data(definition, field, data) - assert field.raw == 3 - integrate_data(definition, field, data, 5) - assert field.raw == 6 - integrate_data(definition, field, data, 9) - assert field.raw is None - integrate_data(definition, field, data, 1) - # Currently there is no magic "not available" value for 32 bit values -> not None - # This applies also to similar lines below - assert field.raw == LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE - - # set value - definition._count = 1 - definition._num_bits = 16 - definition._data_type = 'INT16' - integrate_data(definition, field, data) - assert field.raw == 3 - integrate_data(definition, field, data, 5) - assert field.raw == 6 - 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 - definition._num_bits = 64 - definition._data_type = 'INT64' - integrate_data(definition, field, data) - assert field.raw == 0x00000003_00000004 - integrate_data(definition, field, data, 4) - assert field.raw == 0x00000005_00000006 - integrate_data(definition, field, data, 7) - assert field.raw is None - integrate_data(definition, field, data, 0) - assert field.raw == 0x00000001_00007FFF - - # set array - definition._count = 2 - definition._num_bits = 32 - definition._data_type = 'INT32' - 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 == 0x0001_7FFF - - # set value - definition._count = 1 - definition._num_bits = 32 - definition._data_type = 'INT32' - integrate_data(definition, field, data) - assert field.raw == 0x00000003 - integrate_data(definition, field, data, 5) - assert field.raw == 0x00000006 - integrate_data(definition, field, data, 9) - assert field.raw is None - integrate_data(definition, field, data, 1) - assert field.raw == 0x00007FFF - - # set value - definition._count = 1 - definition._num_bits = 16 - definition._data_type = 'INT16' - 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 - - field.concatenate_multiple_data_chunks = False diff --git a/tests/test_collections.py b/tests/test_collections.py index a9966743..20d8797d 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -7,6 +7,223 @@ Base, Unknown, ) +from luxtronik.constants import LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE +from luxtronik.collections import ( + get_data_arr, + integrate_data, +) +from luxtronik.definitions import LuxtronikDefinition + +############################################################################### +# Tests +############################################################################### + +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 + definition._num_bits = 32 + definition._data_type = 'INT32' + field.raw = 5 + arr = get_data_arr(definition, field) + assert arr == [5] + + # get from value + definition._count = 1 + definition._num_bits = 16 + definition._data_type = 'INT16' + field.raw = 5 + arr = get_data_arr(definition, field) + assert arr == [5] + + # get from array + definition._count = 2 + definition._num_bits = 64 + definition._data_type = 'INT64' + field.raw = [7, 3] + arr = get_data_arr(definition, field) + assert arr == [7, 3] + + # get from array + definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' + field.raw = [7, 3] + arr = get_data_arr(definition, field) + assert arr == [7, 3] + + # too much data + definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' + field.raw = [4, 8, 1] + arr = get_data_arr(definition, field) + assert arr is None + + # insufficient data + definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' + field.raw = [9] + arr = get_data_arr(definition, field) + assert arr is None + + field.concatenate_multiple_data_chunks = True + + # get from array + definition._count = 2 + definition._num_bits = 64 + definition._data_type = 'INT64' + field.raw = 0x00000007_00000003 + arr = get_data_arr(definition, field) + assert arr == [7, 3] + + # get from array + definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' + field.raw = 0x0007_0003 + arr = get_data_arr(definition, field) + assert arr == [7, 3] + + # too much data + definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' + field.raw = 0x0004_0008_0001 + arr = get_data_arr(definition, field) + assert arr == [8, 1] + + # insufficient data + definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' + field.raw = 0x0009 + arr = get_data_arr(definition, field) + assert arr == [0, 9] + + def test_integrate(self): + definition = LuxtronikDefinition.unknown(2, 'Foo', 30) + field = definition.create_field() + data = [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, 3, 4, 5, 6, 7] + + field.concatenate_multiple_data_chunks = False + + # set array + definition._count = 2 + definition._num_bits = 64 + definition._data_type = 'INT64' + integrate_data(definition, field, data) + assert field.raw == [3, 4] + integrate_data(definition, field, data, 4) + assert field.raw == [5, 6] + integrate_data(definition, field, data, 7) + assert field.raw is None + integrate_data(definition, field, data, 0) + assert field.raw == [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] + + # set array + definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' + integrate_data(definition, field, data) + assert field.raw == [3, 4] + integrate_data(definition, field, data, 4) + assert field.raw == [5, 6] + integrate_data(definition, field, data, 7) + assert field.raw is None + integrate_data(definition, field, data, 0) + assert field.raw == [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] + + # set value + definition._count = 1 + definition._num_bits = 32 + definition._data_type = 'INT32' + integrate_data(definition, field, data) + assert field.raw == 3 + integrate_data(definition, field, data, 5) + assert field.raw == 6 + integrate_data(definition, field, data, 9) + assert field.raw is None + integrate_data(definition, field, data, 1) + # Currently there is no magic "not available" value for 32 bit values -> not None + # This applies also to similar lines below + assert field.raw == LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE + + # set value + definition._count = 1 + definition._num_bits = 16 + definition._data_type = 'INT16' + integrate_data(definition, field, data) + assert field.raw == 3 + integrate_data(definition, field, data, 5) + assert field.raw == 6 + 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 + definition._num_bits = 64 + definition._data_type = 'INT64' + integrate_data(definition, field, data) + assert field.raw == 0x00000003_00000004 + integrate_data(definition, field, data, 4) + assert field.raw == 0x00000005_00000006 + integrate_data(definition, field, data, 7) + assert field.raw is None + integrate_data(definition, field, data, 0) + assert field.raw == 0x00000001_00007FFF + + # set array + definition._count = 2 + definition._num_bits = 32 + definition._data_type = 'INT32' + 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 == 0x0001_7FFF + + # set value + definition._count = 1 + definition._num_bits = 32 + definition._data_type = 'INT32' + integrate_data(definition, field, data) + assert field.raw == 0x00000003 + integrate_data(definition, field, data, 5) + assert field.raw == 0x00000006 + integrate_data(definition, field, data, 9) + assert field.raw is None + integrate_data(definition, field, data, 1) + assert field.raw == 0x00007FFF + + # set value + definition._count = 1 + definition._num_bits = 16 + definition._data_type = 'INT16' + 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 + + field.concatenate_multiple_data_chunks = False class TestLuxtronikFieldsDictionary: From 7c0e0d59ee44c92765ce9a15d70b120824bd77b1 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Wed, 21 Jan 2026 23:29:35 +0100 Subject: [PATCH 15/61] wip --- tests/test_collections.py | 75 +++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/tests/test_collections.py b/tests/test_collections.py index 20d8797d..df3d59d4 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -1,18 +1,18 @@ -import pytest -from luxtronik.collections import LuxtronikFieldsDictionary +from luxtronik.collections import ( + get_data_arr, + integrate_data, + LuxtronikDefFieldPair, + LuxtronikFieldsDictionary, +) from luxtronik.definitions import LuxtronikDefinition, LuxtronikDefinitionsDictionary from luxtronik.datatypes import ( Base, Unknown, ) from luxtronik.constants import LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE -from luxtronik.collections import ( - get_data_arr, - integrate_data, -) -from luxtronik.definitions import LuxtronikDefinition + ############################################################################### # Tests @@ -20,9 +20,21 @@ class TestDefinitionFieldPair: + def test_init(self): + definition = LuxtronikDefinition.unknown(2, 'Foo', 30) + field = definition.create_field() + pair = LuxtronikDefFieldPair(definition, field) + + assert pair.definition is definition + assert pair.field is field + d, f = pair + assert d is definition + assert f is field + def test_data_arr(self): definition = LuxtronikDefinition.unknown(2, 'Foo', 30) field = definition.create_field() + pair = LuxtronikDefFieldPair(definition, field) field.concatenate_multiple_data_chunks = False @@ -33,6 +45,7 @@ def test_data_arr(self): field.raw = 5 arr = get_data_arr(definition, field) assert arr == [5] + assert arr == pair.get_data_arr() # get from value definition._count = 1 @@ -41,6 +54,7 @@ def test_data_arr(self): field.raw = 5 arr = get_data_arr(definition, field) assert arr == [5] + assert arr == pair.get_data_arr() # get from array definition._count = 2 @@ -49,6 +63,7 @@ def test_data_arr(self): field.raw = [7, 3] arr = get_data_arr(definition, field) assert arr == [7, 3] + assert arr == pair.get_data_arr() # get from array definition._count = 2 @@ -57,6 +72,7 @@ def test_data_arr(self): field.raw = [7, 3] arr = get_data_arr(definition, field) assert arr == [7, 3] + assert arr == pair.get_data_arr() # too much data definition._count = 2 @@ -65,6 +81,7 @@ def test_data_arr(self): field.raw = [4, 8, 1] arr = get_data_arr(definition, field) assert arr is None + assert arr == pair.get_data_arr() # insufficient data definition._count = 2 @@ -73,6 +90,7 @@ def test_data_arr(self): field.raw = [9] arr = get_data_arr(definition, field) assert arr is None + assert arr == pair.get_data_arr() field.concatenate_multiple_data_chunks = True @@ -83,6 +101,7 @@ def test_data_arr(self): field.raw = 0x00000007_00000003 arr = get_data_arr(definition, field) assert arr == [7, 3] + assert arr == pair.get_data_arr() # get from array definition._count = 2 @@ -91,6 +110,7 @@ def test_data_arr(self): field.raw = 0x0007_0003 arr = get_data_arr(definition, field) assert arr == [7, 3] + assert arr == pair.get_data_arr() # too much data definition._count = 2 @@ -99,6 +119,7 @@ def test_data_arr(self): field.raw = 0x0004_0008_0001 arr = get_data_arr(definition, field) assert arr == [8, 1] + assert arr == pair.get_data_arr() # insufficient data definition._count = 2 @@ -107,10 +128,12 @@ def test_data_arr(self): field.raw = 0x0009 arr = get_data_arr(definition, field) assert arr == [0, 9] + assert arr == pair.get_data_arr() def test_integrate(self): definition = LuxtronikDefinition.unknown(2, 'Foo', 30) field = definition.create_field() + pair = LuxtronikDefFieldPair(definition, field) data = [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, 3, 4, 5, 6, 7] field.concatenate_multiple_data_chunks = False @@ -121,11 +144,11 @@ def test_integrate(self): definition._data_type = 'INT64' integrate_data(definition, field, data) assert field.raw == [3, 4] - integrate_data(definition, field, data, 4) + pair.integrate_data(data, 4) assert field.raw == [5, 6] integrate_data(definition, field, data, 7) assert field.raw is None - integrate_data(definition, field, data, 0) + pair.integrate_data(data, 0) assert field.raw == [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] # set array @@ -134,11 +157,11 @@ def test_integrate(self): definition._data_type = 'INT32' integrate_data(definition, field, data) assert field.raw == [3, 4] - integrate_data(definition, field, data, 4) + pair.integrate_data(data, 4) assert field.raw == [5, 6] integrate_data(definition, field, data, 7) assert field.raw is None - integrate_data(definition, field, data, 0) + pair.integrate_data(data, 0) assert field.raw == [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] # set value @@ -147,11 +170,11 @@ def test_integrate(self): definition._data_type = 'INT32' integrate_data(definition, field, data) assert field.raw == 3 - integrate_data(definition, field, data, 5) + pair.integrate_data(data, 5) assert field.raw == 6 integrate_data(definition, field, data, 9) assert field.raw is None - integrate_data(definition, field, data, 1) + pair.integrate_data(data, 1) # Currently there is no magic "not available" value for 32 bit values -> not None # This applies also to similar lines below assert field.raw == LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE @@ -162,11 +185,11 @@ def test_integrate(self): definition._data_type = 'INT16' integrate_data(definition, field, data) assert field.raw == 3 - integrate_data(definition, field, data, 5) + pair.integrate_data(data, 5) assert field.raw == 6 integrate_data(definition, field, data, 9) assert field.raw is None - integrate_data(definition, field, data, 1) + pair.integrate_data(data, 1) assert field.raw is None field.concatenate_multiple_data_chunks = True @@ -177,11 +200,11 @@ def test_integrate(self): definition._data_type = 'INT64' integrate_data(definition, field, data) assert field.raw == 0x00000003_00000004 - integrate_data(definition, field, data, 4) + pair.integrate_data(data, 4) assert field.raw == 0x00000005_00000006 integrate_data(definition, field, data, 7) assert field.raw is None - integrate_data(definition, field, data, 0) + pair.integrate_data(data, 0) assert field.raw == 0x00000001_00007FFF # set array @@ -190,11 +213,11 @@ def test_integrate(self): definition._data_type = 'INT32' integrate_data(definition, field, data) assert field.raw == 0x0003_0004 - integrate_data(definition, field, data, 4) + pair.integrate_data(data, 4) assert field.raw == 0x0005_0006 integrate_data(definition, field, data, 7) assert field.raw is None - integrate_data(definition, field, data, 0) + pair.integrate_data(data, 0) assert field.raw == 0x0001_7FFF # set value @@ -203,11 +226,11 @@ def test_integrate(self): definition._data_type = 'INT32' integrate_data(definition, field, data) assert field.raw == 0x00000003 - integrate_data(definition, field, data, 5) + pair.integrate_data(data, 5) assert field.raw == 0x00000006 integrate_data(definition, field, data, 9) assert field.raw is None - integrate_data(definition, field, data, 1) + pair.integrate_data(data, 1) assert field.raw == 0x00007FFF # set value @@ -216,11 +239,11 @@ def test_integrate(self): definition._data_type = 'INT16' integrate_data(definition, field, data) assert field.raw == 0x0003 - integrate_data(definition, field, data, 5) + pair.integrate_data(data, 5) assert field.raw == 0x0006 integrate_data(definition, field, data, 9) assert field.raw is None - integrate_data(definition, field, data, 1) + pair.integrate_data(data, 1) assert field.raw is None field.concatenate_multiple_data_chunks = False @@ -240,7 +263,7 @@ def test_init(self): def test_add(self): d = LuxtronikFieldsDictionary() assert len(d) == 0 - assert len(d._pairs) == 0 + assert len(d.pairs()) == 0 u = LuxtronikDefinition.unknown(1, "test", 0) f = u.create_field() @@ -291,7 +314,7 @@ def test_len(self): d, _, _ = self.create_instance() # 3 different indices assert len(d) == 3 - assert len(d._pairs) == 4 + assert len(d.pairs()) == 4 def test_get_contains(self): d, u, f = self.create_instance() @@ -338,7 +361,7 @@ def test_values(self): assert type(value) is Base assert value.name == "base3" - def test_pairs(self): + def test_items(self): d, _, _ = self.create_instance() for idx, (key, value) in enumerate(d.items()): if idx == 0: From d1441a7613895fd46755e81bd39588c83530eb92 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Thu, 22 Jan 2026 20:25:40 +0100 Subject: [PATCH 16/61] wip --- luxtronik/cfi/calculations.py | 4 --- luxtronik/cfi/parameters.py | 4 --- luxtronik/cfi/visibilities.py | 4 --- luxtronik/collections.py | 52 +++++++++++++++--------------- luxtronik/data_vector.py | 35 ++++++++++++++------ luxtronik/shi/holdings.py | 6 +--- luxtronik/shi/inputs.py | 6 +--- tests/cfi/test_cfi_calculations.py | 3 +- tests/cfi/test_cfi_parameters.py | 3 +- tests/cfi/test_cfi_visibilities.py | 3 +- tests/shi/test_shi_modbus.py | 1 - tests/shi/test_shi_vector.py | 4 +-- 12 files changed, 58 insertions(+), 67 deletions(-) diff --git a/luxtronik/cfi/calculations.py b/luxtronik/cfi/calculations.py index 51d98785..f793869b 100644 --- a/luxtronik/cfi/calculations.py +++ b/luxtronik/cfi/calculations.py @@ -34,10 +34,6 @@ class Calculations(DataVectorConfig): "ID_WEB_SoftStand": "get_firmware_version()" } - @property - def calculations(self): - return self._data - 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)]) diff --git a/luxtronik/cfi/parameters.py b/luxtronik/cfi/parameters.py index d3906369..034c3fa8 100644 --- a/luxtronik/cfi/parameters.py +++ b/luxtronik/cfi/parameters.py @@ -28,7 +28,3 @@ class Parameters(DataVectorConfig): name = PARAMETERS_FIELD_NAME definitions = PARAMETERS_DEFINITIONS - - @property - def parameters(self): - return self._data diff --git a/luxtronik/cfi/visibilities.py b/luxtronik/cfi/visibilities.py index 99ceb3e2..0979001f 100644 --- a/luxtronik/cfi/visibilities.py +++ b/luxtronik/cfi/visibilities.py @@ -28,7 +28,3 @@ class Visibilities(DataVectorConfig): name = VISIBILITIES_FIELD_NAME definitions = VISIBILITIES_DEFINITIONS - - @property - def visibilities(self): - return self._data diff --git a/luxtronik/collections.py b/luxtronik/collections.py index f71c4d01..899acdd5 100644 --- a/luxtronik/collections.py +++ b/luxtronik/collections.py @@ -10,7 +10,7 @@ ############################################################################### -# Common functions +# Common methods ############################################################################### def pack_values(values, num_bits, reverse=True): @@ -72,6 +72,30 @@ def unpack_values(packed, count, num_bits, reverse=True): return values +def get_data_arr(definition, field): + """ + Normalize the field's data to a list of the correct size. + + Args: + definition (LuxtronikDefinition): Meta-data of the field. + field (Base): Field object that contains data to get. + + Returns: + list[int] | None: List of length `definition.count`, + or None if the data size does not match. + """ + data = field.raw + if data is None: + return None + should_unpack = field.concatenate_multiple_data_chunks \ + and definition.reg_bits > 0 and definition.count > 1 + if should_unpack and not isinstance(data, list): + # Usually big-endian (reverse=True) is used + data = unpack_values(data, definition.count, definition.reg_bits) + if not isinstance(data, list): + data = [data] + return data if len(data) == definition.count else None + def integrate_data(definition, field, raw_data, data_offset=-1): """ Integrate raw values from a data array into the field. @@ -101,30 +125,6 @@ def integrate_data(definition, field, raw_data, data_offset=-1): raw = raw if definition.check_raw_not_none(raw) else None field.raw = raw -def get_data_arr(definition, field): - """ - Normalize the field's data to a list of the correct size. - - Args: - definition (LuxtronikDefinition): Meta-data of the field. - field (Base): Field object that contains data to get. - - Returns: - list[int] | None: List of length `definition.count`, - or None if the data size does not match. - """ - data = field.raw - if data is None: - return None - should_unpack = field.concatenate_multiple_data_chunks \ - and definition.reg_bits > 0 and definition.count > 1 - if should_unpack and not isinstance(data, list): - # Usually big-endian (reverse=True) is used - data = unpack_values(data, definition.count, definition.reg_bits) - if not isinstance(data, list): - data = [data] - return data if len(data) == definition.count else None - ############################################################################### # Definition / field pair ############################################################################### @@ -175,7 +175,7 @@ def integrate_data(self, raw_data, data_offset=-1): Integrate the related parts of the `raw_data` into the field Args: - raw_data (list): Source array of bytes/words. + raw_data (list): Source array of register values. data_offset (int): Optional offset. Defaults to `definition.index`. """ integrate_data(self.definition, self.field, raw_data, data_offset) diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index f8023726..d3762641 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -16,7 +16,7 @@ class DataVector: """ - Class that holds a vector of data entries. + Class that holds a vector of data fields. Provides access to fields by name, index or alias. To use aliases, they must first be registered here (locally = only valid @@ -50,32 +50,43 @@ def create_unknown_field(cls, idx): return Unknown(f"unknown_{cls.name}_{idx}", False) @classmethod - def create_any_field(cls, name_or_idx): + def create_any_field(cls, def_name_or_idx): """ Create a field object from an available definition (= included in class variable `cls.definitions`). Be careful! The used controller firmware may not support this field. + If `def_name_or_idx` + - is a definition -> create the field from the provided definition + - is a name -> lookup the definition by name and create the field + - is a idx -> lookup definition by index and create the field + Args: - name_or_idx (str | int): Field name or register index. + def_name_or_idx (LuxtronikDefinition | str | int): Definitions object, + field name or register index. Returns: Base | None: The created field, or None if not found or not valid. """ - # The definitions object hold all available definitions - definition = cls.definitions.get(name_or_idx) + definition, _ = self._get_definition(def_name_or_idx, True) if definition is not None and definition.valid: return definition.create_field() return None - def create_field(self, name_or_idx): + def create_field(self, def_name_or_idx): """ Create a field object from a version-dependent definition (= included in class variable `cls.definitions` and is valid for `self.version`). + If `def_name_or_idx` + - is a definition -> create the field from the provided definition + - is a name -> lookup the definition by name and create the field + - is a idx -> lookup definition by index and create the field + Args: - name_or_idx (str | int): Field name or register index. + def_name_or_idx (str | int): Definitions object, + field name or register index. Returns: Base | None: The created field, or None if not found or not valid. @@ -120,6 +131,12 @@ def __contains__(self, def_field_name_or_idx): Check whether the data vector contains a name, index, or definition matching an added field, or the field itself. + If `def_field_name_or_idx` + - is a definition -> check whether a field with this definition has been added + - is a field -> check whether this field has been added + - is a name -> check whether a field with this name has been added + - is a idx -> check whether a field with this index has been added + Args: def_field_name_or_idx (LuxtronikDefinition | Base | str | int): Definition object, field object, field name or register index. @@ -218,6 +235,8 @@ def _get_definition(self, def_field_name_or_idx, all_not_version_dependent): definition = self._data.def_dict.get(definition) return definition, field + + def get(self, def_name_or_idx, default=None): """ Retrieve a field by definition, name or register index. @@ -255,9 +274,7 @@ def set(self, def_field_name_or_idx, value): value (int | List[int]): Value to set """ field = def_field_name_or_idx - print(field) if not isinstance(field, Base): field = self.get(def_field_name_or_idx) - print(field) if field is not None: field.value = value \ No newline at end of file diff --git a/luxtronik/shi/holdings.py b/luxtronik/shi/holdings.py index 53f6e0d4..cdc39054 100644 --- a/luxtronik/shi/holdings.py +++ b/luxtronik/shi/holdings.py @@ -27,8 +27,4 @@ class Holdings(DataVectorSmartHome): """Class that holds holding fields.""" name = HOLDINGS_FIELD_NAME - definitions = HOLDINGS_DEFINITIONS - - @property - def holdings(self): - return self._data \ No newline at end of file + definitions = HOLDINGS_DEFINITIONS \ No newline at end of file diff --git a/luxtronik/shi/inputs.py b/luxtronik/shi/inputs.py index 3a87f703..d609e9c4 100644 --- a/luxtronik/shi/inputs.py +++ b/luxtronik/shi/inputs.py @@ -27,8 +27,4 @@ class Inputs(DataVectorSmartHome): """Class that holds input fields.""" name = INPUTS_FIELD_NAME - definitions = INPUTS_DEFINITIONS - - @property - def inputs(self): - return self._data \ No newline at end of file + definitions = INPUTS_DEFINITIONS \ No newline at end of file diff --git a/tests/cfi/test_cfi_calculations.py b/tests/cfi/test_cfi_calculations.py index fd44cbed..3eda49c8 100644 --- a/tests/cfi/test_cfi_calculations.py +++ b/tests/cfi/test_cfi_calculations.py @@ -13,12 +13,11 @@ def test_init(self): """Test cases for initialization""" calculations = Calculations() assert calculations.name == "calculation" - assert calculations.calculations == calculations._data def test_data(self): """Test cases for the data dictionary""" calculations = Calculations() - data = calculations.calculations + data = calculations.data # The Value must be a fields # The key can be an index diff --git a/tests/cfi/test_cfi_parameters.py b/tests/cfi/test_cfi_parameters.py index 9a033f24..74409097 100644 --- a/tests/cfi/test_cfi_parameters.py +++ b/tests/cfi/test_cfi_parameters.py @@ -13,7 +13,6 @@ def test_init(self): """Test cases for initialization""" parameters = Parameters() assert parameters.name == "parameter" - assert parameters.parameters == parameters._data assert parameters.safe parameters = Parameters(False) @@ -22,7 +21,7 @@ def test_init(self): def test_data(self): """Test cases for the data dictionary""" parameters = Parameters() - data = parameters.parameters + data = parameters.data # The Value must be a fields # The key can be an index diff --git a/tests/cfi/test_cfi_visibilities.py b/tests/cfi/test_cfi_visibilities.py index 8ecb6f72..697005bd 100644 --- a/tests/cfi/test_cfi_visibilities.py +++ b/tests/cfi/test_cfi_visibilities.py @@ -13,12 +13,11 @@ def test_init(self): """Test cases for initialization""" visibilities = Visibilities() assert visibilities.name == "visibility" - assert visibilities.visibilities == visibilities._data def test_data(self): """Test cases for the data dictionary""" visibilities = Visibilities() - data = visibilities.visibilities + data = visibilities.data # The Value must be a fields # The key can be an index diff --git a/tests/shi/test_shi_modbus.py b/tests/shi/test_shi_modbus.py index ba432418..1d0145ee 100644 --- a/tests/shi/test_shi_modbus.py +++ b/tests/shi/test_shi_modbus.py @@ -95,7 +95,6 @@ def test_no_connection(self): # Cannot connect to read holdings data = LuxtronikSmartHomeReadHoldingsTelegram(0, 1) result = self.modbus_interface.send(data) - print(data.data) assert not result # Cannot connect to write holdings diff --git a/tests/shi/test_shi_vector.py b/tests/shi/test_shi_vector.py index b1bd5af3..faeff419 100644 --- a/tests/shi/test_shi_vector.py +++ b/tests/shi/test_shi_vector.py @@ -92,7 +92,7 @@ def test_create(self): data_vector = DataVectorTest(parse_version("1.2")) assert data_vector.version == (1, 2, 0, 0) assert len(data_vector) == 2 - assert len(data_vector._data._pairs) == 3 + assert len(data_vector._data.pairs()) == 3 assert not data_vector._read_blocks_up_to_date assert len(data_vector._read_blocks) == 0 @@ -409,7 +409,6 @@ def test_init(self): """Test cases for initialization""" holdings = Holdings() assert holdings.name == "holding" - assert holdings.holdings == holdings._data class TestInputs: @@ -419,4 +418,3 @@ def test_init(self): """Test cases for initialization""" inputs = Inputs() assert inputs.name == "input" - assert inputs.inputs == inputs._data From db46fbaa35fdeeb2b4385f0d59f7e67ff95170d8 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Thu, 22 Jan 2026 20:40:28 +0100 Subject: [PATCH 17/61] wip --- luxtronik/collections.py | 27 ++++++++++++++++----------- luxtronik/data_vector.py | 5 +++-- luxtronik/definitions/__init__.py | 5 ----- luxtronik/shi/contiguous.py | 3 ++- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/luxtronik/collections.py b/luxtronik/collections.py index 899acdd5..3c9eebe9 100644 --- a/luxtronik/collections.py +++ b/luxtronik/collections.py @@ -72,13 +72,14 @@ def unpack_values(packed, count, num_bits, reverse=True): return values -def get_data_arr(definition, field): +def get_data_arr(definition, field, num_bits): """ Normalize the field's data to a list of the correct size. Args: definition (LuxtronikDefinition): Meta-data of the field. field (Base): Field object that contains data to get. + num_bits (int): Number of bits per register. Returns: list[int] | None: List of length `definition.count`, @@ -88,15 +89,15 @@ def get_data_arr(definition, field): if data is None: return None should_unpack = field.concatenate_multiple_data_chunks \ - and definition.reg_bits > 0 and definition.count > 1 + and definition.count > 1 if should_unpack and not isinstance(data, list): # Usually big-endian (reverse=True) is used - data = unpack_values(data, definition.count, definition.reg_bits) + data = unpack_values(data, definition.count, num_bits) if not isinstance(data, list): data = [data] return data if len(data) == definition.count else None -def integrate_data(definition, field, raw_data, data_offset=-1): +def integrate_data(definition, field, raw_data, num_bits, data_offset=-1): """ Integrate raw values from a data array into the field. @@ -104,6 +105,7 @@ def integrate_data(definition, field, raw_data, data_offset=-1): definition (LuxtronikDefinition): Meta-data of the field. field (Base): Field object where to integrate the data. raw_data (list): Source array of bytes/words. + num_bits (int): Number of bits per register. data_offset (int): Optional offset. Defaults to `definition.index`. """ # Use data_offset if provided, otherwise the index @@ -116,11 +118,10 @@ def integrate_data(definition, field, raw_data, data_offset=-1): else: raw = raw_data[data_offset : data_offset + definition.count] raw = raw if len(raw) == definition.count else None - should_pack = field.concatenate_multiple_data_chunks \ - and definition.reg_bits > 0 # and definition.count > 1 + should_pack = field.concatenate_multiple_data_chunks if should_pack and raw is not None : # Usually big-endian (reverse=True) is used - raw = pack_values(raw, definition.reg_bits) + raw = pack_values(raw, num_bits) raw = raw if definition.check_raw_not_none(raw) else None field.raw = raw @@ -161,24 +162,28 @@ def addr(self): def count(self): return self.definition.count - def get_data_arr(self): + def get_data_arr(self, num_bits): """ Normalize the field's data to a list of the correct size. + Args: + num_bits (int): Number of bits per chunk. + Returns: list[int] | None: List of length `definition.count`, or None if insufficient. """ - return get_data_arr(self.definition, self.field) + return get_data_arr(self.definition, self.field, num_bits) - def integrate_data(self, raw_data, data_offset=-1): + def integrate_data(self, raw_data, num_bits, data_offset=-1): """ Integrate the related parts of the `raw_data` into the field Args: raw_data (list): Source array of register values. data_offset (int): Optional offset. Defaults to `definition.index`. + num_bits (int): Number of bits per chunk. """ - integrate_data(self.definition, self.field, raw_data, data_offset) + integrate_data(self.definition, self.field, raw_data, num_bits, data_offset) ############################################################################### # Field dictionary for data vectors diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index d3762641..e3426105 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -175,13 +175,14 @@ def register_alias(self, def_field_name_or_idx, alias): # Parse methods ############################################################### - def parse(self, raw_data): + def parse(self, raw_data, num_bits): """ Parse raw data into the corresponding fields. Args: raw_data (list[int]): List of raw register values. The raw data must start at register index 0. + num_bits (int): Number of bits per register. """ raw_len = len(raw_data) undefined = {i for i in range(0, raw_len)} @@ -193,7 +194,7 @@ def parse(self, raw_data): continue for index in range(definition.index, next_idx): undefined.discard(index) - pair.integrate_data(raw_data) + pair.integrate_data(raw_data, num_bits) # create an unknown field for additional data for index in undefined: # LOGGER.warning(f"Entry '%d' not in list of {self.name}", index) diff --git a/luxtronik/definitions/__init__.py b/luxtronik/definitions/__init__.py index d92090c4..ed3b14c5 100644 --- a/luxtronik/definitions/__init__.py +++ b/luxtronik/definitions/__init__.py @@ -164,11 +164,6 @@ def writeable(self): def num_bits(self): return self._num_bits - @property - def reg_bits(self): - """Return the number of bits per register.""" - return self._num_bits // self._count - @property def names(self): return self._names diff --git a/luxtronik/shi/contiguous.py b/luxtronik/shi/contiguous.py index 52ec1f8d..a8de5dc2 100644 --- a/luxtronik/shi/contiguous.py +++ b/luxtronik/shi/contiguous.py @@ -7,6 +7,7 @@ import logging from luxtronik.collections import LuxtronikDefFieldPair +from luxtronik.shi.constants import LUXTRONIK_SHI_REGISTER_BIT_SIZE LOGGER = logging.getLogger(__name__) @@ -170,7 +171,7 @@ def integrate_data(self, data_arr): first = self.first_index for part in self._parts: data_offset = part.index - first - part.integrate_data(data_arr, data_offset) + part.integrate_data(data_arr, data_offset, LUXTRONIK_SHI_REGISTER_BIT_SIZE) return True From 2237f72c67ae4859ac06f1e63e1c1b47db82a914 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Thu, 22 Jan 2026 21:34:31 +0100 Subject: [PATCH 18/61] wip --- luxtronik/collections.py | 77 ++++++++++++++---------- luxtronik/data_vector.py | 112 ++++++++++++++++++++++------------- luxtronik/shi/interface.py | 6 +- luxtronik/shi/vector.py | 2 +- tests/shi/test_shi_vector.py | 2 +- tests/test_collections.py | 4 +- 6 files changed, 125 insertions(+), 78 deletions(-) diff --git a/luxtronik/collections.py b/luxtronik/collections.py index 3c9eebe9..1825f681 100644 --- a/luxtronik/collections.py +++ b/luxtronik/collections.py @@ -167,7 +167,7 @@ def get_data_arr(self, num_bits): Normalize the field's data to a list of the correct size. Args: - num_bits (int): Number of bits per chunk. + num_bits (int): Number of bits per register. Returns: list[int] | None: List of length `definition.count`, or None if insufficient. @@ -181,7 +181,7 @@ def integrate_data(self, raw_data, num_bits, data_offset=-1): Args: raw_data (list): Source array of register values. data_offset (int): Optional offset. Defaults to `definition.index`. - num_bits (int): Number of bits per chunk. + num_bits (int): Number of bits per register. """ integrate_data(self.definition, self.field, raw_data, num_bits, data_offset) @@ -191,13 +191,12 @@ 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. """ def __init__(self): - # There may be several names or alias that points to one definition. + # There may be several names or alias that points to one definition and field. # So in order to spare memory we split the name/index-to-field-lookup # into a name/index-to-definition-lookup and a definition-to-field-lookup self._def_lookup = LuxtronikDefinitionsDictionary() @@ -207,24 +206,31 @@ def __init__(self): self._pairs = [] # list of LuxtronikDefFieldPair def __getitem__(self, def_field_name_or_idx): + """ + Array-style access to method `get`. + Please check its documentation. + """ 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): """ Check whether the data vector contains a name, index, or definition matching an added field, or the field itself. + If `def_field_name_or_idx` + - is a definition -> check whether a field with this definition has been added + - is a field -> check whether this field has been added + - is a name -> check whether a field with this name has been added + - is a idx -> check whether a field with this index has been added + Args: def_field_name_or_idx (LuxtronikDefinition | Base | str | int): Definition object, field object, field name or register index. @@ -241,33 +247,34 @@ 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 def def_dict(self): - """Return the internal definition dictionary, containing all added definitions""" + """ + Return the internal definition dictionary, + containing all definitions related to the added fields. + """ return self._def_lookup + @property + def field_dict(self): + """ + Return the internal field dictionary, + containing all added fields. + """ + return self._field_lookup + def add(self, definition, field, alias=None): """ Add a definition-field-pair to the internal dictionaries. @@ -276,6 +283,8 @@ def add(self, definition, field, alias=None): definition (LuxtronikDefinition): Definition related to the field. field (Base): Field to add. alias (Hashable | None): Alias, which can be used to access the field again. + + Note: Only use this method if the definitions order is already correct. """ if definition.valid: self._def_lookup.add(definition, alias) @@ -311,8 +320,8 @@ def register_alias(self, def_field_name_or_idx, alias): Base | None: The field to which the alias was added, or None if not possible """ - # Resolve a field input def_name_or_idx = def_field_name_or_idx + # Resolve a field argument if isinstance(def_name_or_idx, Base): def_name_or_idx = def_name_or_idx.name # register alias @@ -323,7 +332,13 @@ def register_alias(self, def_field_name_or_idx, alias): def get(self, def_field_name_or_idx, default=None): """ - Retrieve a field by definition, name or register index, or the field itself. + Retrieve an added field by definition, name or register index, or the field itself. + + If `def_field_name_or_idx` + - is a definition -> lookup the field by the definition + - is a field -> lookup the field by the field's name + - is a name -> lookup the field by the name + - is a idx -> lookup the field by the index Args: def_field_name_or_idx (LuxtronikDefinition | Base | str | int): diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index e3426105..c70bc2ef 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -45,7 +45,7 @@ def create_unknown_field(cls, idx): idx (int): Register index. Returns: - Unknown: A field instance of type 'Unknown'. + Unknown: A field instance of type `Unknown`. """ return Unknown(f"unknown_{cls.name}_{idx}", False) @@ -112,63 +112,68 @@ def __init__(self): @property def data(self): + """ + Return the internal `LuxtronikFieldsDictionary`. + Please check its documentation. + """ return self._data def __getitem__(self, def_name_or_idx): + """ + Array-style access to method `get`. + Please check its documentation. + """ return self.get(def_name_or_idx) def __setitem__(self, def_name_or_idx, value): + """ + Array-style access to method `set`. + Please check its documentation. + """ return self.set(def_name_or_idx, value) def __len__(self): + """ + Forward the `LuxtronikFieldsDictionary.__len__` method. + Please check its documentation. + """ return len(self._data) def __iter__(self): + """ + Forward the `LuxtronikFieldsDictionary.__iter__` method. + Please check its documentation. + """ return iter(self._data) def __contains__(self, def_field_name_or_idx): """ - Check whether the data vector contains a name, index, - or definition matching an added field, or the field itself. - - If `def_field_name_or_idx` - - is a definition -> check whether a field with this definition has been added - - is a field -> check whether this field has been added - - is a name -> check whether a field with this name has been added - - is a idx -> check whether a field with this index has been added - - Args: - def_field_name_or_idx (LuxtronikDefinition | Base | str | int): - Definition object, field object, field name or register index. - - Returns: - True if the searched element was found, otherwise False. + Forward the `LuxtronikFieldsDictionary.__contains__` method. + Please check its documentation. """ return def_field_name_or_idx in self._data def values(self): - return iter(self._data.values()) + """ + Forward the `LuxtronikFieldsDictionary.values` method. + Please check its documentation. + """ + return self._data.values() def items(self): - return iter(self._data.items()) + """ + Forward the `LuxtronikFieldsDictionary.items` method. + Please check its documentation. + """ + return self._data.items() # Alias methods ############################################################### def register_alias(self, def_field_name_or_idx, alias): """ - Add an alternative name (or anything hashable else) - that can be used to access a specific field. - - Args: - def_field_name_or_idx (LuxtronikDefinition | Base | str | int): - Field to which the alias is to be added. - Either by definition, name, register index, or the field itself. - alias (Hashable): Alias, which can be used to access the field again. - - Returns: - Base | None: The field to which the alias was added, - or None if not possible + Forward the `LuxtronikFieldsDictionary.register_alias` method. + Please check its documentation. """ return self._data.register_alias(def_field_name_or_idx, alias) @@ -185,16 +190,22 @@ def parse(self, raw_data, num_bits): num_bits (int): Number of bits per register. """ raw_len = len(raw_data) + # Prepare a list of undefined indices undefined = {i for i in range(0, raw_len)} - for pair in self._data.pairs(): + + # integrate the data into the fields + for pair in self._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 continue + # remove all used indices from the list of undefined indices for index in range(definition.index, next_idx): undefined.discard(index) pair.integrate_data(raw_data, num_bits) + # create an unknown field for additional data for index in undefined: # LOGGER.warning(f"Entry '%d' not in list of {self.name}", index) @@ -210,6 +221,12 @@ def _get_definition(self, def_field_name_or_idx, all_not_version_dependent): """ Look-up a definition by name, index, a field instance or by the definition itself. + If `def_field_name_or_idx` + - is a definition -> lookup the definition by the definition's name + - is a field -> lookup the definition by the field's name + - is a name -> lookup the field by the name + - is a idx -> lookup the field by the index + Args: def_field_name_or_idx (LuxtronikDefinition | Base | str | int): Definition object, field object, field name or register index. @@ -226,24 +243,31 @@ def _get_definition(self, def_field_name_or_idx, all_not_version_dependent): definition = def_field_name_or_idx field = None if isinstance(def_field_name_or_idx, Base): + # In case we got a field, search for the description by the field name definition = def_field_name_or_idx.name field = def_field_name_or_idx if not isinstance(def_field_name_or_idx, LuxtronikDefinition): if all_not_version_dependent: + # definitions contains all available definitions definition = self.definitions.get(definition) else: # _data.def_dict contains only valid and previously added definitions definition = self._data.def_dict.get(definition) return definition, field - - - def get(self, def_name_or_idx, default=None): + def get(self, def_field_name_or_idx, default=None): """ - Retrieve a field by definition, name or register index. + Retrieve an added field by definition, field, name or register index. + Triggers a key error when we try to query obsolete fields. + + If `def_field_name_or_idx` + - is a definition -> lookup the field by the definition + - is a field -> lookup the field by the field's name + - is a name -> lookup the field by the name + - is a idx -> lookup the field by the index Args: - def_name_or_idx (LuxtronikDefinition | str | int): + def_field_name_or_idx (LuxtronikDefinition | Base | str | int): Definition, name, or register index to be used to search for the field. Returns: @@ -253,12 +277,14 @@ def get(self, def_name_or_idx, default=None): If multiple fields added for the same index/name, the last added takes precedence. """ - obsolete_entry = self._obsolete.get(def_name_or_idx, None) + # check for obsolete + obsolete_entry = self._obsolete.get(def_field_name_or_idx, None) if obsolete_entry: - raise KeyError(f"The name '{def_name_or_idx}' is obsolete! Use '{obsolete_entry}' instead.") - field = self._data.get(def_name_or_idx, default) + raise KeyError(f"The name '{def_field_name_or_idx}' is obsolete! Use '{obsolete_entry}' instead.") + # look-up the field + field = self._data.get(def_field_name_or_idx, default) if field is None: - LOGGER.warning(f"entry '{def_name_or_idx}' not found") + LOGGER.warning(f"entry '{def_field_name_or_idx}' not found") return field def set(self, def_field_name_or_idx, value): @@ -268,6 +294,12 @@ def set(self, def_field_name_or_idx, value): The value is set, even if the field marked as non-writeable. No data validation is performed either. + If `def_field_name_or_idx` + - is a definition -> lookup the field by the definition and set the value + - is a field -> set the value of this field + - is a name -> lookup the field by the name and set the value + - is a idx -> lookup the field by the index and set the value + Args: def_field_name_or_idx (LuxtronikDefinition | Base | int | str): Definition, name, or register index to be used to search for the field. diff --git a/luxtronik/shi/interface.py b/luxtronik/shi/interface.py index 66977abc..b33b75fa 100644 --- a/luxtronik/shi/interface.py +++ b/luxtronik/shi/interface.py @@ -466,12 +466,12 @@ def _collect_fields(self, blocks_list, data_vector, definitions, read_not_write) # Trial-and-error mode: Add a block for every field blocks = ContiguousDataBlockList(definitions.name, read_not_write) if (read_not_write == READ): - for definition, field in data_vector.data.pairs(): + for definition, field in data_vector.data.items(): # _prepare_read_field will never fail, no need to call it #if self._prepare_read_field(definition, field): blocks.append_single(definition, field) else: - for definition, field in data_vector.data.pairs(): + for definition, field in data_vector.data.items(): if self._prepare_write_field(definition, field, data_vector.safe, None): blocks.append_single(definition, field) if len(blocks) > 0: @@ -485,7 +485,7 @@ def _collect_fields(self, blocks_list, data_vector, definitions, read_not_write) else: blocks = ContiguousDataBlockList(definitions.name, read_not_write) # Organize data into contiguous blocks - for definition, field in data_vector.data.pairs(): + for definition, field in data_vector.data.items(): if self._prepare_write_field(definition, field, data_vector.safe, None): blocks.collect(definition, field) if len(blocks) > 0: diff --git a/luxtronik/shi/vector.py b/luxtronik/shi/vector.py index 2b23f298..9acceab0 100644 --- a/luxtronik/shi/vector.py +++ b/luxtronik/shi/vector.py @@ -84,7 +84,7 @@ def update_read_blocks(self): """ if not self._read_blocks_up_to_date: self._read_blocks.clear() - for definition, field in self._data.pairs(): + for definition, field in self._data.items(): self._read_blocks.collect(definition, field) self._read_blocks_up_to_date = True diff --git a/tests/shi/test_shi_vector.py b/tests/shi/test_shi_vector.py index faeff419..91f07238 100644 --- a/tests/shi/test_shi_vector.py +++ b/tests/shi/test_shi_vector.py @@ -92,7 +92,7 @@ def test_create(self): data_vector = DataVectorTest(parse_version("1.2")) assert data_vector.version == (1, 2, 0, 0) assert len(data_vector) == 2 - assert len(data_vector._data.pairs()) == 3 + assert len(data_vector._data.items()) == 3 assert not data_vector._read_blocks_up_to_date assert len(data_vector._read_blocks) == 0 diff --git a/tests/test_collections.py b/tests/test_collections.py index df3d59d4..3c6a7a84 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -263,7 +263,7 @@ def test_init(self): def test_add(self): d = LuxtronikFieldsDictionary() assert len(d) == 0 - assert len(d.pairs()) == 0 + assert len(d.items()) == 0 u = LuxtronikDefinition.unknown(1, "test", 0) f = u.create_field() @@ -314,7 +314,7 @@ def test_len(self): d, _, _ = self.create_instance() # 3 different indices assert len(d) == 3 - assert len(d.pairs()) == 4 + assert len(d.items()) == 4 def test_get_contains(self): d, u, f = self.create_instance() From edae33c99f6f9e39c5c9e0b782405a911e8f6acd Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Thu, 22 Jan 2026 21:59:38 +0100 Subject: [PATCH 19/61] wip --- tests/test_collections.py | 216 ++++++++++++++++++-------------------- 1 file changed, 102 insertions(+), 114 deletions(-) diff --git a/tests/test_collections.py b/tests/test_collections.py index 3c6a7a84..3dee3e0c 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -40,95 +40,75 @@ def test_data_arr(self): # get from value definition._count = 1 - definition._num_bits = 32 - definition._data_type = 'INT32' field.raw = 5 - arr = get_data_arr(definition, field) + arr = get_data_arr(definition, field, 32) assert arr == [5] - assert arr == pair.get_data_arr() + assert arr == pair.get_data_arr(32) # get from value definition._count = 1 - definition._num_bits = 16 - definition._data_type = 'INT16' field.raw = 5 - arr = get_data_arr(definition, field) + arr = get_data_arr(definition, field, 16) assert arr == [5] - assert arr == pair.get_data_arr() + assert arr == pair.get_data_arr(16) # get from array definition._count = 2 - definition._num_bits = 64 - definition._data_type = 'INT64' field.raw = [7, 3] - arr = get_data_arr(definition, field) + arr = get_data_arr(definition, field, 32) assert arr == [7, 3] - assert arr == pair.get_data_arr() + assert arr == pair.get_data_arr(32) # get from array definition._count = 2 - definition._num_bits = 32 - definition._data_type = 'INT32' field.raw = [7, 3] - arr = get_data_arr(definition, field) + arr = get_data_arr(definition, field, 16) assert arr == [7, 3] - assert arr == pair.get_data_arr() + assert arr == pair.get_data_arr(16) # too much data definition._count = 2 - definition._num_bits = 32 - definition._data_type = 'INT32' field.raw = [4, 8, 1] - arr = get_data_arr(definition, field) + arr = get_data_arr(definition, field, 16) assert arr is None - assert arr == pair.get_data_arr() + assert arr == pair.get_data_arr(16) # insufficient data definition._count = 2 - definition._num_bits = 32 - definition._data_type = 'INT32' field.raw = [9] - arr = get_data_arr(definition, field) + arr = get_data_arr(definition, field, 16) assert arr is None - assert arr == pair.get_data_arr() + assert arr == pair.get_data_arr(16) field.concatenate_multiple_data_chunks = True # get from array definition._count = 2 - definition._num_bits = 64 - definition._data_type = 'INT64' field.raw = 0x00000007_00000003 - arr = get_data_arr(definition, field) + arr = get_data_arr(definition, field, 32) assert arr == [7, 3] - assert arr == pair.get_data_arr() + assert arr == pair.get_data_arr(32) # get from array definition._count = 2 - definition._num_bits = 32 - definition._data_type = 'INT32' field.raw = 0x0007_0003 - arr = get_data_arr(definition, field) + arr = get_data_arr(definition, field, 16) assert arr == [7, 3] - assert arr == pair.get_data_arr() + assert arr == pair.get_data_arr(16) # too much data definition._count = 2 - definition._num_bits = 32 - definition._data_type = 'INT32' field.raw = 0x0004_0008_0001 - arr = get_data_arr(definition, field) + arr = get_data_arr(definition, field, 16) assert arr == [8, 1] - assert arr == pair.get_data_arr() + assert arr == pair.get_data_arr(16) # insufficient data definition._count = 2 - definition._num_bits = 32 - definition._data_type = 'INT32' field.raw = 0x0009 - arr = get_data_arr(definition, field) + arr = get_data_arr(definition, field, 16) assert arr == [0, 9] - assert arr == pair.get_data_arr() + assert arr == pair.get_data_arr(16) def test_integrate(self): definition = LuxtronikDefinition.unknown(2, 'Foo', 30) @@ -140,110 +120,102 @@ def test_integrate(self): # set array definition._count = 2 - definition._num_bits = 64 definition._data_type = 'INT64' - integrate_data(definition, field, data) + integrate_data(definition, field, data, 32) assert field.raw == [3, 4] - pair.integrate_data(data, 4) + pair.integrate_data(data, 32, 4) assert field.raw == [5, 6] - integrate_data(definition, field, data, 7) + integrate_data(definition, field, data, 32, 7) assert field.raw is None - pair.integrate_data(data, 0) + pair.integrate_data(data, 32, 0) assert field.raw == [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] # set array definition._count = 2 - definition._num_bits = 32 definition._data_type = 'INT32' - integrate_data(definition, field, data) + integrate_data(definition, field, data, 16) assert field.raw == [3, 4] - pair.integrate_data(data, 4) + pair.integrate_data(data, 16, 4) assert field.raw == [5, 6] - integrate_data(definition, field, data, 7) + integrate_data(definition, field, data, 16, 7) assert field.raw is None - pair.integrate_data(data, 0) + pair.integrate_data(data, 16, 0) assert field.raw == [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] # set value definition._count = 1 - definition._num_bits = 32 definition._data_type = 'INT32' - integrate_data(definition, field, data) + integrate_data(definition, field, data, 32) assert field.raw == 3 - pair.integrate_data(data, 5) + pair.integrate_data(data, 32, 5) assert field.raw == 6 - integrate_data(definition, field, data, 9) + integrate_data(definition, field, data, 32, 9) assert field.raw is None - pair.integrate_data(data, 1) + pair.integrate_data(data, 32, 1) # Currently there is no magic "not available" value for 32 bit values -> not None # This applies also to similar lines below assert field.raw == LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE # set value definition._count = 1 - definition._num_bits = 16 definition._data_type = 'INT16' - integrate_data(definition, field, data) + integrate_data(definition, field, data, 16) assert field.raw == 3 - pair.integrate_data(data, 5) + pair.integrate_data(data, 16, 5) assert field.raw == 6 - integrate_data(definition, field, data, 9) + integrate_data(definition, field, data, 16, 9) assert field.raw is None - pair.integrate_data(data, 1) + pair.integrate_data(data, 16, 1) assert field.raw is None field.concatenate_multiple_data_chunks = True # set array definition._count = 2 - definition._num_bits = 64 definition._data_type = 'INT64' - integrate_data(definition, field, data) + integrate_data(definition, field, data, 32) assert field.raw == 0x00000003_00000004 - pair.integrate_data(data, 4) + pair.integrate_data(data, 32, 4) assert field.raw == 0x00000005_00000006 - integrate_data(definition, field, data, 7) + integrate_data(definition, field, data, 32, 7) assert field.raw is None - pair.integrate_data(data, 0) + pair.integrate_data(data, 32, 0) assert field.raw == 0x00000001_00007FFF # set array definition._count = 2 - definition._num_bits = 32 definition._data_type = 'INT32' - integrate_data(definition, field, data) + integrate_data(definition, field, data, 16) assert field.raw == 0x0003_0004 - pair.integrate_data(data, 4) + pair.integrate_data(data, 16, 4) assert field.raw == 0x0005_0006 - integrate_data(definition, field, data, 7) + integrate_data(definition, field, data, 16, 7) assert field.raw is None - pair.integrate_data(data, 0) + pair.integrate_data(data, 16, 0) assert field.raw == 0x0001_7FFF # set value definition._count = 1 - definition._num_bits = 32 definition._data_type = 'INT32' - integrate_data(definition, field, data) + integrate_data(definition, field, data, 32) assert field.raw == 0x00000003 - pair.integrate_data(data, 5) + pair.integrate_data(data, 32, 5) assert field.raw == 0x00000006 - integrate_data(definition, field, data, 9) + integrate_data(definition, field, data, 32, 9) assert field.raw is None - pair.integrate_data(data, 1) + pair.integrate_data(data, 32, 1) assert field.raw == 0x00007FFF # set value definition._count = 1 - definition._num_bits = 16 definition._data_type = 'INT16' - integrate_data(definition, field, data) + integrate_data(definition, field, data, 16) assert field.raw == 0x0003 - pair.integrate_data(data, 5) + pair.integrate_data(data, 16, 5) assert field.raw == 0x0006 - integrate_data(definition, field, data, 9) + integrate_data(definition, field, data, 16, 9) assert field.raw is None - pair.integrate_data(data, 1) + pair.integrate_data(data, 16, 1) assert field.raw is None field.concatenate_multiple_data_chunks = False @@ -255,39 +227,42 @@ def test_init(self): d = LuxtronikFieldsDictionary() assert type(d._def_lookup) is LuxtronikDefinitionsDictionary + assert d.def_dict is d._def_lookup assert type(d._field_lookup) is dict - assert len(d._field_lookup.values()) == 0 + assert d.field_dict is d._field_lookup assert type(d._pairs) is list - assert len(d._pairs) == 0 + assert d.pairs is d._pairs + assert len(d.field_dict) == 0 + assert len(d.pairs) == 0 def test_add(self): d = LuxtronikFieldsDictionary() assert len(d) == 0 - assert len(d.items()) == 0 + assert len(d.pairs) == 0 u = LuxtronikDefinition.unknown(1, "test", 0) f = u.create_field() d.add(u, f) assert len(d) == 1 - assert len(d._pairs) == 1 - assert d._pairs[0].definition is u - assert d._pairs[0].field is f + assert len(d.pairs) == 1 + assert d.pairs[0].definition is u + assert d.pairs[0].field is f u = LuxtronikDefinition.unknown(2, "test", 0) f = u.create_field() d.add(u, f) assert len(d) == 2 - assert len(d._pairs) == 2 - assert d._pairs[1].definition is u - assert d._pairs[1].field is f + assert len(d.pairs) == 2 + assert d.pairs[1].definition is u + assert d.pairs[1].field is f u = LuxtronikDefinition.unknown(0, "test", 0) f = u.create_field() d.add_sorted(u, f) assert len(d) == 3 assert len(d._pairs) == 3 - assert d._pairs[0].definition is u - assert d._pairs[0].field is f + assert d.pairs[0].definition is u + assert d.pairs[0].field is f def create_instance(self): d = LuxtronikFieldsDictionary() @@ -313,8 +288,8 @@ def create_instance(self): def test_len(self): d, _, _ = self.create_instance() # 3 different indices - assert len(d) == 3 - assert len(d.items()) == 4 + assert len(d) == 4 + assert len(d.pairs) == 4 def test_get_contains(self): d, u, f = self.create_instance() @@ -340,42 +315,55 @@ def test_get_contains(self): def test_iter(self): d, _, _ = self.create_instance() - for idx, key in enumerate(d): + for idx, d in enumerate(d): if idx == 0: - assert key == 1 + assert d.name == "unknown_test_1" + assert d.index == 1 if idx == 1: - assert key == 2 + assert d.name == "unknown_test_2" + assert d.index == 2 if idx == 2: - assert key == 3 + assert d.name == "base2" + assert d.index == 2 + if idx == 3: + assert d.name == "base3" + assert d.index == 3 def test_values(self): d, _, _ = self.create_instance() - for idx, value in enumerate(d.values()): + for idx, f in enumerate(d.values()): if idx == 0: - assert type(value) is Unknown - assert value.name == "unknown_test_1" + assert type(f) is Unknown + assert f.name == "unknown_test_1" if idx == 1: - assert type(value) is Base - assert value.name == "base2" + assert type(f) is Unknown + assert f.name == "unknown_test_2" if idx == 2: - assert type(value) is Base - assert value.name == "base3" + assert type(f) is Base + assert f.name == "base2" + if idx == 3: + assert type(f) is Base + assert f.name == "base3" def test_items(self): d, _, _ = self.create_instance() - for idx, (key, value) in enumerate(d.items()): + for idx, (d, f) in enumerate(d.items()): if idx == 0: - assert key == 1 - assert type(value) is Unknown - assert value.name == "unknown_test_1" + assert d.index == 1 + assert type(f) is Unknown + assert f.name == "unknown_test_1" if idx == 1: - assert key == 2 - assert type(value) is Base - assert value.name == "base2" + assert d.index == 2 + assert type(f) is Unknown + assert f.name == "unknown_test_2" if idx == 2: - assert key == 3 - assert type(value) is Base - assert value.name == "base3" + assert d.index == 2 + assert type(f) is Base + assert f.name == "base2" + if idx == 3: + assert d.index == 3 + assert type(f) is Base + assert f.name == "base3" class MyTestClass: pass From 7317bb99cd7484bed54b3fc1a001e30fec040c1e Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Thu, 22 Jan 2026 23:07:05 +0100 Subject: [PATCH 20/61] wip --- luxtronik/cfi/interface.py | 54 +++++------------------------- luxtronik/data_vector.py | 4 +-- luxtronik/scripts/__init__.py | 14 ++++---- luxtronik/shi/contiguous.py | 4 +-- tests/cfi/test_cfi_calculations.py | 9 ++--- tests/cfi/test_cfi_parameters.py | 33 ++++++++++++++---- tests/cfi/test_cfi_visibilities.py | 9 ++--- tests/fake/fake_luxtronik.py | 12 +++---- tests/shi/test_shi_contiguous.py | 27 ++++++++------- tests/shi/test_shi_interface.py | 4 +-- tests/shi/test_shi_vector.py | 54 +++++++++++++++++++++++++----- tests/test_compatibility.py | 6 ++-- tests/test_socket_interaction.py | 9 ++--- 13 files changed, 132 insertions(+), 107 deletions(-) diff --git a/luxtronik/cfi/interface.py b/luxtronik/cfi/interface.py index 7ddcc662..7750d099 100644 --- a/luxtronik/cfi/interface.py +++ b/luxtronik/cfi/interface.py @@ -162,20 +162,20 @@ def _write_and_read(self, parameters, data): return self._read(data) def _write(self, parameters): - for index, field in parameters.items(): + for definition, field in parameters.items(): if field.write_pending: value = field.raw - if not isinstance(index, int) or not isinstance(value, int): + if not isinstance(definition.index, int) or not isinstance(value, int): LOGGER.warning( "%s: Parameter id '%s' or value '%s' invalid!", self._host, - index, + definition.index, value, ) field.write_pending = False continue - LOGGER.info("%s: Parameter '%d' set to '%s'", self._host, index, value) - self._send_ints(LUXTRONIK_PARAMETERS_WRITE, index, value) + 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() @@ -194,7 +194,7 @@ def _read_parameters(self, parameters): for _ in range(0, length): data.append(self._read_int()) LOGGER.info("%s: Read %d parameters", self._host, length) - self._parse(parameters, data) + parameters.parse(data, LUXTRONIK_CFI_REGISTER_BIT_SIZE) return parameters def _read_calculations(self, calculations): @@ -209,7 +209,7 @@ def _read_calculations(self, calculations): for _ in range(0, length): data.append(self._read_int()) LOGGER.info("%s: Read %d calculations", self._host, length) - self._parse(calculations, data) + calculations.parse(data, LUXTRONIK_CFI_REGISTER_BIT_SIZE) return calculations def _read_visibilities(self, visibilities): @@ -222,7 +222,7 @@ def _read_visibilities(self, visibilities): for _ in range(0, length): data.append(self._read_char()) LOGGER.info("%s: Read %d visibilities", self._host, length) - self._parse(visibilities, data) + visibilities.parse(data, LUXTRONIK_CFI_REGISTER_BIT_SIZE) return visibilities def _send_ints(self, *ints): @@ -259,40 +259,4 @@ def _read_int(self): def _read_char(self): "Low-level helper to receive a signed int" reading = self._read_bytes(LUXTRONIK_SOCKET_READ_SIZE_CHAR) - return struct.unpack(">b", reading)[0] - - def _parse(self, data_vector, raw_data): - """ - Parse raw data into the corresponding fields. - - Args: - data_vector (DataVector): Data vector in which - the raw data is to be integrated. - raw_data (list[int]): List of raw register values. - The raw data must start at register index 0. - """ - raw_len = len(raw_data) - # Prepare a list of undefined indices - undefined = {i for i in range(0, raw_len)} - - # integrate the data into the fields - for pair in data_vector.data.pairs(): - 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 - continue - # remove all used indices from the list of undefined indices - for index in range(definition.index, next_idx): - undefined.discard(index) - pair.integrate_data(raw_data, LUXTRONIK_CFI_REGISTER_BIT_SIZE) - - # create an unknown field for additional data - for index in undefined: - # LOGGER.warning(f"Entry '%d' not in list of {self.name}", index) - definition = data_vector.definitions.create_unknown_definition(index) - field = definition.create_field() - field.raw = raw_data[index] - data_vector.data.add_sorted(definition, field) + return struct.unpack(">b", reading)[0] \ No newline at end of file diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index c70bc2ef..5da6c9e9 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -165,7 +165,7 @@ def items(self): Forward the `LuxtronikFieldsDictionary.items` method. Please check its documentation. """ - return self._data.items() + return iter(self._data.items()) # Alias methods ############################################################### @@ -198,7 +198,7 @@ def parse(self, raw_data, num_bits): definition, field = pair # skip this field if there are not enough data next_idx = definition.index + definition.count - if next_idx >= raw_len: + if next_idx > raw_len: # not enough registers continue # remove all used indices from the list of undefined indices diff --git a/luxtronik/scripts/__init__.py b/luxtronik/scripts/__init__.py index c9072d23..86e259bf 100644 --- a/luxtronik/scripts/__init__.py +++ b/luxtronik/scripts/__init__.py @@ -25,8 +25,8 @@ def print_dump_row(number, field): def dump_fields(data_vector): print_dump_header(f"{data_vector.name}s") - for index, field in data_vector.data.items(): - print_dump_row(index, field) + for definition, field in data_vector.data.items(): + print_dump_row(definition.index, field) def print_watch_header(screen, caption): cols, _ = screen.get_visible_size() @@ -41,11 +41,11 @@ def get_watch_row(short_name, number, prev_field, this_field): return text def update_changes(changes, prev_data_vector, this_data_vector): - for index, this_field in this_data_vector.data.items(): + for definition, this_field in this_data_vector.data.items(): short_name = this_data_vector.name[:4] - key = f"{short_name}_{str(index).zfill(5)}" - prev_field = prev_data_vector.get(index) + key = f"{short_name}_{str(definition.index).zfill(5)}" + prev_field = prev_data_vector.get(definition.name) if this_field.raw != prev_field.raw: - changes[key] = get_watch_row(short_name, index, prev_field, this_field) + changes[key] = get_watch_row(short_name, definition.index, prev_field, this_field) elif key in changes: - changes[key] = get_watch_row(short_name, index, prev_field, None) + changes[key] = get_watch_row(short_name, definition.index, prev_field, None) diff --git a/luxtronik/shi/contiguous.py b/luxtronik/shi/contiguous.py index a8de5dc2..fcb6c500 100644 --- a/luxtronik/shi/contiguous.py +++ b/luxtronik/shi/contiguous.py @@ -171,7 +171,7 @@ def integrate_data(self, data_arr): first = self.first_index for part in self._parts: data_offset = part.index - first - part.integrate_data(data_arr, data_offset, LUXTRONIK_SHI_REGISTER_BIT_SIZE) + part.integrate_data(data_arr, LUXTRONIK_SHI_REGISTER_BIT_SIZE, data_offset) return True @@ -192,7 +192,7 @@ def get_data_arr(self): valid = True for part in self._parts: data_offset = part.index - first - data = part.get_data_arr() + data = part.get_data_arr(LUXTRONIK_SHI_REGISTER_BIT_SIZE) if data is None: valid = False diff --git a/tests/cfi/test_cfi_calculations.py b/tests/cfi/test_cfi_calculations.py index 3eda49c8..97f3bb01 100644 --- a/tests/cfi/test_cfi_calculations.py +++ b/tests/cfi/test_cfi_calculations.py @@ -4,6 +4,7 @@ from luxtronik import Calculations from luxtronik.datatypes import Base +from luxtronik.definitions import LuxtronikDefinition class TestCalculations: @@ -22,10 +23,10 @@ def test_data(self): # The Value must be a fields # The key can be an index assert isinstance(data[0], Base) - for k in data: - assert isinstance(k, int) + for d in data: + assert isinstance(d, LuxtronikDefinition) for v in data.values(): assert isinstance(v, Base) - for k, v in data.items(): - assert isinstance(k, int) + for d, v in data.items(): + assert isinstance(d, LuxtronikDefinition) assert isinstance(v, Base) diff --git a/tests/cfi/test_cfi_parameters.py b/tests/cfi/test_cfi_parameters.py index 74409097..e8c7c022 100644 --- a/tests/cfi/test_cfi_parameters.py +++ b/tests/cfi/test_cfi_parameters.py @@ -4,6 +4,8 @@ from luxtronik import Parameters from luxtronik.datatypes import Base +from luxtronik.definitions import LuxtronikDefinition +from luxtronik.cfi.constants import LUXTRONIK_CFI_REGISTER_BIT_SIZE class TestParameters: @@ -26,12 +28,12 @@ def test_data(self): # The Value must be a fields # The key can be an index assert isinstance(data[0], Base) - for k in data: - assert isinstance(k, int) + for d in data: + assert isinstance(d, LuxtronikDefinition) for v in data.values(): assert isinstance(v, Base) - for k, v in data.items(): - assert isinstance(k, int) + for d, v in data.items(): + assert isinstance(d, LuxtronikDefinition) assert isinstance(v, Base) def test_get(self): @@ -55,15 +57,32 @@ def test_get(self): j = 0.0 assert parameters.get(j) is None + def test_parse(self): + """Test cases for _parse""" + parameters = Parameters() + + n = 2000 + t = list(range(0, n + 1)) + parameters.parse(t, LUXTRONIK_CFI_REGISTER_BIT_SIZE) + + p = parameters.get(n) + + assert p.name == f"unknown_parameter_{n}" + assert p.raw == n + def test___iter__(self): """Test cases for __iter__""" parameters = Parameters() - for i, p in parameters: - if i == 0: + for d, p in parameters.items(): + if d.index == 0: assert p.name == "ID_Transfert_LuxNet" - elif i == 1: + assert d is parameters.data.def_dict.get(0) + assert p is parameters.get(0) + elif d.index == 1: assert p.name == "ID_Einst_WK_akt" + assert d is parameters.data.def_dict.get(1) + assert p is parameters.get(1) else: break diff --git a/tests/cfi/test_cfi_visibilities.py b/tests/cfi/test_cfi_visibilities.py index 697005bd..aa5cbc19 100644 --- a/tests/cfi/test_cfi_visibilities.py +++ b/tests/cfi/test_cfi_visibilities.py @@ -4,6 +4,7 @@ from luxtronik import Visibilities from luxtronik.datatypes import Base +from luxtronik.definitions import LuxtronikDefinition class TestVisibilities: @@ -22,10 +23,10 @@ def test_data(self): # The Value must be a fields # The key can be an index assert isinstance(data[0], Base) - for k in data: - assert isinstance(k, int) + for d in data: + assert isinstance(d, LuxtronikDefinition) for v in data.values(): assert isinstance(v, Base) - for k, v in data.items(): - assert isinstance(k, int) + for d, v in data.items(): + assert isinstance(d, LuxtronikDefinition) assert isinstance(v, Base) diff --git a/tests/fake/fake_luxtronik.py b/tests/fake/fake_luxtronik.py index 8aaf38bc..a3968f87 100644 --- a/tests/fake/fake_luxtronik.py +++ b/tests/fake/fake_luxtronik.py @@ -5,9 +5,9 @@ class FakeLuxtronik(Luxtronik): def __init__(self): LuxtronikAllData.__init__(self) - for idx, field in self.parameters: - field.raw = idx - for idx, field in self.calculations: - field.raw = idx - for idx, field in self.visibilities: - field.raw = idx \ No newline at end of file + for definition, field in self.parameters.items(): + field.raw = definition.idx + for definition, field in self.calculations.items(): + field.raw = definition.idx + for definition, field in self.visibilities.items(): + field.raw = definition.idx \ No newline at end of file diff --git a/tests/shi/test_shi_contiguous.py b/tests/shi/test_shi_contiguous.py index f74f5bf1..62a27d50 100644 --- a/tests/shi/test_shi_contiguous.py +++ b/tests/shi/test_shi_contiguous.py @@ -2,6 +2,7 @@ from luxtronik.constants import LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE from luxtronik.datatypes import Base from luxtronik.definitions import LuxtronikDefinition +from luxtronik.shi.constants import LUXTRONIK_SHI_REGISTER_BIT_SIZE from luxtronik.shi.contiguous import ( ContiguousDataPart, ContiguousDataBlock, @@ -73,49 +74,49 @@ def test_repr(self): def test_get_data(self): part = ContiguousDataPart(def_a, field_a) field_a.raw = [4, 2] - assert part.get_data_arr() == [4, 2] + assert part.get_data_arr(LUXTRONIK_SHI_REGISTER_BIT_SIZE) == [4, 2] field_a.raw = [1, 3, 5] - assert part.get_data_arr() is None + assert part.get_data_arr(LUXTRONIK_SHI_REGISTER_BIT_SIZE) is None field_a.raw = [9] - assert part.get_data_arr() is None + assert part.get_data_arr(LUXTRONIK_SHI_REGISTER_BIT_SIZE) is None part = ContiguousDataPart(def_a1, field_a1) field_a1.raw = [8] - assert part.get_data_arr() == [8] + assert part.get_data_arr(LUXTRONIK_SHI_REGISTER_BIT_SIZE) == [8] field_a1.raw = 7 - assert part.get_data_arr() == [7] + assert part.get_data_arr(LUXTRONIK_SHI_REGISTER_BIT_SIZE) == [7] def test_integrate_data(self): part = ContiguousDataPart(def_a, field_a) - part.integrate_data([1, 5, 7, 9], 0) + part.integrate_data([1, 5, 7, 9], LUXTRONIK_SHI_REGISTER_BIT_SIZE, 0) assert part.field.raw == [1, 5] - part.integrate_data([1, 5, 7, 9]) + part.integrate_data([1, 5, 7, 9], LUXTRONIK_SHI_REGISTER_BIT_SIZE) assert part.field.raw == [5, 7] - part.integrate_data([1, 5, 7, 9], 2) + part.integrate_data([1, 5, 7, 9], LUXTRONIK_SHI_REGISTER_BIT_SIZE, 2) assert part.field.raw == [7, 9] - part.integrate_data([1, 5, 7, 9], 3) + part.integrate_data([1, 5, 7, 9], LUXTRONIK_SHI_REGISTER_BIT_SIZE, 3) assert part.field.raw is None - part.integrate_data([1, 5, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, 9], 1) + part.integrate_data([1, 5, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, 9], LUXTRONIK_SHI_REGISTER_BIT_SIZE, 1) assert part.field.raw == [5, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] part = ContiguousDataPart(def_c1, field_c1) - part.integrate_data([2, 4, 6], 1) + part.integrate_data([2, 4, 6], LUXTRONIK_SHI_REGISTER_BIT_SIZE, 1) assert part.field.raw == 4 - part.integrate_data([2, 4, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE], 2) + part.integrate_data([2, 4, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE], LUXTRONIK_SHI_REGISTER_BIT_SIZE, 2) assert part.field.raw is None - part.integrate_data([2, 4, 6], 5) + part.integrate_data([2, 4, 6], LUXTRONIK_SHI_REGISTER_BIT_SIZE, 5) assert part.field.raw is None diff --git a/tests/shi/test_shi_interface.py b/tests/shi/test_shi_interface.py index dd1596a6..76fc209d 100644 --- a/tests/shi/test_shi_interface.py +++ b/tests/shi/test_shi_interface.py @@ -1244,7 +1244,7 @@ def check_definitions(self, interface): for d in definitions: assert d.name in vector assert d in interface.holdings - for f in vector: + for f in vector.values(): assert f.name in definitions assert f.name in interface.holdings @@ -1255,7 +1255,7 @@ def check_definitions(self, interface): for d in definitions: assert d.name in vector assert d in interface.inputs - for f in vector: + for f in vector.values(): assert f.name in definitions assert f.name in interface.inputs diff --git a/tests/shi/test_shi_vector.py b/tests/shi/test_shi_vector.py index 91f07238..614861fa 100644 --- a/tests/shi/test_shi_vector.py +++ b/tests/shi/test_shi_vector.py @@ -1,6 +1,7 @@ from luxtronik.common import parse_version from luxtronik.datatypes import Base, Unknown from luxtronik.definitions import LuxtronikDefinitionsList +from luxtronik.shi.constants import LUXTRONIK_SHI_REGISTER_BIT_SIZE from luxtronik.shi.vector import DataVectorSmartHome from luxtronik.shi.holdings import Holdings from luxtronik.shi.inputs import Inputs @@ -249,30 +250,40 @@ def test_iter(self): data_vector.add(5) data_vector.add(9) - for index, idx in enumerate(data_vector): + for index, definition in enumerate(data_vector): if index == 0: - assert idx == 5 + assert definition.idx == 5 + assert definition.name == "field_5" if index == 1: - assert idx == 9 # field_9 + assert definition.idx == 9 + assert definition.name == "field_9a" if index == 2: + assert definition.idx == 9 + assert definition.name == "field_9" + if index == 3: assert False for index, field in enumerate(data_vector.values()): if index == 0: assert field.name == 'field_5' if index == 1: - assert field.name == 'field_9' + assert field.name == 'field_9a' if index == 2: + assert field.name == 'field_9' + if index == 3: assert False - for index, (idx, field) in enumerate(data_vector.items()): + for index, (definition, field) in enumerate(data_vector.items()): if index == 0: - assert idx == 5 + assert definition.idx == 5 assert field.name == 'field_5' if index == 1: - assert idx == 9 - assert field.name == 'field_9' + assert definition.idx == 9 + assert field.name == 'field_9a' if index == 2: + assert definition.idx == 9 + assert field.name == 'field_9' + if index == 3: assert False def test_set(self): @@ -302,6 +313,33 @@ def test_set(self): assert field_9.value == 6 assert field_9.write_pending + def test_parse(self): + data_vector = DataVectorTest(parse_version("1.1.2")) + field_5 = data_vector[5] + field_9 = data_vector[9] + field_9a = data_vector['field_9a'] + + # not enough data + data = [1] + data_vector.parse(data, LUXTRONIK_SHI_REGISTER_BIT_SIZE) + assert field_5.value is None + assert field_9.value is None + assert field_9a.value is None + + # data only for field 5 + data = [1, 2, 3, 4, 5, 6, 7] + data_vector.parse(data, LUXTRONIK_SHI_REGISTER_BIT_SIZE) + assert field_5.value == 6 + assert field_9.value is None + assert field_9a.value is None + + # data for all fields + data = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0, -1, -2] + data_vector.parse(data, LUXTRONIK_SHI_REGISTER_BIT_SIZE) + assert field_5.value == 4 + assert field_9.value == [0, -1] + assert field_9a.value == 0 + def test_alias(self): TEST_DEFINITIONS.register_alias('field_9a', 10) data_vector = DataVectorTest(parse_version("1.1.2")) diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index d7b6c941..a2f79f46 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -1933,13 +1933,13 @@ def test_compatibilities(self): ok = True for mapping, obj, caption in values: print_caption = True - for cur_idx, entry in obj: - if entry.name not in mapping: + for definition, field in obj.items(): + if field.name not in mapping: # We do not use assert here, in order to catch all incompatibilities at once. # The output can be copied to the dicts above if print_caption: print(f"### Missing - {caption}:") print_caption = False - print(f'"{entry.name}": {cur_idx},') + print(f'"{field.name}": {definition.index},') ok = False assert ok, f"Found missing {obj.name}. Please consider to add them to the test suite." \ No newline at end of file diff --git a/tests/test_socket_interaction.py b/tests/test_socket_interaction.py index c6e50dbb..96948d5f 100644 --- a/tests/test_socket_interaction.py +++ b/tests/test_socket_interaction.py @@ -16,6 +16,7 @@ @mock.patch("socket.create_connection", fake_create_connection) @mock.patch("luxtronik.LuxtronikModbusTcpInterface", FakeModbus) class TestSocketInteraction: + def check_luxtronik_data(self, lux, check_for_true=True): cp = self.check_data_vector(lux.parameters) cc = self.check_data_vector(lux.calculations) @@ -32,8 +33,8 @@ def check_data_vector(self, data_vector): fct = fake_calculation_value elif type(data_vector) is Visibilities: fct = fake_visibility_value - for idx, entry in data_vector.items(): - if entry.raw != fct(idx): + for d, f in data_vector.items(): + if f.raw != fct(d.index): return False return True @@ -43,8 +44,8 @@ def clear_luxtronik_data(self, lux): self.clear_data_vector(lux.visibilities) def clear_data_vector(self, data_vector): - for idx, entry in data_vector.items(): - entry.raw = 0 + for d, f in data_vector.items(): + f.raw = 0 def test_luxtronik_socket_interface(self): host = "my_heatpump" From d867fde0fda1eeb7cfd431b90101276287a42337 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Thu, 22 Jan 2026 23:16:39 +0100 Subject: [PATCH 21/61] wip --- luxtronik/data_vector.py | 8 ++++++-- luxtronik/shi/interface.py | 7 +++++-- tests/fake/fake_luxtronik.py | 6 +++--- tests/shi/test_shi_vector.py | 24 ++++++++++++------------ 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index 5da6c9e9..fe7e1df9 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -69,7 +69,11 @@ def create_any_field(cls, def_name_or_idx): Returns: Base | None: The created field, or None if not found or not valid. """ - definition, _ = self._get_definition(def_name_or_idx, True) + if isinstance(def_name_or_idx, LuxtronikDefinition): + definition = def_name_or_idx + else: + # The definitions object hold all available definitions + definition = cls.definitions.get(def_name_or_idx) if definition is not None and definition.valid: return definition.create_field() return None @@ -91,7 +95,7 @@ class variable `cls.definitions` and is valid for `self.version`). Returns: Base | None: The created field, or None if not found or not valid. """ - definition, _ = self._get_definition(name_or_idx, False) + definition, _ = self._get_definition(def_name_or_idx, False) if definition is not None and definition.valid: return definition.create_field() return None diff --git a/luxtronik/shi/interface.py b/luxtronik/shi/interface.py index b33b75fa..2ff751e8 100644 --- a/luxtronik/shi/interface.py +++ b/luxtronik/shi/interface.py @@ -9,7 +9,10 @@ LuxtronikDefinition, LuxtronikDefinitionsList, ) -from luxtronik.shi.constants import LUXTRONIK_LATEST_SHI_VERSION +from luxtronik.shi.constants import ( + LUXTRONIK_LATEST_SHI_VERSION, + LUXTRONIK_SHI_REGISTER_BIT_SIZE +) from luxtronik.shi.common import ( LuxtronikSmartHomeReadHoldingsTelegram, LuxtronikSmartHomeReadInputsTelegram, @@ -387,7 +390,7 @@ def _prepare_write_field(self, definition, field, safe, data): field.value = data # Abort if insufficient data is provided - if not get_data_arr(definition, field): + if not get_data_arr(definition, field, LUXTRONIK_SHI_REGISTER_BIT_SIZE): LOGGER.warning("Data error / insufficient data provided: " \ + f"name={definition.name}, data={field.raw}") return False diff --git a/tests/fake/fake_luxtronik.py b/tests/fake/fake_luxtronik.py index a3968f87..bc9e639f 100644 --- a/tests/fake/fake_luxtronik.py +++ b/tests/fake/fake_luxtronik.py @@ -6,8 +6,8 @@ class FakeLuxtronik(Luxtronik): def __init__(self): LuxtronikAllData.__init__(self) for definition, field in self.parameters.items(): - field.raw = definition.idx + field.raw = definition.index for definition, field in self.calculations.items(): - field.raw = definition.idx + field.raw = definition.index for definition, field in self.visibilities.items(): - field.raw = definition.idx \ No newline at end of file + field.raw = definition.index \ No newline at end of file diff --git a/tests/shi/test_shi_vector.py b/tests/shi/test_shi_vector.py index 614861fa..5ef6d0fc 100644 --- a/tests/shi/test_shi_vector.py +++ b/tests/shi/test_shi_vector.py @@ -92,8 +92,8 @@ def test_create(self): # create versioned data vector data_vector = DataVectorTest(parse_version("1.2")) assert data_vector.version == (1, 2, 0, 0) - assert len(data_vector) == 2 - assert len(data_vector._data.items()) == 3 + assert len(data_vector) == 3 + assert len(data_vector._data.pairs) == 3 assert not data_vector._read_blocks_up_to_date assert len(data_vector._read_blocks) == 0 @@ -216,8 +216,8 @@ def test_add(self): def_9a = data_vector.definitions['field_9a'] field = data_vector.add(def_9a) assert def_9a in data_vector - assert len(data_vector) == 2 - assert len(data_vector.data._pairs) == 3 + assert len(data_vector) == 3 + assert len(data_vector.data.pairs) == 3 assert field.name == 'field_9a' # Get via index (last added) @@ -252,13 +252,13 @@ def test_iter(self): for index, definition in enumerate(data_vector): if index == 0: - assert definition.idx == 5 + assert definition.index == 5 assert definition.name == "field_5" if index == 1: - assert definition.idx == 9 + assert definition.index == 9 assert definition.name == "field_9a" if index == 2: - assert definition.idx == 9 + assert definition.index == 9 assert definition.name == "field_9" if index == 3: assert False @@ -275,13 +275,13 @@ def test_iter(self): for index, (definition, field) in enumerate(data_vector.items()): if index == 0: - assert definition.idx == 5 + assert definition.index == 5 assert field.name == 'field_5' if index == 1: - assert definition.idx == 9 + assert definition.index == 9 assert field.name == 'field_9a' if index == 2: - assert definition.idx == 9 + assert definition.index == 9 assert field.name == 'field_9' if index == 3: assert False @@ -436,8 +436,8 @@ def test_version_none(self): data_vector.add("field_invalid") assert len(data_vector) == 3 data_vector.add(10) # field_9a alias - assert len(data_vector) == 3 - assert len(data_vector._data._pairs) == 4 + assert len(data_vector) == 4 + assert len(data_vector.data.pairs) == 4 class TestHoldings: From a456ad94edf1dd382a4975ba40480bcbfa488b86 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Fri, 23 Jan 2026 20:24:41 +0100 Subject: [PATCH 22/61] wip --- tests/shi/test_shi_vector.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/shi/test_shi_vector.py b/tests/shi/test_shi_vector.py index 5ef6d0fc..c66a062f 100644 --- a/tests/shi/test_shi_vector.py +++ b/tests/shi/test_shi_vector.py @@ -89,6 +89,12 @@ def test_create(self): field = DataVectorTest.create_any_field('BAR') assert field is None + # create field by def + field = DataVectorTest.create_any_field(def_list[3]) + assert field.name == 'field_9a' + assert field.writeable + assert type(field) is Base + # create versioned data vector data_vector = DataVectorTest(parse_version("1.2")) assert data_vector.version == (1, 2, 0, 0) From d3830a88d47aaae3e2e7533994c39d4db11b480f Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Fri, 23 Jan 2026 20:30:13 +0100 Subject: [PATCH 23/61] wip --- tests/shi/test_shi_vector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/shi/test_shi_vector.py b/tests/shi/test_shi_vector.py index c66a062f..795870df 100644 --- a/tests/shi/test_shi_vector.py +++ b/tests/shi/test_shi_vector.py @@ -90,7 +90,7 @@ def test_create(self): assert field is None # create field by def - field = DataVectorTest.create_any_field(def_list[3]) + field = DataVectorTest.create_any_field(TEST_DEFINITIONS._definitions[2]) assert field.name == 'field_9a' assert field.writeable assert type(field) is Base From 6630180e777875ddfd291d96b3deadf9afc217ac Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Fri, 23 Jan 2026 21:39:19 +0100 Subject: [PATCH 24/61] wip last unify --- luxtronik/cfi/interface.py | 42 +++++++++++++++++++++++++++++--- luxtronik/collections.py | 36 +++++++++++++++------------ luxtronik/constants.py | 8 ------ luxtronik/data_vector.py | 37 ---------------------------- luxtronik/datatypes.py | 18 -------------- tests/cfi/test_cfi_parameters.py | 13 ---------- tests/test_datatypes.py | 14 ----------- 7 files changed, 59 insertions(+), 109 deletions(-) diff --git a/luxtronik/cfi/interface.py b/luxtronik/cfi/interface.py index 7750d099..87ad8c99 100644 --- a/luxtronik/cfi/interface.py +++ b/luxtronik/cfi/interface.py @@ -194,7 +194,7 @@ def _read_parameters(self, parameters): for _ in range(0, length): data.append(self._read_int()) LOGGER.info("%s: Read %d parameters", self._host, length) - parameters.parse(data, LUXTRONIK_CFI_REGISTER_BIT_SIZE) + self._parse(parameters, data) return parameters def _read_calculations(self, calculations): @@ -209,7 +209,7 @@ def _read_calculations(self, calculations): for _ in range(0, length): data.append(self._read_int()) LOGGER.info("%s: Read %d calculations", self._host, length) - calculations.parse(data, LUXTRONIK_CFI_REGISTER_BIT_SIZE) + self._parse(calculations, data) return calculations def _read_visibilities(self, visibilities): @@ -222,7 +222,7 @@ def _read_visibilities(self, visibilities): for _ in range(0, length): data.append(self._read_char()) LOGGER.info("%s: Read %d visibilities", self._host, length) - visibilities.parse(data, LUXTRONIK_CFI_REGISTER_BIT_SIZE) + self._parse(visibilities, data) return visibilities def _send_ints(self, *ints): @@ -259,4 +259,38 @@ def _read_int(self): def _read_char(self): "Low-level helper to receive a signed int" reading = self._read_bytes(LUXTRONIK_SOCKET_READ_SIZE_CHAR) - return struct.unpack(">b", reading)[0] \ No newline at end of file + return struct.unpack(">b", reading)[0] + + def _parse(self, data_vector, raw_data): + """ + Parse raw data into the corresponding fields. + + Args: + raw_data (list[int]): List of raw register values. + The raw data must start at register index 0. + num_bits (int): Number of bits per register. + """ + raw_len = len(raw_data) + # Prepare a list of undefined indices + undefined = {i for i in range(0, raw_len)} + + # integrate the data into the fields + 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 + continue + # remove all used indices from the list of undefined indices + for index in range(definition.index, next_idx): + undefined.discard(index) + pair.integrate_data(raw_data, LUXTRONIK_CFI_REGISTER_BIT_SIZE) + + # create an unknown field for additional data + for index in undefined: + # LOGGER.warning(f"Entry '%d' not in list of {self.name}", index) + definition = data_vector.definitions.create_unknown_definition(index) + field = definition.create_field() + field.raw = raw_data[index] + data_vector.data.add_sorted(definition, field) diff --git a/luxtronik/collections.py b/luxtronik/collections.py index 1825f681..814411ad 100644 --- a/luxtronik/collections.py +++ b/luxtronik/collections.py @@ -99,12 +99,12 @@ def get_data_arr(definition, field, num_bits): def integrate_data(definition, field, raw_data, num_bits, data_offset=-1): """ - Integrate raw values from a data array into the field. + Integrate the related parts of the `raw_data` into the field. Args: definition (LuxtronikDefinition): Meta-data of the field. field (Base): Field object where to integrate the data. - raw_data (list): Source array of bytes/words. + raw_data (list): Source array of register values. num_bits (int): Number of bits per register. data_offset (int): Optional offset. Defaults to `definition.index`. """ @@ -147,41 +147,47 @@ def __init__(self, definition, field): self.definition = definition def __iter__(self): + """ + Yield the definition and the field to unpack the object like `d, f = pair`. + """ yield self.definition yield self.field @property def index(self): + """ + Forward the `LuxtronikDefinition.index` property. + Please check its documentation. + """ return self.definition.index @property def addr(self): + """ + Forward the `LuxtronikDefinition.addr` property. + Please check its documentation. + """ return self.definition.addr @property def count(self): + """ + Forward the `LuxtronikDefinition.count` property. + Please check its documentation. + """ return self.definition.count def get_data_arr(self, num_bits): """ - Normalize the field's data to a list of the correct size. - - Args: - num_bits (int): Number of bits per register. - - Returns: - list[int] | None: List of length `definition.count`, or None if insufficient. + Forward the `get_data_arr` method with the stored objects. + Please check its documentation. """ return get_data_arr(self.definition, self.field, num_bits) def integrate_data(self, raw_data, num_bits, data_offset=-1): """ - Integrate the related parts of the `raw_data` into the field - - Args: - raw_data (list): Source array of register values. - data_offset (int): Optional offset. Defaults to `definition.index`. - num_bits (int): Number of bits per register. + Forward the `integrate_data` method with the stored objects. + Please check its documentation. """ integrate_data(self.definition, self.field, raw_data, num_bits, data_offset) diff --git a/luxtronik/constants.py b/luxtronik/constants.py index 35ddd4a7..bb88c487 100644 --- a/luxtronik/constants.py +++ b/luxtronik/constants.py @@ -14,14 +14,6 @@ # Content of response that is contained in responses to discovery broadcast LUXTRONIK_DISCOVERY_RESPONSE_PREFIX: Final = "2500;111;" -# 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" - # Since version 3.92.0, all unavailable 16 bit data fields # have been returning this value (0x7FFF) LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE: Final = 32767 diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index fe7e1df9..c440559e 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -182,43 +182,6 @@ def register_alias(self, def_field_name_or_idx, alias): return self._data.register_alias(def_field_name_or_idx, alias) -# Parse methods ############################################################### - - def parse(self, raw_data, num_bits): - """ - Parse raw data into the corresponding fields. - - Args: - raw_data (list[int]): List of raw register values. - The raw data must start at register index 0. - num_bits (int): Number of bits per register. - """ - raw_len = len(raw_data) - # Prepare a list of undefined indices - undefined = {i for i in range(0, raw_len)} - - # integrate the data into the fields - for pair in self._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 - continue - # remove all used indices from the list of undefined indices - for index in range(definition.index, next_idx): - undefined.discard(index) - pair.integrate_data(raw_data, num_bits) - - # create an unknown field for additional data - for index in undefined: - # LOGGER.warning(f"Entry '%d' not in list of {self.name}", index) - definition = self.definitions.create_unknown_definition(index) - field = definition.create_field() - field.raw = raw_data[index] - self._data.add_sorted(definition, field) - - # Get and set methods ######################################################### def _get_definition(self, def_field_name_or_idx, all_not_version_dependent): diff --git a/luxtronik/datatypes.py b/luxtronik/datatypes.py index be3a1d17..a6a79235 100755 --- a/luxtronik/datatypes.py +++ b/luxtronik/datatypes.py @@ -4,11 +4,6 @@ import socket import struct -from luxtronik.constants import ( - LUXTRONIK_NAME_CHECK_NONE, - LUXTRONIK_NAME_CHECK_PREFERRED, - LUXTRONIK_NAME_CHECK_OBSOLETE, -) from luxtronik.common import classproperty from functools import total_ordering @@ -61,19 +56,6 @@ def name(self): """Return the (most common) name of the entry.""" return self._names[0] - def check_name(self, name): - """ - Check whether a name matches one of the supported entry names. - The result string can be used to trigger a exception for obsolete names. - """ - name_lower = name.lower() - if name_lower == self.name.lower(): - return LUXTRONIK_NAME_CHECK_PREFERRED - elif name_lower in (n.lower() for n in self._names): - return LUXTRONIK_NAME_CHECK_OBSOLETE - else: - return LUXTRONIK_NAME_CHECK_NONE - @property def value(self): """Return the stored value converted from heatpump units.""" diff --git a/tests/cfi/test_cfi_parameters.py b/tests/cfi/test_cfi_parameters.py index e8c7c022..3764af8c 100644 --- a/tests/cfi/test_cfi_parameters.py +++ b/tests/cfi/test_cfi_parameters.py @@ -57,19 +57,6 @@ def test_get(self): j = 0.0 assert parameters.get(j) is None - def test_parse(self): - """Test cases for _parse""" - parameters = Parameters() - - n = 2000 - t = list(range(0, n + 1)) - parameters.parse(t, LUXTRONIK_CFI_REGISTER_BIT_SIZE) - - p = parameters.get(n) - - assert p.name == f"unknown_parameter_{n}" - assert p.raw == n - def test___iter__(self): """Test cases for __iter__""" parameters = Parameters() diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index b3852fa0..29a6bf02 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -4,12 +4,6 @@ import datetime -from luxtronik.constants import ( - LUXTRONIK_NAME_CHECK_NONE, - LUXTRONIK_NAME_CHECK_PREFERRED, - LUXTRONIK_NAME_CHECK_OBSOLETE, -) - from luxtronik.datatypes import ( Base, BitMaskBase, @@ -121,14 +115,6 @@ def test_empty_name(self): except Exception: pass - def test_check_name(self): - """Test cases for check_name() function""" - base = Base(["foo", "bar"]) - - assert base.check_name("foo") == LUXTRONIK_NAME_CHECK_PREFERRED - assert base.check_name("bar") == LUXTRONIK_NAME_CHECK_OBSOLETE - assert base.check_name("baz") == LUXTRONIK_NAME_CHECK_NONE - def test_value_property(self): """Test case for value property""" From dce9dc24cd7d417695c01aa0aadc87a16a9a383c Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Fri, 23 Jan 2026 22:09:20 +0100 Subject: [PATCH 25/61] add v0.3.14 fields to test compatibility --- luxtronik/datatypes.py | 35 + tests/test_compatibility.py | 3463 ++++++++++++++++++----------------- 2 files changed, 1767 insertions(+), 1731 deletions(-) diff --git a/luxtronik/datatypes.py b/luxtronik/datatypes.py index a6a79235..d060eea7 100755 --- a/luxtronik/datatypes.py +++ b/luxtronik/datatypes.py @@ -1,6 +1,7 @@ """datatype conversions.""" import datetime +import ipaddress import socket import struct @@ -327,6 +328,31 @@ class Seconds(Base): datatype_unit = "s" +class Pulses(Base): + """Pulses datatype, converts from and to Pulses.""" + + datatype_class = "pulses" + + +class IPAddress(Base): + """IP Address datatype, converts from and to an IP Address.""" + + datatype_class = "ipaddress" + + def from_heatpump(self, value): + if value < 0: + return str(ipaddress.IPv4Address(value + 2**32)) + if value > 2**32: + return str(ipaddress.IPv4Address(value - 2**32)) + return str(ipaddress.IPv4Address(value)) + + def to_heatpump(self, value): + result = int(ipaddress.IPv4Address(value)) + if result > 2**32: + return result - 2**32 + return result + + class IPv4Address(Base): """IPv4 address datatype, converts from and to an IPv4 address.""" @@ -590,6 +616,15 @@ class Count(Base): datatype_class = "count" +class Version(Base): + """Version datatype, converts from and to a Heatpump Version.""" + + datatype_class = "version" + + def from_heatpump(self, value): + return "".join([chr(c) for c in value]).strip("\x00") + + class Character(Base): """Character datatype, converts from and to a Character.""" diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index a2f79f46..4829a295 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -16,1132 +16,1132 @@ def test_compatibilities(self): paras = { # Status of 0.3.14: - "ID_Transfert_LuxNet": 0, - "ID_Einst_WK_akt": 1, - "ID_Einst_BWS_akt": 2, - "ID_Ba_Hz_akt": 3, - "ID_Ba_Bw_akt": 4, - "ID_Ba_Al_akt": 5, - "ID_SU_FrkdHz": 6, - "ID_SU_FrkdBw": 7, - "ID_SU_FrkdAl": 8, - "ID_Einst_HReg_akt": 9, - "ID_Einst_HzHwMAt_akt": 10, - "ID_Einst_HzHwHKE_akt": 11, - "ID_Einst_HzHKRANH_akt": 12, - "ID_Einst_HzHKRABS_akt": 13, - "ID_Einst_HzMK1E_akt": 14, - "ID_Einst_HzMK1ANH_akt": 15, - "ID_Einst_HzMK1ABS_akt": 16, - "ID_Einst_HzFtRl_akt": 17, - "ID_Einst_HzFtMK1Vl_akt": 18, - "ID_Einst_SUBW_akt": 19, - "ID_Einst_BwTDI_akt_MO": 20, - "ID_Einst_BwTDI_akt_DI": 21, - "ID_Einst_BwTDI_akt_MI": 22, - "ID_Einst_BwTDI_akt_DO": 23, - "ID_Einst_BwTDI_akt_FR": 24, - "ID_Einst_BwTDI_akt_SA": 25, - "ID_Einst_BwTDI_akt_SO": 26, - "ID_Einst_BwTDI_akt_AL": 27, - "ID_Einst_AnlKonf_akt": 28, - "ID_Einst_Sprache_akt": 29, - "ID_Switchoff_Zahler": 30, - "ID_Switchoff_index": 31, - "ID_Einst_EvuTyp_akt": 32, - "ID_Einst_RFVEinb_akt": 33, - "ID_Einst_AbtZykMax_akt": 34, - "ID_Einst_HREinb_akt": 35, - "ID_Einst_ZWE1Art_akt": 36, - "ID_Einst_ZWE1Fkt_akt": 37, - "ID_Einst_ZWE2Art_akt": 38, - "ID_Einst_ZWE2Fkt_akt": 39, - "ID_Einst_BWBer_akt": 40, - "ID_Einst_En_Inst": 41, - "ID_Einst_MK1Typ_akt": 42, - "ID_Einst_ABTLuft_akt": 43, - "ID_Einst_TLAbt_akt": 44, - "ID_Einst_LAbtTime_akt": 45, - "ID_Einst_ASDTyp_akt": 46, - "ID_Einst_LGST_akt": 47, - "ID_Einst_BwWpTime_akt": 48, - "ID_Einst_Popt_akt": 49, - "ID_Einst_Kurzprog_akt": 50, - "ID_Timer_Kurzprog_akt": 51, - "ID_Einst_ManAbt_akt": 52, - "ID_Einst_Ahz_akt": 53, - "ID_Einst_TVL_Ahz_1": 54, - "ID_Einst_TVL_Ahz_2": 55, - "ID_Einst_TVL_Ahz_3": 56, - "ID_Einst_TVL_Ahz_4": 57, - "ID_Einst_TVL_Ahz_5": 58, - "ID_Einst_TVL_Ahz_6": 59, - "ID_Einst_TVL_Ahz_7": 60, - "ID_Einst_TVL_Ahz_8": 61, - "ID_Einst_TVL_Ahz_9": 62, - "ID_Einst_TVL_Ahz_10": 63, - "ID_Einst_TVL_Std_1": 64, - "ID_Einst_TVL_Std_2": 65, - "ID_Einst_TVL_Std_3": 66, - "ID_Einst_TVL_Std_4": 67, - "ID_Einst_TVL_Std_5": 68, - "ID_Einst_TVL_Std_6": 69, - "ID_Einst_TVL_Std_7": 70, - "ID_Einst_TVL_Std_8": 71, - "ID_Einst_TVL_Std_9": 72, - "ID_Einst_TVL_Std_10": 73, - "ID_Einst_BWS_Hyst_akt": 74, - "ID_Temp_TBW_BwHD_saved": 75, - "ID_Einst_ABT1_akt": 76, - "ID_Einst_LABTpaus_akt": 77, - "ID_AHZ_state_akt": 78, - "ID_Sollwert_TRL_HZ_AHZ": 79, - "ID_AHP_valid_records": 80, - "ID_Timer_AHZ_akt": 81, - "ID_Einst_BWTINP_akt": 82, - "ID_Einst_ZUPTYP_akt": 83, - "ID_Sollwert_TLG_max": 84, - "ID_Einst_BWZIP_akt": 85, - "ID_Einst_ERRmZWE_akt": 86, - "ID_Einst_TRBegr_akt": 87, - "ID_Einst_HRHyst_akt": 88, - "ID_Einst_TRErhmax_akt": 89, - "ID_Einst_ZWEFreig_akt": 90, - "ID_Einst_TAmax_akt": 91, - "ID_Einst_TAmin_akt": 92, - "ID_Einst_TWQmin_akt": 93, - "ID_Einst_THGmax_akt": 94, - "ID_Einst_FRGT2VD_akt": 95, - "ID_Einst_TV2VDBW_akt": 96, - "ID_Einst_SuAll_akt": 97, - "ID_Einst_TAbtEnd_akt": 98, - "ID_Einst_NrKlingel_akt": 99, - "ID_Einst_BWStyp_akt": 100, - "ID_Einst_ABT2_akt": 101, - "ID_Einst_UeVd_akt": 102, - "ID_Einst_RTyp_akt": 103, - "ID_Einst_AhpM_akt": 104, - "ID_Soll_BWS_akt": 105, - "ID_Timer_Password": 106, - "ID_Einst_Zugangscode": 107, - "ID_Einst_BA_Kuehl_akt": 108, - "ID_Sollwert_Kuehl1_akt": 109, - "ID_Einst_KuehlFreig_akt": 110, - "ID_Einst_TAbsMin_akt": 111, - "ID_TWQmin_saved": 112, - "ID_CWP_saved": 113, - "ID_Einst_Anode_akt": 114, - "ID_Timer_pexoff_akt": 115, - "ID_Einst_AnlPrio_Hzakt": 116, - "ID_Einst_AnlPrio_Bwakt": 117, - "ID_Einst_AnlPrio_Swakt": 118, - "ID_Ba_Sw_akt": 119, - "ID_Einst_RTypMK1_akt": 120, - "ID_Einst_RTypMK2_akt": 121, - "ID_Einst_TDC_Ein_akt": 122, - "ID_Einst_TDC_Aus_akt": 123, - "ID_Einst_TDC_Max_akt": 124, - "ID_Einst_HysHzExEn_akt": 125, - "ID_Einst_HysBwExEn_akt": 126, - "ID_Einst_ZWE3Art_akt": 127, - "ID_Einst_ZWE3Fkt_akt": 128, - "ID_Einst_HzSup_akt": 129, - "ID_Einst_MK2Typ_akt": 130, - "ID_Einst_KuTyp_akt": 131, - "ID_Sollwert_KuCft1_akt": 132, - "ID_Sollwert_KuCft2_akt": 133, - "ID_Sollwert_AtDif1_akt": 134, - "ID_Sollwert_AtDif2_akt": 135, - "ID_SU_FrkdSwb": 136, - "ID_Einst_SwbBer_akt": 137, - "ID_Einst_TV2VDSWB_akt": 138, - "ID_Einst_MinSwan_Time_akt": 139, - "ID_Einst_SuMk2_akt": 140, - "ID_Einst_HzMK2E_akt": 141, - "ID_Einst_HzMK2ANH_akt": 142, - "ID_Einst_HzMK2ABS_akt": 143, - "ID_Einst_HzMK2Hgr_akt": 144, - "ID_Einst_HzFtMK2Vl_akt": 145, - "ID_Temp_THG_BwHD_saved": 146, - "ID_Temp_TA_BwHD_saved": 147, - "ID_Einst_BwHup_akt": 148, - "ID_Einst_TVLmax_akt": 149, - "ID_Einst_MK1LzFaktor_akt": 150, - "ID_Einst_MK2LzFaktor_akt": 151, - "ID_Einst_MK1PerFaktor_akt": 152, - "ID_Einst_MK2PerFaktor_akt": 153, - "ID_Entl_Zyklus_akt": 154, - "ID_Einst_Entl_time_akt": 155, - "ID_Entl_Pause": 156, - "ID_Entl_timer": 157, - "ID_Einst_Entl_akt": 158, - "ID_Ahz_HLeist_confirmed": 159, - "ID_FirstInit_akt": 160, - "ID_Einst_SuAll_akt2": 161, - "ID_Einst_SuAllWo_zeit_0_0": 162, - "ID_Einst_SuAllWo_zeit_0_1": 163, - "ID_Einst_SuAllWo_zeit_1_0": 164, - "ID_Einst_SuAllWo_zeit_1_1": 165, - "ID_Einst_SuAllWo_zeit_2_0": 166, - "ID_Einst_SuAllWo_zeit_2_1": 167, - "ID_Einst_SuAll25_zeit_0_0": 168, - "ID_Einst_SuAll25_zeit_0_1": 169, - "ID_Einst_SuAll25_zeit_1_0": 170, - "ID_Einst_SuAll25_zeit_1_1": 171, - "ID_Einst_SuAll25_zeit_2_0": 172, - "ID_Einst_SuAll25_zeit_2_1": 173, - "ID_Einst_SuAll25_zeit_0_2": 174, - "ID_Einst_SuAll25_zeit_0_3": 175, - "ID_Einst_SuAll25_zeit_1_2": 176, - "ID_Einst_SuAll25_zeit_1_3": 177, - "ID_Einst_SuAll25_zeit_2_2": 178, - "ID_Einst_SuAll25_zeit_2_3": 179, - "ID_Einst_SuAllTg_zeit_0_0": 180, - "ID_Einst_SuAllTg_zeit_0_1": 181, - "ID_Einst_SuAllTg_zeit_1_0": 182, - "ID_Einst_SuAllTg_zeit_1_1": 183, - "ID_Einst_SuAllTg_zeit_2_0": 184, - "ID_Einst_SuAllTg_zeit_2_1": 185, - "ID_Einst_SuAllTg_zeit_0_2": 186, - "ID_Einst_SuAllTg_zeit_0_3": 187, - "ID_Einst_SuAllTg_zeit_1_2": 188, - "ID_Einst_SuAllTg_zeit_1_3": 189, - "ID_Einst_SuAllTg_zeit_2_2": 190, - "ID_Einst_SuAllTg_zeit_2_3": 191, - "ID_Einst_SuAllTg_zeit_0_4": 192, - "ID_Einst_SuAllTg_zeit_0_5": 193, - "ID_Einst_SuAllTg_zeit_1_4": 194, - "ID_Einst_SuAllTg_zeit_1_5": 195, - "ID_Einst_SuAllTg_zeit_2_4": 196, - "ID_Einst_SuAllTg_zeit_2_5": 197, - "ID_Einst_SuAllTg_zeit_0_6": 198, - "ID_Einst_SuAllTg_zeit_0_7": 199, - "ID_Einst_SuAllTg_zeit_1_6": 200, - "ID_Einst_SuAllTg_zeit_1_7": 201, - "ID_Einst_SuAllTg_zeit_2_6": 202, - "ID_Einst_SuAllTg_zeit_2_7": 203, - "ID_Einst_SuAllTg_zeit_0_8": 204, - "ID_Einst_SuAllTg_zeit_0_9": 205, - "ID_Einst_SuAllTg_zeit_1_8": 206, - "ID_Einst_SuAllTg_zeit_1_9": 207, - "ID_Einst_SuAllTg_zeit_2_8": 208, - "ID_Einst_SuAllTg_zeit_2_9": 209, - "ID_Einst_SuAllTg_zeit_0_10": 210, - "ID_Einst_SuAllTg_zeit_0_11": 211, - "ID_Einst_SuAllTg_zeit_1_10": 212, - "ID_Einst_SuAllTg_zeit_1_11": 213, - "ID_Einst_SuAllTg_zeit_2_10": 214, - "ID_Einst_SuAllTg_zeit_2_11": 215, - "ID_Einst_SuAllTg_zeit_0_12": 216, - "ID_Einst_SuAllTg_zeit_0_13": 217, - "ID_Einst_SuAllTg_zeit_1_12": 218, - "ID_Einst_SuAllTg_zeit_1_13": 219, - "ID_Einst_SuAllTg_zeit_2_12": 220, - "ID_Einst_SuAllTg_zeit_2_13": 221, - "ID_Einst_SuHkr_akt": 222, - "ID_Einst_SuHkrW0_zeit_0_0": 223, - "ID_Einst_SuHkrW0_zeit_0_1": 224, - "ID_Einst_SuHkrW0_zeit_1_0": 225, - "ID_Einst_SuHkrW0_zeit_1_1": 226, - "ID_Einst_SuHkrW0_zeit_2_0": 227, - "ID_Einst_SuHkrW0_zeit_2_1": 228, - "ID_Einst_SuHkr25_zeit_0_0": 229, - "ID_Einst_SuHkr25_zeit_0_1": 230, - "ID_Einst_SuHkr25_zeit_1_0": 231, - "ID_Einst_SuHkr25_zeit_1_1": 232, - "ID_Einst_SuHkr25_zeit_2_0": 233, - "ID_Einst_SuHkr25_zeit_2_1": 234, - "ID_Einst_SuHkr25_zeit_0_2": 235, - "ID_Einst_SuHkr25_zeit_0_3": 236, - "ID_Einst_SuHkr25_zeit_1_2": 237, - "ID_Einst_SuHkr25_zeit_1_3": 238, - "ID_Einst_SuHkr25_zeit_2_2": 239, - "ID_Einst_SuHkr25_zeit_2_3": 240, - "ID_Einst_SuHkrTG_zeit_0_0": 241, - "ID_Einst_SuHkrTG_zeit_0_1": 242, - "ID_Einst_SuHkrTG_zeit_1_0": 243, - "ID_Einst_SuHkrTG_zeit_1_1": 244, - "ID_Einst_SuHkrTG_zeit_2_0": 245, - "ID_Einst_SuHkrTG_zeit_2_1": 246, - "ID_Einst_SuHkrTG_zeit_0_2": 247, - "ID_Einst_SuHkrTG_zeit_0_3": 248, - "ID_Einst_SuHkrTG_zeit_1_2": 249, - "ID_Einst_SuHkrTG_zeit_1_3": 250, - "ID_Einst_SuHkrTG_zeit_2_2": 251, - "ID_Einst_SuHkrTG_zeit_2_3": 252, - "ID_Einst_SuHkrTG_zeit_0_4": 253, - "ID_Einst_SuHkrTG_zeit_0_5": 254, - "ID_Einst_SuHkrTG_zeit_1_4": 255, - "ID_Einst_SuHkrTG_zeit_1_5": 256, - "ID_Einst_SuHkrTG_zeit_2_4": 257, - "ID_Einst_SuHkrTG_zeit_2_5": 258, - "ID_Einst_SuHkrTG_zeit_0_6": 259, - "ID_Einst_SuHkrTG_zeit_0_7": 260, - "ID_Einst_SuHkrTG_zeit_1_6": 261, - "ID_Einst_SuHkrTG_zeit_1_7": 262, - "ID_Einst_SuHkrTG_zeit_2_6": 263, - "ID_Einst_SuHkrTG_zeit_2_7": 264, - "ID_Einst_SuHkrTG_zeit_0_8": 265, - "ID_Einst_SuHkrTG_zeit_0_9": 266, - "ID_Einst_SuHkrTG_zeit_1_8": 267, - "ID_Einst_SuHkrTG_zeit_1_9": 268, - "ID_Einst_SuHkrTG_zeit_2_8": 269, - "ID_Einst_SuHkrTG_zeit_2_9": 270, - "ID_Einst_SuHkrTG_zeit_0_10": 271, - "ID_Einst_SuHkrTG_zeit_0_11": 272, - "ID_Einst_SuHkrTG_zeit_1_10": 273, - "ID_Einst_SuHkrTG_zeit_1_11": 274, - "ID_Einst_SuHkrTG_zeit_2_10": 275, - "ID_Einst_SuHkrTG_zeit_2_11": 276, - "ID_Einst_SuHkrTG_zeit_0_12": 277, - "ID_Einst_SuHkrTG_zeit_0_13": 278, - "ID_Einst_SuHkrTG_zeit_1_12": 279, - "ID_Einst_SuHkrTG_zeit_1_13": 280, - "ID_Einst_SuHkrTG_zeit_2_12": 281, - "ID_Einst_SuHkrTG_zeit_2_13": 282, - "ID_Einst_SuMk1_akt": 283, - "ID_Einst_SuMk1W0_zeit_0_0": 284, - "ID_Einst_SuMk1W0_zeit_0_1": 285, - "ID_Einst_SuMk1W0_zeit_1_0": 286, - "ID_Einst_SuMk1W0_zeit_1_1": 287, - "ID_Einst_SuMk1W0_zeit_2_0": 288, - "ID_Einst_SuMk1W0_zeit_2_1": 289, - "ID_Einst_SuMk125_zeit_0_0": 290, - "ID_Einst_SuMk125_zeit_0_1": 291, - "ID_Einst_SuMk125_zeit_1_0": 292, - "ID_Einst_SuMk125_zeit_1_1": 293, - "ID_Einst_SuMk125_zeit_2_0": 294, - "ID_Einst_SuMk125_zeit_2_1": 295, - "ID_Einst_SuMk125_zeit_0_2": 296, - "ID_Einst_SuMk125_zeit_0_3": 297, - "ID_Einst_SuMk125_zeit_1_2": 298, - "ID_Einst_SuMk125_zeit_1_3": 299, - "ID_Einst_SuMk125_zeit_2_2": 300, - "ID_Einst_SuMk125_zeit_2_3": 301, - "ID_Einst_SuMk1TG_zeit_0_0": 302, - "ID_Einst_SuMk1TG_zeit_0_1": 303, - "ID_Einst_SuMk1TG_zeit_1_0": 304, - "ID_Einst_SuMk1TG_zeit_1_1": 305, - "ID_Einst_SuMk1TG_zeit_2_0": 306, - "ID_Einst_SuMk1TG_zeit_2_1": 307, - "ID_Einst_SuMk1TG_zeit_0_2": 308, - "ID_Einst_SuMk1TG_zeit_0_3": 309, - "ID_Einst_SuMk1TG_zeit_1_2": 310, - "ID_Einst_SuMk1TG_zeit_1_3": 311, - "ID_Einst_SuMk1TG_zeit_2_2": 312, - "ID_Einst_SuMk1TG_zeit_2_3": 313, - "ID_Einst_SuMk1TG_zeit_0_4": 314, - "ID_Einst_SuMk1TG_zeit_0_5": 315, - "ID_Einst_SuMk1TG_zeit_1_4": 316, - "ID_Einst_SuMk1TG_zeit_1_5": 317, - "ID_Einst_SuMk1TG_zeit_2_4": 318, - "ID_Einst_SuMk1TG_zeit_2_5": 319, - "ID_Einst_SuMk1TG_zeit_0_6": 320, - "ID_Einst_SuMk1TG_zeit_0_7": 321, - "ID_Einst_SuMk1TG_zeit_1_6": 322, - "ID_Einst_SuMk1TG_zeit_1_7": 323, - "ID_Einst_SuMk1TG_zeit_2_6": 324, - "ID_Einst_SuMk1TG_zeit_2_7": 325, - "ID_Einst_SuMk1TG_zeit_0_8": 326, - "ID_Einst_SuMk1TG_zeit_0_9": 327, - "ID_Einst_SuMk1TG_zeit_1_8": 328, - "ID_Einst_SuMk1TG_zeit_1_9": 329, - "ID_Einst_SuMk1TG_zeit_2_8": 330, - "ID_Einst_SuMk1TG_zeit_2_9": 331, - "ID_Einst_SuMk1TG_zeit_0_10": 332, - "ID_Einst_SuMk1TG_zeit_0_11": 333, - "ID_Einst_SuMk1TG_zeit_1_10": 334, - "ID_Einst_SuMk1TG_zeit_1_11": 335, - "ID_Einst_SuMk1TG_zeit_2_10": 336, - "ID_Einst_SuMk1TG_zeit_2_11": 337, - "ID_Einst_SuMk1TG_zeit_0_12": 338, - "ID_Einst_SuMk1TG_zeit_0_13": 339, - "ID_Einst_SuMk1TG_zeit_1_12": 340, - "ID_Einst_SuMk1TG_zeit_1_13": 341, - "ID_Einst_SuMk1TG_zeit_2_12": 342, - "ID_Einst_SuMk1TG_zeit_2_13": 343, - "ID_Einst_SuMk2_akt2": 344, - "ID_Einst_SuMk2Wo_zeit_0_0": 345, - "ID_Einst_SuMk2Wo_zeit_0_1": 346, - "ID_Einst_SuMk2Wo_zeit_1_0": 347, - "ID_Einst_SuMk2Wo_zeit_1_1": 348, - "ID_Einst_SuMk2Wo_zeit_2_0": 349, - "ID_Einst_SuMk2Wo_zeit_2_1": 350, - "ID_Einst_SuMk225_zeit_0_0": 351, - "ID_Einst_SuMk225_zeit_0_1": 352, - "ID_Einst_SuMk225_zeit_1_0": 353, - "ID_Einst_SuMk225_zeit_1_1": 354, - "ID_Einst_SuMk225_zeit_2_0": 355, - "ID_Einst_SuMk225_zeit_2_1": 356, - "ID_Einst_SuMk225_zeit_0_2": 357, - "ID_Einst_SuMk225_zeit_0_3": 358, - "ID_Einst_SuMk225_zeit_1_2": 359, - "ID_Einst_SuMk225_zeit_1_3": 360, - "ID_Einst_SuMk225_zeit_2_2": 361, - "ID_Einst_SuMk225_zeit_2_3": 362, - "ID_Einst_SuMk2Tg_zeit_0_0": 363, - "ID_Einst_SuMk2Tg_zeit_0_1": 364, - "ID_Einst_SuMk2Tg_zeit_1_0": 365, - "ID_Einst_SuMk2Tg_zeit_1_1": 366, - "ID_Einst_SuMk2Tg_zeit_2_0": 367, - "ID_Einst_SuMk2Tg_zeit_2_1": 368, - "ID_Einst_SuMk2Tg_zeit_0_2": 369, - "ID_Einst_SuMk2Tg_zeit_0_3": 370, - "ID_Einst_SuMk2Tg_zeit_1_2": 371, - "ID_Einst_SuMk2Tg_zeit_1_3": 372, - "ID_Einst_SuMk2Tg_zeit_2_2": 373, - "ID_Einst_SuMk2Tg_zeit_2_3": 374, - "ID_Einst_SuMk2Tg_zeit_0_4": 375, - "ID_Einst_SuMk2Tg_zeit_0_5": 376, - "ID_Einst_SuMk2Tg_zeit_1_4": 377, - "ID_Einst_SuMk2Tg_zeit_1_5": 378, - "ID_Einst_SuMk2Tg_zeit_2_4": 379, - "ID_Einst_SuMk2Tg_zeit_2_5": 380, - "ID_Einst_SuMk2Tg_zeit_0_6": 381, - "ID_Einst_SuMk2Tg_zeit_0_7": 382, - "ID_Einst_SuMk2Tg_zeit_1_6": 383, - "ID_Einst_SuMk2Tg_zeit_1_7": 384, - "ID_Einst_SuMk2Tg_zeit_2_6": 385, - "ID_Einst_SuMk2Tg_zeit_2_7": 386, - "ID_Einst_SuMk2Tg_zeit_0_8": 387, - "ID_Einst_SuMk2Tg_zeit_0_9": 388, - "ID_Einst_SuMk2Tg_zeit_1_8": 389, - "ID_Einst_SuMk2Tg_zeit_1_9": 390, - "ID_Einst_SuMk2Tg_zeit_2_8": 391, - "ID_Einst_SuMk2Tg_zeit_2_9": 392, - "ID_Einst_SuMk2Tg_zeit_0_10": 393, - "ID_Einst_SuMk2Tg_zeit_0_11": 394, - "ID_Einst_SuMk2Tg_zeit_1_10": 395, - "ID_Einst_SuMk2Tg_zeit_1_11": 396, - "ID_Einst_SuMk2Tg_zeit_2_10": 397, - "ID_Einst_SuMk2Tg_zeit_2_11": 398, - "ID_Einst_SuMk2Tg_zeit_0_12": 399, - "ID_Einst_SuMk2Tg_zeit_0_13": 400, - "ID_Einst_SuMk2Tg_zeit_1_12": 401, - "ID_Einst_SuMk2Tg_zeit_1_13": 402, - "ID_Einst_SuMk2Tg_zeit_2_12": 403, - "ID_Einst_SuMk2Tg_zeit_2_13": 404, - "ID_Einst_SUBW_akt2": 405, - "ID_Einst_SuBwWO_zeit_0_0": 406, - "ID_Einst_SuBwWO_zeit_0_1": 407, - "ID_Einst_SuBwWO_zeit_1_0": 408, - "ID_Einst_SuBwWO_zeit_1_1": 409, - "ID_Einst_SuBwWO_zeit_2_0": 410, - "ID_Einst_SuBwWO_zeit_2_1": 411, - "ID_Einst_SuBwWO_zeit_3_0": 412, - "ID_Einst_SuBwWO_zeit_3_1": 413, - "ID_Einst_SuBwWO_zeit_4_0": 414, - "ID_Einst_SuBwWO_zeit_4_1": 415, - "ID_Einst_SuBw25_zeit_0_0": 416, - "ID_Einst_SuBw25_zeit_0_1": 417, - "ID_Einst_SuBw25_zeit_1_0": 418, - "ID_Einst_SuBw25_zeit_1_1": 419, - "ID_Einst_SuBw25_zeit_2_0": 420, - "ID_Einst_SuBw25_zeit_2_1": 421, - "ID_Einst_SuBw25_zeit_3_0": 422, - "ID_Einst_SuBw25_zeit_3_1": 423, - "ID_Einst_SuBw25_zeit_4_0": 424, - "ID_Einst_SuBw25_zeit_4_1": 425, - "ID_Einst_SuBw25_zeit_0_2": 426, - "ID_Einst_SuBw25_zeit_0_3": 427, - "ID_Einst_SuBw25_zeit_1_2": 428, - "ID_Einst_SuBw25_zeit_1_3": 429, - "ID_Einst_SuBw25_zeit_2_2": 430, - "ID_Einst_SuBw25_zeit_2_3": 431, - "ID_Einst_SuBw25_zeit_3_2": 432, - "ID_Einst_SuBw25_zeit_3_3": 433, - "ID_Einst_SuBw25_zeit_4_2": 434, - "ID_Einst_SuBw25_zeit_4_3": 435, - "ID_Einst_SuBwTG_zeit_0_0": 436, - "ID_Einst_SuBwTG_zeit_0_1": 437, - "ID_Einst_SuBwTG_zeit_1_0": 438, - "ID_Einst_SuBwTG_zeit_1_1": 439, - "ID_Einst_SuBwTG_zeit_2_0": 440, - "ID_Einst_SuBwTG_zeit_2_1": 441, - "ID_Einst_SuBwTG_zeit_3_0": 442, - "ID_Einst_SuBwTG_zeit_3_1": 443, - "ID_Einst_SuBwTG_zeit_4_0": 444, - "ID_Einst_SuBwTG_zeit_4_1": 445, - "ID_Einst_SuBwTG_zeit_0_2": 446, - "ID_Einst_SuBwTG_zeit_0_3": 447, - "ID_Einst_SuBwTG_zeit_1_2": 448, - "ID_Einst_SuBwTG_zeit_1_3": 449, - "ID_Einst_SuBwTG_zeit_2_2": 450, - "ID_Einst_SuBwTG_zeit_2_3": 451, - "ID_Einst_SuBwTG_zeit_3_2": 452, - "ID_Einst_SuBwTG_zeit_3_3": 453, - "ID_Einst_SuBwTG_zeit_4_2": 454, - "ID_Einst_SuBwTG_zeit_4_3": 455, - "ID_Einst_SuBwTG_zeit_0_4": 456, - "ID_Einst_SuBwTG_zeit_0_5": 457, - "ID_Einst_SuBwTG_zeit_1_4": 458, - "ID_Einst_SuBwTG_zeit_1_5": 459, - "ID_Einst_SuBwTG_zeit_2_4": 460, - "ID_Einst_SuBwTG_zeit_2_5": 461, - "ID_Einst_SuBwTG_zeit_3_4": 462, - "ID_Einst_SuBwTG_zeit_3_5": 463, - "ID_Einst_SuBwTG_zeit_4_4": 464, - "ID_Einst_SuBwTG_zeit_4_5": 465, - "ID_Einst_SuBwTG_zeit_0_6": 466, - "ID_Einst_SuBwTG_zeit_0_7": 467, - "ID_Einst_SuBwTG_zeit_1_6": 468, - "ID_Einst_SuBwTG_zeit_1_7": 469, - "ID_Einst_SuBwTG_zeit_2_6": 470, - "ID_Einst_SuBwTG_zeit_2_7": 471, - "ID_Einst_SuBwTG_zeit_3_6": 472, - "ID_Einst_SuBwTG_zeit_3_7": 473, - "ID_Einst_SuBwTG_zeit_4_6": 474, - "ID_Einst_SuBwTG_zeit_4_7": 475, - "ID_Einst_SuBwTG_zeit_0_8": 476, - "ID_Einst_SuBwTG_zeit_0_9": 477, - "ID_Einst_SuBwTG_zeit_1_8": 478, - "ID_Einst_SuBwTG_zeit_1_9": 479, - "ID_Einst_SuBwTG_zeit_2_8": 480, - "ID_Einst_SuBwTG_zeit_2_9": 481, - "ID_Einst_SuBwTG_zeit_3_8": 482, - "ID_Einst_SuBwTG_zeit_3_9": 483, - "ID_Einst_SuBwTG_zeit_4_8": 484, - "ID_Einst_SuBwTG_zeit_4_9": 485, - "ID_Einst_SuBwTG_zeit_0_10": 486, - "ID_Einst_SuBwTG_zeit_0_11": 487, - "ID_Einst_SuBwTG_zeit_1_10": 488, - "ID_Einst_SuBwTG_zeit_1_11": 489, - "ID_Einst_SuBwTG_zeit_2_10": 490, - "ID_Einst_SuBwTG_zeit_2_11": 491, - "ID_Einst_SuBwTG_zeit_3_10": 492, - "ID_Einst_SuBwTG_zeit_3_11": 493, - "ID_Einst_SuBwTG_zeit_4_10": 494, - "ID_Einst_SuBwTG_zeit_4_11": 495, - "ID_Einst_SuBwTG_zeit_0_12": 496, - "ID_Einst_SuBwTG_zeit_0_13": 497, - "ID_Einst_SuBwTG_zeit_1_12": 498, - "ID_Einst_SuBwTG_zeit_1_13": 499, - "ID_Einst_SuBwTG_zeit_2_12": 500, - "ID_Einst_SuBwTG_zeit_2_13": 501, - "ID_Einst_SuBwTG_zeit_3_12": 502, - "ID_Einst_SuBwTG_zeit_3_13": 503, - "ID_Einst_SuBwTG_zeit_4_12": 504, - "ID_Einst_SuBwTG_zeit_4_13": 505, - "ID_Einst_SuZIP_akt": 506, - "ID_Einst_SuZIPWo_zeit_0_0": 507, - "ID_Einst_SuZIPWo_zeit_0_1": 508, - "ID_Einst_SuZIPWo_zeit_1_0": 509, - "ID_Einst_SuZIPWo_zeit_1_1": 510, - "ID_Einst_SuZIPWo_zeit_2_0": 511, - "ID_Einst_SuZIPWo_zeit_2_1": 512, - "ID_Einst_SuZIPWo_zeit_3_0": 513, - "ID_Einst_SuZIPWo_zeit_3_1": 514, - "ID_Einst_SuZIPWo_zeit_4_0": 515, - "ID_Einst_SuZIPWo_zeit_4_1": 516, - "ID_Einst_SuZIP25_zeit_0_0": 517, - "ID_Einst_SuZIP25_zeit_0_1": 518, - "ID_Einst_SuZIP25_zeit_1_0": 519, - "ID_Einst_SuZIP25_zeit_1_1": 520, - "ID_Einst_SuZIP25_zeit_2_0": 521, - "ID_Einst_SuZIP25_zeit_2_1": 522, - "ID_Einst_SuZIP25_zeit_3_0": 523, - "ID_Einst_SuZIP25_zeit_3_1": 524, - "ID_Einst_SuZIP25_zeit_4_0": 525, - "ID_Einst_SuZIP25_zeit_4_1": 526, - "ID_Einst_SuZIP25_zeit_0_2": 527, - "ID_Einst_SuZIP25_zeit_0_3": 528, - "ID_Einst_SuZIP25_zeit_1_2": 529, - "ID_Einst_SuZIP25_zeit_1_3": 530, - "ID_Einst_SuZIP25_zeit_2_2": 531, - "ID_Einst_SuZIP25_zeit_2_3": 532, - "ID_Einst_SuZIP25_zeit_3_2": 533, - "ID_Einst_SuZIP25_zeit_3_3": 534, - "ID_Einst_SuZIP25_zeit_4_2": 535, - "ID_Einst_SuZIP25_zeit_4_3": 536, - "ID_Einst_SuZIPTg_zeit_0_0": 537, - "ID_Einst_SuZIPTg_zeit_0_1": 538, - "ID_Einst_SuZIPTg_zeit_1_0": 539, - "ID_Einst_SuZIPTg_zeit_1_1": 540, - "ID_Einst_SuZIPTg_zeit_2_0": 541, - "ID_Einst_SuZIPTg_zeit_2_1": 542, - "ID_Einst_SuZIPTg_zeit_3_0": 543, - "ID_Einst_SuZIPTg_zeit_3_1": 544, - "ID_Einst_SuZIPTg_zeit_4_0": 545, - "ID_Einst_SuZIPTg_zeit_4_1": 546, - "ID_Einst_SuZIPTg_zeit_0_2": 547, - "ID_Einst_SuZIPTg_zeit_0_3": 548, - "ID_Einst_SuZIPTg_zeit_1_2": 549, - "ID_Einst_SuZIPTg_zeit_1_3": 550, - "ID_Einst_SuZIPTg_zeit_2_2": 551, - "ID_Einst_SuZIPTg_zeit_2_3": 552, - "ID_Einst_SuZIPTg_zeit_3_2": 553, - "ID_Einst_SuZIPTg_zeit_3_3": 554, - "ID_Einst_SuZIPTg_zeit_4_2": 555, - "ID_Einst_SuZIPTg_zeit_4_3": 556, - "ID_Einst_SuZIPTg_zeit_0_4": 557, - "ID_Einst_SuZIPTg_zeit_0_5": 558, - "ID_Einst_SuZIPTg_zeit_1_4": 559, - "ID_Einst_SuZIPTg_zeit_1_5": 560, - "ID_Einst_SuZIPTg_zeit_2_4": 561, - "ID_Einst_SuZIPTg_zeit_2_5": 562, - "ID_Einst_SuZIPTg_zeit_3_4": 563, - "ID_Einst_SuZIPTg_zeit_3_5": 564, - "ID_Einst_SuZIPTg_zeit_4_4": 565, - "ID_Einst_SuZIPTg_zeit_4_5": 566, - "ID_Einst_SuZIPTg_zeit_0_6": 567, - "ID_Einst_SuZIPTg_zeit_0_7": 568, - "ID_Einst_SuZIPTg_zeit_1_6": 569, - "ID_Einst_SuZIPTg_zeit_1_7": 570, - "ID_Einst_SuZIPTg_zeit_2_6": 571, - "ID_Einst_SuZIPTg_zeit_2_7": 572, - "ID_Einst_SuZIPTg_zeit_3_6": 573, - "ID_Einst_SuZIPTg_zeit_3_7": 574, - "ID_Einst_SuZIPTg_zeit_4_6": 575, - "ID_Einst_SuZIPTg_zeit_4_7": 576, - "ID_Einst_SuZIPTg_zeit_0_8": 577, - "ID_Einst_SuZIPTg_zeit_0_9": 578, - "ID_Einst_SuZIPTg_zeit_1_8": 579, - "ID_Einst_SuZIPTg_zeit_1_9": 580, - "ID_Einst_SuZIPTg_zeit_2_8": 581, - "ID_Einst_SuZIPTg_zeit_2_9": 582, - "ID_Einst_SuZIPTg_zeit_3_8": 583, - "ID_Einst_SuZIPTg_zeit_3_9": 584, - "ID_Einst_SuZIPTg_zeit_4_8": 585, - "ID_Einst_SuZIPTg_zeit_4_9": 586, - "ID_Einst_SuZIPTg_zeit_0_10": 587, - "ID_Einst_SuZIPTg_zeit_0_11": 588, - "ID_Einst_SuZIPTg_zeit_1_10": 589, - "ID_Einst_SuZIPTg_zeit_1_11": 590, - "ID_Einst_SuZIPTg_zeit_2_10": 591, - "ID_Einst_SuZIPTg_zeit_2_11": 592, - "ID_Einst_SuZIPTg_zeit_3_10": 593, - "ID_Einst_SuZIPTg_zeit_3_11": 594, - "ID_Einst_SuZIPTg_zeit_4_10": 595, - "ID_Einst_SuZIPTg_zeit_4_11": 596, - "ID_Einst_SuZIPTg_zeit_0_12": 597, - "ID_Einst_SuZIPTg_zeit_0_13": 598, - "ID_Einst_SuZIPTg_zeit_1_12": 599, - "ID_Einst_SuZIPTg_zeit_1_13": 600, - "ID_Einst_SuZIPTg_zeit_2_12": 601, - "ID_Einst_SuZIPTg_zeit_2_13": 602, - "ID_Einst_SuZIPTg_zeit_3_12": 603, - "ID_Einst_SuZIPTg_zeit_3_13": 604, - "ID_Einst_SuZIPTg_zeit_4_12": 605, - "ID_Einst_SuZIPTg_zeit_4_13": 606, - "ID_Einst_SuSwb_akt": 607, - "ID_Einst_SuSwbWo_zeit_0_0": 608, - "ID_Einst_SuSwbWo_zeit_0_1": 609, - "ID_Einst_SuSwbWo_zeit_1_0": 610, - "ID_Einst_SuSwbWo_zeit_1_1": 611, - "ID_Einst_SuSwbWo_zeit_2_0": 612, - "ID_Einst_SuSwbWo_zeit_2_1": 613, - "ID_Einst_SuSwb25_zeit_0_0": 614, - "ID_Einst_SuSwb25_zeit_0_1": 615, - "ID_Einst_SuSwb25_zeit_1_0": 616, - "ID_Einst_SuSwb25_zeit_1_1": 617, - "ID_Einst_SuSwb25_zeit_2_0": 618, - "ID_Einst_SuSwb25_zeit_2_1": 619, - "ID_Einst_SuSwb25_zeit_0_2": 620, - "ID_Einst_SuSwb25_zeit_0_3": 621, - "ID_Einst_SuSwb25_zeit_1_2": 622, - "ID_Einst_SuSwb25_zeit_1_3": 623, - "ID_Einst_SuSwb25_zeit_2_2": 624, - "ID_Einst_SuSwb25_zeit_2_3": 625, - "ID_Einst_SuSwbTg_zeit_0_0": 626, - "ID_Einst_SuSwbTg_zeit_0_1": 627, - "ID_Einst_SuSwbTg_zeit_1_0": 628, - "ID_Einst_SuSwbTg_zeit_1_1": 629, - "ID_Einst_SuSwbTg_zeit_2_0": 630, - "ID_Einst_SuSwbTg_zeit_2_1": 631, - "ID_Einst_SuSwbTg_zeit_0_2": 632, - "ID_Einst_SuSwbTg_zeit_0_3": 633, - "ID_Einst_SuSwbTg_zeit_1_2": 634, - "ID_Einst_SuSwbTg_zeit_1_3": 635, - "ID_Einst_SuSwbTg_zeit_2_2": 636, - "ID_Einst_SuSwbTg_zeit_2_3": 637, - "ID_Einst_SuSwbTg_zeit_0_4": 638, - "ID_Einst_SuSwbTg_zeit_0_5": 639, - "ID_Einst_SuSwbTg_zeit_1_4": 640, - "ID_Einst_SuSwbTg_zeit_1_5": 641, - "ID_Einst_SuSwbTg_zeit_2_4": 642, - "ID_Einst_SuSwbTg_zeit_2_5": 643, - "ID_Einst_SuSwbTg_zeit_0_6": 644, - "ID_Einst_SuSwbTg_zeit_0_7": 645, - "ID_Einst_SuSwbTg_zeit_1_6": 646, - "ID_Einst_SuSwbTg_zeit_1_7": 647, - "ID_Einst_SuSwbTg_zeit_2_6": 648, - "ID_Einst_SuSwbTg_zeit_2_7": 649, - "ID_Einst_SuSwbTg_zeit_0_8": 650, - "ID_Einst_SuSwbTg_zeit_0_9": 651, - "ID_Einst_SuSwbTg_zeit_1_8": 652, - "ID_Einst_SuSwbTg_zeit_1_9": 653, - "ID_Einst_SuSwbTg_zeit_2_8": 654, - "ID_Einst_SuSwbTg_zeit_2_9": 655, - "ID_Einst_SuSwbTg_zeit_0_10": 656, - "ID_Einst_SuSwbTg_zeit_0_11": 657, - "ID_Einst_SuSwbTg_zeit_1_10": 658, - "ID_Einst_SuSwbTg_zeit_1_11": 659, - "ID_Einst_SuSwbTg_zeit_2_10": 660, - "ID_Einst_SuSwbTg_zeit_2_11": 661, - "ID_Einst_SuSwbTg_zeit_0_12": 662, - "ID_Einst_SuSwbTg_zeit_0_13": 663, - "ID_Einst_SuSwbTg_zeit_1_12": 664, - "ID_Einst_SuSwbTg_zeit_1_13": 665, - "ID_Einst_SuSwbTg_zeit_2_12": 666, - "ID_Einst_SuSwbTg_zeit_2_13": 667, - "ID_Zaehler_BetrZeitWP": 668, - "ID_Zaehler_BetrZeitVD1": 669, - "ID_Zaehler_BetrZeitVD2": 670, - "ID_Zaehler_BetrZeitZWE1": 671, - "ID_Zaehler_BetrZeitZWE2": 672, - "ID_Zaehler_BetrZeitZWE3": 673, - "ID_Zaehler_BetrZeitImpVD1": 674, - "ID_Zaehler_BetrZeitImpVD2": 675, - "ID_Zaehler_BetrZeitEZMVD1": 676, - "ID_Zaehler_BetrZeitEZMVD2": 677, - "ID_Einst_Entl_Typ_0": 678, - "ID_Einst_Entl_Typ_1": 679, - "ID_Einst_Entl_Typ_2": 680, - "ID_Einst_Entl_Typ_3": 681, - "ID_Einst_Entl_Typ_4": 682, - "ID_Einst_Entl_Typ_5": 683, - "ID_Einst_Entl_Typ_6": 684, - "ID_Einst_Entl_Typ_7": 685, - "ID_Einst_Entl_Typ_8": 686, - "ID_Einst_Entl_Typ_9": 687, - "ID_Einst_Entl_Typ_10": 688, - "ID_Einst_Entl_Typ_11": 689, - "ID_Einst_Entl_Typ_12": 690, - "ID_Einst_Vorl_max_MK1": 691, - "ID_Einst_Vorl_max_MK2": 692, - "ID_SU_FrkdMK1": 693, - "ID_SU_FrkdMK2": 694, - "ID_Ba_Hz_MK1_akt": 695, - "ID_Ba_Hz_MK2_akt": 696, - "ID_Einst_Zirk_Ein_akt": 697, - "ID_Einst_Zirk_Aus_akt": 698, - "ID_Einst_Heizgrenze": 699, - "ID_Einst_Heizgrenze_Temp": 700, - "ID_VariablenIBNgespeichert": 701, - "ID_SchonIBNAssistant": 702, - "ID_Heizgrenze_0": 703, - "ID_Heizgrenze_1": 704, - "ID_Heizgrenze_2": 705, - "ID_Heizgrenze_3": 706, - "ID_Heizgrenze_4": 707, - "ID_Heizgrenze_5": 708, - "ID_Heizgrenze_6": 709, - "ID_Heizgrenze_7": 710, - "ID_Heizgrenze_8": 711, - "ID_Heizgrenze_9": 712, - "ID_Heizgrenze_10": 713, - "ID_Heizgrenze_11": 714, - "ID_SchemenIBNgewahlt": 715, - "ID_Switchoff_file_0_0": 716, - "ID_Switchoff_file_1_0": 717, - "ID_Switchoff_file_2_0": 718, - "ID_Switchoff_file_3_0": 719, - "ID_Switchoff_file_4_0": 720, - "ID_Switchoff_file_0_1": 721, - "ID_Switchoff_file_1_1": 722, - "ID_Switchoff_file_2_1": 723, - "ID_Switchoff_file_3_1": 724, - "ID_Switchoff_file_4_1": 725, - "ID_DauerDatenLoggerAktiv": 726, - "ID_Laufvar_Heizgrenze": 727, - "ID_Zaehler_BetrZeitHz": 728, - "ID_Zaehler_BetrZeitBW": 729, - "ID_Zaehler_BetrZeitKue": 730, - "ID_SU_FstdHz": 731, - "ID_SU_FstdBw": 732, - "ID_SU_FstdSwb": 733, - "ID_SU_FstdMK1": 734, - "ID_SU_FstdMK2": 735, - "ID_FerienAbsenkungHz": 736, - "ID_FerienAbsenkungMK1": 737, - "ID_FerienAbsenkungMK2": 738, - "ID_FerienModusAktivHz": 739, - "ID_FerienModusAktivBw": 740, - "ID_FerienModusAktivSwb": 741, - "ID_FerienModusAktivMk1": 742, - "ID_FerienModusAktivMk2": 743, - "ID_DisplayContrast_akt": 744, - "ID_Ba_Hz_saved": 745, - "ID_Ba_Bw_saved": 746, - "ID_Ba_Sw_saved": 747, - "ID_Ba_Hz_MK1_saved": 748, - "ID_Ba_Hz_MK2_saved": 749, - "ID_AdresseIP_akt": 750, - "ID_SubNetMask_akt": 751, - "ID_Add_Broadcast_akt": 752, - "ID_Add_StdGateway_akt": 753, - "ID_DHCPServerAktiv_akt": 754, - "ID_WebserverPasswort_1_akt": 755, - "ID_WebserverPasswort_2_akt": 756, - "ID_WebserverPasswort_3_akt": 757, - "ID_WebserverPasswort_4_akt": 758, - "ID_WebserverPasswort_5_akt": 759, - "ID_WebserverPasswort_6_akt": 760, - "ID_WebServerWerteBekommen": 761, - "ID_Einst_ParBetr_akt": 762, - "ID_Einst_WpAnz_akt": 763, - "ID_Einst_PhrTime_akt": 764, - "ID_Einst_HysPar_akt": 765, - "ID_IP_PB_Slave_0": 766, - "ID_IP_PB_Slave_1": 767, - "ID_IP_PB_Slave_2": 768, - "ID_IP_PB_Slave_3": 769, - "ID_IP_PB_Slave_4": 770, - "ID_IP_PB_Slave_5": 771, - "ID_Einst_BwHup_akt_backup": 772, - "ID_Einst_SuMk3_akt": 773, - "ID_Einst_HzMK3E_akt": 774, - "ID_Einst_HzMK3ANH_akt": 775, - "ID_Einst_HzMK3ABS_akt": 776, - "ID_Einst_HzMK3Hgr_akt": 777, - "ID_Einst_HzFtMK3Vl_akt": 778, - "ID_Ba_Hz_MK3_akt": 779, - "ID_Einst_MK3Typ_akt": 780, - "ID_Einst_RTypMK3_akt": 781, - "ID_Einst_MK3LzFaktor_akt": 782, - "ID_Einst_MK3PerFaktor_akt": 783, - "ID_FerienModusAktivMk3": 784, - "ID_SU_FrkdMK3": 785, - "ID_FerienAbsenkungMK3": 786, - "ID_SU_FstdMK3": 787, - "ID_Einst_SuMk3_akt2": 788, - "ID_Einst_SuMk3Wo_zeit_0_0": 789, - "ID_Einst_SuMk3Wo_zeit_0_1": 790, - "ID_Einst_SuMk3Wo_zeit_1_0": 791, - "ID_Einst_SuMk3Wo_zeit_1_1": 792, - "ID_Einst_SuMk3Wo_zeit_2_0": 793, - "ID_Einst_SuMk3Wo_zeit_2_1": 794, - "ID_Einst_SuMk325_zeit_0_0": 795, - "ID_Einst_SuMk325_zeit_0_1": 796, - "ID_Einst_SuMk325_zeit_1_0": 797, - "ID_Einst_SuMk325_zeit_1_1": 798, - "ID_Einst_SuMk325_zeit_2_0": 799, - "ID_Einst_SuMk325_zeit_2_1": 800, - "ID_Einst_SuMk325_zeit_0_2": 801, - "ID_Einst_SuMk325_zeit_0_3": 802, - "ID_Einst_SuMk325_zeit_1_2": 803, - "ID_Einst_SuMk325_zeit_1_3": 804, - "ID_Einst_SuMk325_zeit_2_2": 805, - "ID_Einst_SuMk325_zeit_2_3": 806, - "ID_Einst_SuMk3Tg_zeit_0_0": 807, - "ID_Einst_SuMk3Tg_zeit_0_1": 808, - "ID_Einst_SuMk3Tg_zeit_1_0": 809, - "ID_Einst_SuMk3Tg_zeit_1_1": 810, - "ID_Einst_SuMk3Tg_zeit_2_0": 811, - "ID_Einst_SuMk3Tg_zeit_2_1": 812, - "ID_Einst_SuMk3Tg_zeit_0_2": 813, - "ID_Einst_SuMk3Tg_zeit_0_3": 814, - "ID_Einst_SuMk3Tg_zeit_1_2": 815, - "ID_Einst_SuMk3Tg_zeit_1_3": 816, - "ID_Einst_SuMk3Tg_zeit_2_2": 817, - "ID_Einst_SuMk3Tg_zeit_2_3": 818, - "ID_Einst_SuMk3Tg_zeit_0_4": 819, - "ID_Einst_SuMk3Tg_zeit_0_5": 820, - "ID_Einst_SuMk3Tg_zeit_1_4": 821, - "ID_Einst_SuMk3Tg_zeit_1_5": 822, - "ID_Einst_SuMk3Tg_zeit_2_4": 823, - "ID_Einst_SuMk3Tg_zeit_2_5": 824, - "ID_Einst_SuMk3Tg_zeit_0_6": 825, - "ID_Einst_SuMk3Tg_zeit_0_7": 826, - "ID_Einst_SuMk3Tg_zeit_1_6": 827, - "ID_Einst_SuMk3Tg_zeit_1_7": 828, - "ID_Einst_SuMk3Tg_zeit_2_6": 829, - "ID_Einst_SuMk3Tg_zeit_2_7": 830, - "ID_Einst_SuMk3Tg_zeit_0_8": 831, - "ID_Einst_SuMk3Tg_zeit_0_9": 832, - "ID_Einst_SuMk3Tg_zeit_1_8": 833, - "ID_Einst_SuMk3Tg_zeit_1_9": 834, - "ID_Einst_SuMk3Tg_zeit_2_8": 835, - "ID_Einst_SuMk3Tg_zeit_2_9": 836, - "ID_Einst_SuMk3Tg_zeit_0_10": 837, - "ID_Einst_SuMk3Tg_zeit_0_11": 838, - "ID_Einst_SuMk3Tg_zeit_1_10": 839, - "ID_Einst_SuMk3Tg_zeit_1_11": 840, - "ID_Einst_SuMk3Tg_zeit_2_10": 841, - "ID_Einst_SuMk3Tg_zeit_2_11": 842, - "ID_Einst_SuMk3Tg_zeit_0_12": 843, - "ID_Einst_SuMk3Tg_zeit_0_13": 844, - "ID_Einst_SuMk3Tg_zeit_1_12": 845, - "ID_Einst_SuMk3Tg_zeit_1_13": 846, - "ID_Einst_SuMk3Tg_zeit_2_12": 847, - "ID_Einst_SuMk3Tg_zeit_2_13": 848, - "ID_Ba_Hz_MK3_saved": 849, - "ID_Einst_Kuhl_Zeit_Ein_akt": 850, - "ID_Einst_Kuhl_Zeit_Aus_akt": 851, - "ID_Waermemenge_Seit": 852, - "ID_Waermemenge_WQ": 853, - "ID_Waermemenge_Hz": 854, - "ID_Waermemenge_WQ_ges": 855, - "ID_Einst_Entl_Typ_13": 856, - "ID_Einst_Entl_Typ_14": 857, - "ID_Einst_Entl_Typ_15": 858, - "ID_Zaehler_BetrZeitSW": 859, - "ID_Einst_Fernwartung_akt": 860, - "ID_AdresseIPServ_akt": 861, - "ID_Einst_TA_EG_akt": 862, - "ID_Einst_TVLmax_EG_akt": 863, - "ID_Einst_Popt_Nachlauf_akt": 864, - "ID_FernwartungVertrag_akt": 865, - "ID_FernwartungAktuZeit": 866, - "ID_Einst_Effizienzpumpe_Nominal_akt": 867, - "ID_Einst_Effizienzpumpe_Minimal_akt": 868, - "ID_Einst_Effizienzpumpe_akt": 869, - "ID_Einst_Waermemenge_akt": 870, - "ID_Einst_Wm_Versorgung_Korrektur_akt": 871, - "ID_Einst_Wm_Auswertung_Korrektur_akt": 872, - "ID_SoftwareUpdateJetztGemacht_akt": 873, - "ID_WP_SerienNummer_DATUM": 874, - "ID_WP_SerienNummer_HEX": 875, - "ID_WP_SerienNummer_INDEX": 876, - "ID_ProgWerteWebSrvBeobarten": 877, - "ID_Waermemenge_BW": 878, - "ID_Waermemenge_SW": 879, - "ID_Waermemenge_Datum": 880, - "ID_Einst_Solar_akt": 881, - "ID_BSTD_Solar": 882, - "ID_Einst_TDC_Koll_Max_akt": 883, - "ID_Einst_Akt_Kuehlung_akt": 884, - "ID_Einst_Vorlauf_VBO_akt": 885, - "ID_Einst_KRHyst_akt": 886, - "ID_Einst_Akt_Kuehl_Speicher_min_akt": 887, - "ID_Einst_Akt_Kuehl_Freig_WQE_akt": 888, - "ID_NDAB_WW_Anzahl": 889, - "ID_NDS_WW_KD_Quitt": 890, - "ID_Einst_AbtZykMin_akt": 891, - "ID_Einst_VD2_Zeit_Min_akt": 892, - "ID_Einst_Hysterese_HR_verkuerzt_akt": 893, - "ID_Einst_BA_Lueftung_akt": 894, - "ID_Einst_SuLuf_akt": 895, - "ID_Einst_SuLufWo_zeit_0_0_0": 896, - "ID_Einst_SuLufWo_zeit_0_1_0": 897, - "ID_Einst_SuLufWo_zeit_0_2_0": 898, - "ID_Einst_SuLuf25_zeit_0_0_0": 899, - "ID_Einst_SuLuf25_zeit_0_1_0": 900, - "ID_Einst_SuLuf25_zeit_0_2_0": 901, - "ID_Einst_SuLuf25_zeit_0_0_2": 902, - "ID_Einst_SuLuf25_zeit_0_1_2": 903, - "ID_Einst_SuLuf25_zeit_0_2_2": 904, - "ID_Einst_SuLufTg_zeit_0_0_0": 905, - "ID_Einst_SuLufTg_zeit_0_1_0": 906, - "ID_Einst_SuLufTg_zeit_0_2_0": 907, - "ID_Einst_SuLufTg_zeit_0_0_2": 908, - "ID_Einst_SuLufTg_zeit_0_1_2": 909, - "ID_Einst_SuLufTg_zeit_0_2_2": 910, - "ID_Einst_SuLufTg_zeit_0_0_4": 911, - "ID_Einst_SuLufTg_zeit_0_1_4": 912, - "ID_Einst_SuLufTg_zeit_0_2_4": 913, - "ID_Einst_SuLufTg_zeit_0_0_6": 914, - "ID_Einst_SuLufTg_zeit_0_1_6": 915, - "ID_Einst_SuLufTg_zeit_0_2_6": 916, - "ID_Einst_SuLufTg_zeit_0_0_8": 917, - "ID_Einst_SuLufTg_zeit_0_1_8": 918, - "ID_Einst_SuLufTg_zeit_0_2_8": 919, - "ID_Einst_SuLufTg_zeit_0_0_10": 920, - "ID_Einst_SuLufTg_zeit_0_1_10": 921, - "ID_Einst_SuLufTg_zeit_0_2_10": 922, - "ID_Einst_SuLufTg_zeit_0_0_12": 923, - "ID_Einst_SuLufTg_zeit_0_1_12": 924, - "ID_Einst_SuLufTg_zeit_0_2_12": 925, - "ID_Einst_SuLufWo_zeit_1_0_0": 926, - "ID_Einst_SuLufWo_zeit_1_1_0": 927, - "ID_Einst_SuLufWo_zeit_1_2_0": 928, - "ID_Einst_SuLuf25_zeit_1_0_0": 929, - "ID_Einst_SuLuf25_zeit_1_1_0": 930, - "ID_Einst_SuLuf25_zeit_1_2_0": 931, - "ID_Einst_SuLuf25_zeit_1_0_2": 932, - "ID_Einst_SuLuf25_zeit_1_1_2": 933, - "ID_Einst_SuLuf25_zeit_1_2_2": 934, - "ID_Einst_SuLufTg_zeit_1_0_0": 935, - "ID_Einst_SuLufTg_zeit_1_1_0": 936, - "ID_Einst_SuLufTg_zeit_1_2_0": 937, - "ID_Einst_SuLufTg_zeit_1_0_2": 938, - "ID_Einst_SuLufTg_zeit_1_1_2": 939, - "ID_Einst_SuLufTg_zeit_1_2_2": 940, - "ID_Einst_SuLufTg_zeit_1_0_4": 941, - "ID_Einst_SuLufTg_zeit_1_1_4": 942, - "ID_Einst_SuLufTg_zeit_1_2_4": 943, - "ID_Einst_SuLufTg_zeit_1_0_6": 944, - "ID_Einst_SuLufTg_zeit_1_1_6": 945, - "ID_Einst_SuLufTg_zeit_1_2_6": 946, - "ID_Einst_SuLufTg_zeit_1_0_8": 947, - "ID_Einst_SuLufTg_zeit_1_1_8": 948, - "ID_Einst_SuLufTg_zeit_1_2_8": 949, - "ID_Einst_SuLufTg_zeit_1_0_10": 950, - "ID_Einst_SuLufTg_zeit_1_1_10": 951, - "ID_Einst_SuLufTg_zeit_1_2_10": 952, - "ID_Einst_SuLufTg_zeit_1_0_12": 953, - "ID_Einst_SuLufTg_zeit_1_1_12": 954, - "ID_Einst_SuLufTg_zeit_1_2_12": 955, - "ID_FerienModusAktivLueftung": 956, - "ID_Einst_BA_Lueftung_saved": 957, - "ID_SU_FrkdLueftung": 958, - "ID_SU_FstdLueftung": 959, - "ID_Einst_Luf_Feuchteschutz_akt": 960, - "ID_Einst_Luf_Reduziert_akt": 961, - "ID_Einst_Luf_Nennlueftung_akt": 962, - "ID_Einst_Luf_Intensivlueftung_akt": 963, - "ID_Timer_Fil_4Makt": 964, - "ID_Timer_Fil_WoAkt": 965, - "ID_Sollwert_KuCft3_akt": 966, - "ID_Sollwert_AtDif3_akt": 967, - "ID_Bitmaske_0": 968, - "ID_Einst_Lueftungsstufen": 969, - "ID_SysEin_Meldung_TDI": 970, - "ID_SysEin_Typ_WZW": 971, - "ID_Einst_GLT_aktiviert": 972, - "ID_Einst_BW_max": 973, - "ID_Einst_Sollwert_TRL_Kuehlen": 974, - "ID_Einst_Medium_Waermequelle": 975, - "ID_Einst_Photovoltaik_akt": 976, - "ID_Einst_Multispeicher_akt": 977, - "ID_Einst_PKuehlTime_akt": 978, - "ID_Einst_Minimale_Ruecklaufsolltemperatur": 979, - "ID_RBE_Einflussfaktor_RT_akt": 980, - "ID_RBE_Freigabe_Kuehlung_akt": 981, - "ID_RBE_Waermeverteilsystem_akt": 982, - "ID_RBE_Zeit_Heizstab_aktiv": 983, - "ID_SEC_ND_Alarmgrenze": 984, - "ID_SEC_HD_Alarmgrenze": 985, - "ID_SEC_Abtauendtemperatur": 986, - "ID_Einst_Min_RPM_BW": 987, - "ID_Einst_Luf_Feuchteschutz_Faktor_akt": 988, - "ID_Einst_Luf_Reduziert_Faktor_akt": 989, - "ID_Einst_Luf_Nennlueftung_Faktor_akt": 990, - "ID_Einst_Luf_Intensivlueftung_Faktor_akt": 991, - "ID_Einst_Freigabe_Zeit_ZWE": 992, - "ID_Einst_min_VL_Kuehl": 993, - "ID_Einst_Warmwasser_Nachheizung": 994, - "ID_Switchoff_file_LWD2_0_0": 995, - "ID_Switchoff_file_LWD2_1_0": 996, - "ID_Switchoff_file_LWD2_2_0": 997, - "ID_Switchoff_file_LWD2_3_0": 998, - "ID_Switchoff_file_LWD2_4_0": 999, - "ID_Switchoff_file_LWD2_0_1": 1000, - "ID_Switchoff_file_LWD2_1_1": 1001, - "ID_Switchoff_file_LWD2_2_1": 1002, - "ID_Switchoff_file_LWD2_3_1": 1003, - "ID_Switchoff_file_LWD2_4_1": 1004, - "ID_Switchoff_index_LWD2": 1005, - "ID_Einst_Effizienzpumpe_Nominal_2": 1006, - "ID_Einst_Effizienzpumpe_Minimal_2": 1007, - "ID_Einst_Wm_Versorgung_Korrektur_2": 1008, - "ID_Einst_Wm_Auswertung_Korrektur_2": 1009, - "ID_Einst_isTwin": 1010, - "ID_Einst_TAmin_2": 1011, - "ID_Einst_TVLmax_2": 1012, - "ID_Einst_TA_EG_2": 1013, - "ID_Einst_TVLmax_EG_2": 1014, - "ID_Waermemenge_Hz_2": 1015, - "ID_Waermemenge_BW_2": 1016, - "ID_Waermemenge_SW_2": 1017, - "ID_Waermemenge_Seit_2": 1018, - "ID_Einst_Entl_Typ_15_2": 1019, - "ID_Einst_WW_Nachheizung_max": 1020, - "ID_Einst_Kuhl_Zeit_Ein_RT": 1021, - "ID_Einst_ZWE1_Pos": 1022, - "ID_Einst_ZWE2_Pos": 1023, - "ID_Einst_ZWE3_Pos": 1024, - "ID_Einst_Leistung_ZWE": 1025, - "ID_WP_SN2_DATUM": 1026, - "ID_WP_SN2_HEX": 1027, - "ID_WP_SN2_INDEX": 1028, - "ID_CWP_saved2": 1029, - "ID_Einst_SmartGrid": 1030, - "ID_Einst_P155_HDS": 1031, - "ID_Einst_P155_PumpHeat_Max": 1032, - "ID_Einst_P155_PumpHeatCtrl": 1033, - "ID_Einst_P155_PumpDHWCtrl": 1034, - "ID_Einst_P155_PumpDHW_RPM": 1035, - "ID_Einst_P155_PumpPoolCtrl": 1036, - "ID_Einst_P155_PumpPool_RPM": 1037, - "ID_Einst_P155_PumpCool_RPM": 1038, - "ID_Einst_P155_PumpVBOCtrl": 1039, - "ID_Einst_P155_PumpVBO_RPM_C": 1040, - "ID_Einst_P155_PumpDHW_Max": 1041, - "ID_Einst_P155_PumpPool_Max": 1042, - "ID_Einst_P155_Sperrband_1": 1043, - "ID_Einst_P155_Leistungsfreigabe": 1044, - "ID_Einst_P155_DHW_Freq": 1045, - "ID_Einst_SWHUP": 1046, - "ID_Einst_P155_SWB_Freq": 1047, - "ID_Einst_MK1_Regelung": 1048, - "ID_Einst_MK2_Regelung": 1049, - "ID_Einst_MK3_Regelung": 1050, - "ID_Einst_PV_WW_Sperrzeit": 1051, - "ID_Einst_Warmwasser_extra": 1052, - "ID_Einst_Vorl_akt_Kuehl": 1053, - "ID_WP_SN3_DATUM": 1054, - "ID_WP_SN3_HEX": 1055, - "ID_WP_SN3_INDEX": 1056, - "ID_Einst_Vorlauf_ZUP": 1057, - "ID_Einst_Abtauen_im_Warmwasser": 1058, - "ID_Waermemenge_ZWE": 1059, - "ID_Waermemenge_Reset": 1060, - "ID_Waermemenge_Reset_2": 1061, - "ID_Einst_Brunnenpumpe_min": 1062, - "ID_Einst_Brunnenpumpe_max": 1063, - "ID_Einst_SmartHomeID": 1064, - "ID_Einst_SmartHK": 1065, - "ID_Einst_SmartMK1": 1066, - "ID_Einst_SmartMK2": 1067, - "ID_Einst_SmartMK3": 1068, - "ID_Einst_SmartWW": 1069, - "ID_Einst_SmartDefrost": 1070, - "ID_Einst_Empty1071": 1071, - "ID_Einst_MinVLMK1": 1072, - "ID_Einst_MinVLMK2": 1073, - "ID_Einst_MinVLMK3": 1074, - "ID_Einst_MaxVLMK1": 1075, - "ID_Einst_MaxVLMK2": 1076, - "ID_Einst_MaxVLMK3": 1077, - "ID_Einst_SmartPlusHz": 1078, - "ID_Einst_SmartMinusHz": 1079, - "ID_Einst_SmartPlusMK1": 1080, - "ID_Einst_SmartMinusMK1": 1081, - "ID_Einst_SmartPlusMK2": 1082, - "ID_Einst_SmartMinusMK2": 1083, - "ID_Einst_SmartPlusMK3": 1084, - "ID_Einst_SmartMinusMK3": 1085, - "Unknown_Parameter_1086": 1086, - "Unknown_Parameter_1087": 1087, - "Unknown_Parameter_1088": 1088, - "Unknown_Parameter_1089": 1089, - "Unknown_Parameter_1090": 1090, - "Unknown_Parameter_1091": 1091, - "Unknown_Parameter_1092": 1092, - "Unknown_Parameter_1093": 1093, - "Unknown_Parameter_1094": 1094, - "Unknown_Parameter_1095": 1095, - "Unknown_Parameter_1096": 1096, - "Unknown_Parameter_1097": 1097, - "Unknown_Parameter_1098": 1098, - "Unknown_Parameter_1099": 1099, - "Unknown_Parameter_1100": 1100, - "Unknown_Parameter_1101": 1101, - "Unknown_Parameter_1102": 1102, - "Unknown_Parameter_1103": 1103, - "Unknown_Parameter_1104": 1104, - "Unknown_Parameter_1105": 1105, - "Unknown_Parameter_1106": 1106, - "Unknown_Parameter_1107": 1107, - "Unknown_Parameter_1108": 1108, - "Unknown_Parameter_1109": 1109, - "Unknown_Parameter_1110": 1110, - "Unknown_Parameter_1111": 1111, - "Unknown_Parameter_1112": 1112, - "Unknown_Parameter_1113": 1113, - "Unknown_Parameter_1114": 1114, - "Unknown_Parameter_1115": 1115, - "Unknown_Parameter_1116": 1116, - "Unknown_Parameter_1117": 1117, - "Unknown_Parameter_1118": 1118, - "Unknown_Parameter_1119": 1119, - "Unknown_Parameter_1120": 1120, - "Unknown_Parameter_1121": 1121, - "Unknown_Parameter_1122": 1122, - "Unknown_Parameter_1123": 1123, - "Unknown_Parameter_1124": 1124, - "Unknown_Parameter_1125": 1125, + "ID_Transfert_LuxNet": (0, Unknown), + "ID_Einst_WK_akt": (1, Celsius), + "ID_Einst_BWS_akt": (2, Celsius), + "ID_Ba_Hz_akt": (3, HeatingMode), + "ID_Ba_Bw_akt": (4, HotWaterMode), + "ID_Ba_Al_akt": (5, Unknown), + "ID_SU_FrkdHz": (6, Unknown), + "ID_SU_FrkdBw": (7, Unknown), + "ID_SU_FrkdAl": (8, Unknown), + "ID_Einst_HReg_akt": (9, Unknown), + "ID_Einst_HzHwMAt_akt": (10, Unknown), + "ID_Einst_HzHwHKE_akt": (11, Celsius), + "ID_Einst_HzHKRANH_akt": (12, Celsius), + "ID_Einst_HzHKRABS_akt": (13, Celsius), + "ID_Einst_HzMK1E_akt": (14, Unknown), + "ID_Einst_HzMK1ANH_akt": (15, Unknown), + "ID_Einst_HzMK1ABS_akt": (16, Unknown), + "ID_Einst_HzFtRl_akt": (17, Unknown), + "ID_Einst_HzFtMK1Vl_akt": (18, Unknown), + "ID_Einst_SUBW_akt": (19, Unknown), + "ID_Einst_BwTDI_akt_MO": (20, Unknown), + "ID_Einst_BwTDI_akt_DI": (21, Unknown), + "ID_Einst_BwTDI_akt_MI": (22, Unknown), + "ID_Einst_BwTDI_akt_DO": (23, Unknown), + "ID_Einst_BwTDI_akt_FR": (24, Unknown), + "ID_Einst_BwTDI_akt_SA": (25, Unknown), + "ID_Einst_BwTDI_akt_SO": (26, Unknown), + "ID_Einst_BwTDI_akt_AL": (27, Unknown), + "ID_Einst_AnlKonf_akt": (28, Unknown), + "ID_Einst_Sprache_akt": (29, Unknown), + "ID_Switchoff_Zahler": (30, Unknown), + "ID_Switchoff_index": (31, Unknown), + "ID_Einst_EvuTyp_akt": (32, Unknown), + "ID_Einst_RFVEinb_akt": (33, Unknown), + "ID_Einst_AbtZykMax_akt": (34, Unknown), + "ID_Einst_HREinb_akt": (35, Unknown), + "ID_Einst_ZWE1Art_akt": (36, Unknown), + "ID_Einst_ZWE1Fkt_akt": (37, Unknown), + "ID_Einst_ZWE2Art_akt": (38, Unknown), + "ID_Einst_ZWE2Fkt_akt": (39, Unknown), + "ID_Einst_BWBer_akt": (40, Unknown), + "ID_Einst_En_Inst": (41, Unknown), + "ID_Einst_MK1Typ_akt": (42, Unknown), + "ID_Einst_ABTLuft_akt": (43, Unknown), + "ID_Einst_TLAbt_akt": (44, Unknown), + "ID_Einst_LAbtTime_akt": (45, Unknown), + "ID_Einst_ASDTyp_akt": (46, Unknown), + "ID_Einst_LGST_akt": (47, Unknown), + "ID_Einst_BwWpTime_akt": (48, Unknown), + "ID_Einst_Popt_akt": (49, Unknown), + "ID_Einst_Kurzprog_akt": (50, Unknown), + "ID_Timer_Kurzprog_akt": (51, Unknown), + "ID_Einst_ManAbt_akt": (52, Unknown), + "ID_Einst_Ahz_akt": (53, Unknown), + "ID_Einst_TVL_Ahz_1": (54, Unknown), + "ID_Einst_TVL_Ahz_2": (55, Unknown), + "ID_Einst_TVL_Ahz_3": (56, Unknown), + "ID_Einst_TVL_Ahz_4": (57, Unknown), + "ID_Einst_TVL_Ahz_5": (58, Unknown), + "ID_Einst_TVL_Ahz_6": (59, Unknown), + "ID_Einst_TVL_Ahz_7": (60, Unknown), + "ID_Einst_TVL_Ahz_8": (61, Unknown), + "ID_Einst_TVL_Ahz_9": (62, Unknown), + "ID_Einst_TVL_Ahz_10": (63, Unknown), + "ID_Einst_TVL_Std_1": (64, Unknown), + "ID_Einst_TVL_Std_2": (65, Unknown), + "ID_Einst_TVL_Std_3": (66, Unknown), + "ID_Einst_TVL_Std_4": (67, Unknown), + "ID_Einst_TVL_Std_5": (68, Unknown), + "ID_Einst_TVL_Std_6": (69, Unknown), + "ID_Einst_TVL_Std_7": (70, Unknown), + "ID_Einst_TVL_Std_8": (71, Unknown), + "ID_Einst_TVL_Std_9": (72, Unknown), + "ID_Einst_TVL_Std_10": (73, Unknown), + "ID_Einst_BWS_Hyst_akt": (74, Kelvin), + "ID_Temp_TBW_BwHD_saved": (75, Unknown), + "ID_Einst_ABT1_akt": (76, Unknown), + "ID_Einst_LABTpaus_akt": (77, Unknown), + "ID_AHZ_state_akt": (78, Unknown), + "ID_Sollwert_TRL_HZ_AHZ": (79, Celsius), + "ID_AHP_valid_records": (80, Unknown), + "ID_Timer_AHZ_akt": (81, Unknown), + "ID_Einst_BWTINP_akt": (82, Unknown), + "ID_Einst_ZUPTYP_akt": (83, Unknown), + "ID_Sollwert_TLG_max": (84, Unknown), + "ID_Einst_BWZIP_akt": (85, Unknown), + "ID_Einst_ERRmZWE_akt": (86, Unknown), + "ID_Einst_TRBegr_akt": (87, Unknown), + "ID_Einst_HRHyst_akt": (88, Unknown), + "ID_Einst_TRErhmax_akt": (89, Unknown), + "ID_Einst_ZWEFreig_akt": (90, Unknown), + "ID_Einst_TAmax_akt": (91, Unknown), + "ID_Einst_TAmin_akt": (92, Unknown), + "ID_Einst_TWQmin_akt": (93, Unknown), + "ID_Einst_THGmax_akt": (94, Unknown), + "ID_Einst_FRGT2VD_akt": (95, Unknown), + "ID_Einst_TV2VDBW_akt": (96, Unknown), + "ID_Einst_SuAll_akt": (97, Unknown), + "ID_Einst_TAbtEnd_akt": (98, Unknown), + "ID_Einst_NrKlingel_akt": (99, Unknown), + "ID_Einst_BWStyp_akt": (100, Unknown), + "ID_Einst_ABT2_akt": (101, Unknown), + "ID_Einst_UeVd_akt": (102, Unknown), + "ID_Einst_RTyp_akt": (103, Unknown), + "ID_Einst_AhpM_akt": (104, Unknown), + "ID_Soll_BWS_akt": (105, Celsius), + "ID_Timer_Password": (106, Unknown), + "ID_Einst_Zugangscode": (107, Unknown), + "ID_Einst_BA_Kuehl_akt": (108, CoolingMode), + "ID_Sollwert_Kuehl1_akt": (109, Unknown), + "ID_Einst_KuehlFreig_akt": (110, Celsius), + "ID_Einst_TAbsMin_akt": (111, Unknown), + "ID_TWQmin_saved": (112, Unknown), + "ID_CWP_saved": (113, Unknown), + "ID_Einst_Anode_akt": (114, Unknown), + "ID_Timer_pexoff_akt": (115, Unknown), + "ID_Einst_AnlPrio_Hzakt": (116, Unknown), + "ID_Einst_AnlPrio_Bwakt": (117, Unknown), + "ID_Einst_AnlPrio_Swakt": (118, Unknown), + "ID_Ba_Sw_akt": (119, PoolMode), + "ID_Einst_RTypMK1_akt": (120, Unknown), + "ID_Einst_RTypMK2_akt": (121, Unknown), + "ID_Einst_TDC_Ein_akt": (122, Kelvin), + "ID_Einst_TDC_Aus_akt": (123, Kelvin), + "ID_Einst_TDC_Max_akt": (124, Celsius), + "ID_Einst_HysHzExEn_akt": (125, Unknown), + "ID_Einst_HysBwExEn_akt": (126, Unknown), + "ID_Einst_ZWE3Art_akt": (127, Unknown), + "ID_Einst_ZWE3Fkt_akt": (128, Unknown), + "ID_Einst_HzSup_akt": (129, Unknown), + "ID_Einst_MK2Typ_akt": (130, Unknown), + "ID_Einst_KuTyp_akt": (131, Unknown), + "ID_Sollwert_KuCft1_akt": (132, Celsius), + "ID_Sollwert_KuCft2_akt": (133, Celsius), + "ID_Sollwert_AtDif1_akt": (134, Celsius), + "ID_Sollwert_AtDif2_akt": (135, Celsius), + "ID_SU_FrkdSwb": (136, Unknown), + "ID_Einst_SwbBer_akt": (137, Unknown), + "ID_Einst_TV2VDSWB_akt": (138, Unknown), + "ID_Einst_MinSwan_Time_akt": (139, Unknown), + "ID_Einst_SuMk2_akt": (140, Unknown), + "ID_Einst_HzMK2E_akt": (141, Unknown), + "ID_Einst_HzMK2ANH_akt": (142, Unknown), + "ID_Einst_HzMK2ABS_akt": (143, Unknown), + "ID_Einst_HzMK2Hgr_akt": (144, Unknown), + "ID_Einst_HzFtMK2Vl_akt": (145, Unknown), + "ID_Temp_THG_BwHD_saved": (146, Unknown), + "ID_Temp_TA_BwHD_saved": (147, Unknown), + "ID_Einst_BwHup_akt": (148, Unknown), + "ID_Einst_TVLmax_akt": (149, Unknown), + "ID_Einst_MK1LzFaktor_akt": (150, Unknown), + "ID_Einst_MK2LzFaktor_akt": (151, Unknown), + "ID_Einst_MK1PerFaktor_akt": (152, Unknown), + "ID_Einst_MK2PerFaktor_akt": (153, Unknown), + "ID_Entl_Zyklus_akt": (154, Unknown), + "ID_Einst_Entl_time_akt": (155, Unknown), + "ID_Entl_Pause": (156, Unknown), + "ID_Entl_timer": (157, Unknown), + "ID_Einst_Entl_akt": (158, Unknown), + "ID_Ahz_HLeist_confirmed": (159, Unknown), + "ID_FirstInit_akt": (160, Unknown), + "ID_Einst_SuAll_akt2": (161, Unknown), + "ID_Einst_SuAllWo_zeit_0_0": (162, Unknown), + "ID_Einst_SuAllWo_zeit_0_1": (163, Unknown), + "ID_Einst_SuAllWo_zeit_1_0": (164, Unknown), + "ID_Einst_SuAllWo_zeit_1_1": (165, Unknown), + "ID_Einst_SuAllWo_zeit_2_0": (166, Unknown), + "ID_Einst_SuAllWo_zeit_2_1": (167, Unknown), + "ID_Einst_SuAll25_zeit_0_0": (168, Unknown), + "ID_Einst_SuAll25_zeit_0_1": (169, Unknown), + "ID_Einst_SuAll25_zeit_1_0": (170, Unknown), + "ID_Einst_SuAll25_zeit_1_1": (171, Unknown), + "ID_Einst_SuAll25_zeit_2_0": (172, Unknown), + "ID_Einst_SuAll25_zeit_2_1": (173, Unknown), + "ID_Einst_SuAll25_zeit_0_2": (174, Unknown), + "ID_Einst_SuAll25_zeit_0_3": (175, Unknown), + "ID_Einst_SuAll25_zeit_1_2": (176, Unknown), + "ID_Einst_SuAll25_zeit_1_3": (177, Unknown), + "ID_Einst_SuAll25_zeit_2_2": (178, Unknown), + "ID_Einst_SuAll25_zeit_2_3": (179, Unknown), + "ID_Einst_SuAllTg_zeit_0_0": (180, Unknown), + "ID_Einst_SuAllTg_zeit_0_1": (181, Unknown), + "ID_Einst_SuAllTg_zeit_1_0": (182, Unknown), + "ID_Einst_SuAllTg_zeit_1_1": (183, Unknown), + "ID_Einst_SuAllTg_zeit_2_0": (184, Unknown), + "ID_Einst_SuAllTg_zeit_2_1": (185, Unknown), + "ID_Einst_SuAllTg_zeit_0_2": (186, Unknown), + "ID_Einst_SuAllTg_zeit_0_3": (187, Unknown), + "ID_Einst_SuAllTg_zeit_1_2": (188, Unknown), + "ID_Einst_SuAllTg_zeit_1_3": (189, Unknown), + "ID_Einst_SuAllTg_zeit_2_2": (190, Unknown), + "ID_Einst_SuAllTg_zeit_2_3": (191, Unknown), + "ID_Einst_SuAllTg_zeit_0_4": (192, Unknown), + "ID_Einst_SuAllTg_zeit_0_5": (193, Unknown), + "ID_Einst_SuAllTg_zeit_1_4": (194, Unknown), + "ID_Einst_SuAllTg_zeit_1_5": (195, Unknown), + "ID_Einst_SuAllTg_zeit_2_4": (196, Unknown), + "ID_Einst_SuAllTg_zeit_2_5": (197, Unknown), + "ID_Einst_SuAllTg_zeit_0_6": (198, Unknown), + "ID_Einst_SuAllTg_zeit_0_7": (199, Unknown), + "ID_Einst_SuAllTg_zeit_1_6": (200, Unknown), + "ID_Einst_SuAllTg_zeit_1_7": (201, Unknown), + "ID_Einst_SuAllTg_zeit_2_6": (202, Unknown), + "ID_Einst_SuAllTg_zeit_2_7": (203, Unknown), + "ID_Einst_SuAllTg_zeit_0_8": (204, Unknown), + "ID_Einst_SuAllTg_zeit_0_9": (205, Unknown), + "ID_Einst_SuAllTg_zeit_1_8": (206, Unknown), + "ID_Einst_SuAllTg_zeit_1_9": (207, Unknown), + "ID_Einst_SuAllTg_zeit_2_8": (208, Unknown), + "ID_Einst_SuAllTg_zeit_2_9": (209, Unknown), + "ID_Einst_SuAllTg_zeit_0_10": (210, Unknown), + "ID_Einst_SuAllTg_zeit_0_11": (211, Unknown), + "ID_Einst_SuAllTg_zeit_1_10": (212, Unknown), + "ID_Einst_SuAllTg_zeit_1_11": (213, Unknown), + "ID_Einst_SuAllTg_zeit_2_10": (214, Unknown), + "ID_Einst_SuAllTg_zeit_2_11": (215, Unknown), + "ID_Einst_SuAllTg_zeit_0_12": (216, Unknown), + "ID_Einst_SuAllTg_zeit_0_13": (217, Unknown), + "ID_Einst_SuAllTg_zeit_1_12": (218, Unknown), + "ID_Einst_SuAllTg_zeit_1_13": (219, Unknown), + "ID_Einst_SuAllTg_zeit_2_12": (220, Unknown), + "ID_Einst_SuAllTg_zeit_2_13": (221, Unknown), + "ID_Einst_SuHkr_akt": (222, Unknown), + "ID_Einst_SuHkrW0_zeit_0_0": (223, Unknown), + "ID_Einst_SuHkrW0_zeit_0_1": (224, Unknown), + "ID_Einst_SuHkrW0_zeit_1_0": (225, Unknown), + "ID_Einst_SuHkrW0_zeit_1_1": (226, Unknown), + "ID_Einst_SuHkrW0_zeit_2_0": (227, Unknown), + "ID_Einst_SuHkrW0_zeit_2_1": (228, Unknown), + "ID_Einst_SuHkr25_zeit_0_0": (229, Unknown), + "ID_Einst_SuHkr25_zeit_0_1": (230, Unknown), + "ID_Einst_SuHkr25_zeit_1_0": (231, Unknown), + "ID_Einst_SuHkr25_zeit_1_1": (232, Unknown), + "ID_Einst_SuHkr25_zeit_2_0": (233, Unknown), + "ID_Einst_SuHkr25_zeit_2_1": (234, Unknown), + "ID_Einst_SuHkr25_zeit_0_2": (235, Unknown), + "ID_Einst_SuHkr25_zeit_0_3": (236, Unknown), + "ID_Einst_SuHkr25_zeit_1_2": (237, Unknown), + "ID_Einst_SuHkr25_zeit_1_3": (238, Unknown), + "ID_Einst_SuHkr25_zeit_2_2": (239, Unknown), + "ID_Einst_SuHkr25_zeit_2_3": (240, Unknown), + "ID_Einst_SuHkrTG_zeit_0_0": (241, Unknown), + "ID_Einst_SuHkrTG_zeit_0_1": (242, Unknown), + "ID_Einst_SuHkrTG_zeit_1_0": (243, Unknown), + "ID_Einst_SuHkrTG_zeit_1_1": (244, Unknown), + "ID_Einst_SuHkrTG_zeit_2_0": (245, Unknown), + "ID_Einst_SuHkrTG_zeit_2_1": (246, Unknown), + "ID_Einst_SuHkrTG_zeit_0_2": (247, Unknown), + "ID_Einst_SuHkrTG_zeit_0_3": (248, Unknown), + "ID_Einst_SuHkrTG_zeit_1_2": (249, Unknown), + "ID_Einst_SuHkrTG_zeit_1_3": (250, Unknown), + "ID_Einst_SuHkrTG_zeit_2_2": (251, Unknown), + "ID_Einst_SuHkrTG_zeit_2_3": (252, Unknown), + "ID_Einst_SuHkrTG_zeit_0_4": (253, Unknown), + "ID_Einst_SuHkrTG_zeit_0_5": (254, Unknown), + "ID_Einst_SuHkrTG_zeit_1_4": (255, Unknown), + "ID_Einst_SuHkrTG_zeit_1_5": (256, Unknown), + "ID_Einst_SuHkrTG_zeit_2_4": (257, Unknown), + "ID_Einst_SuHkrTG_zeit_2_5": (258, Unknown), + "ID_Einst_SuHkrTG_zeit_0_6": (259, Unknown), + "ID_Einst_SuHkrTG_zeit_0_7": (260, Unknown), + "ID_Einst_SuHkrTG_zeit_1_6": (261, Unknown), + "ID_Einst_SuHkrTG_zeit_1_7": (262, Unknown), + "ID_Einst_SuHkrTG_zeit_2_6": (263, Unknown), + "ID_Einst_SuHkrTG_zeit_2_7": (264, Unknown), + "ID_Einst_SuHkrTG_zeit_0_8": (265, Unknown), + "ID_Einst_SuHkrTG_zeit_0_9": (266, Unknown), + "ID_Einst_SuHkrTG_zeit_1_8": (267, Unknown), + "ID_Einst_SuHkrTG_zeit_1_9": (268, Unknown), + "ID_Einst_SuHkrTG_zeit_2_8": (269, Unknown), + "ID_Einst_SuHkrTG_zeit_2_9": (270, Unknown), + "ID_Einst_SuHkrTG_zeit_0_10": (271, Unknown), + "ID_Einst_SuHkrTG_zeit_0_11": (272, Unknown), + "ID_Einst_SuHkrTG_zeit_1_10": (273, Unknown), + "ID_Einst_SuHkrTG_zeit_1_11": (274, Unknown), + "ID_Einst_SuHkrTG_zeit_2_10": (275, Unknown), + "ID_Einst_SuHkrTG_zeit_2_11": (276, Unknown), + "ID_Einst_SuHkrTG_zeit_0_12": (277, Unknown), + "ID_Einst_SuHkrTG_zeit_0_13": (278, Unknown), + "ID_Einst_SuHkrTG_zeit_1_12": (279, Unknown), + "ID_Einst_SuHkrTG_zeit_1_13": (280, Unknown), + "ID_Einst_SuHkrTG_zeit_2_12": (281, Unknown), + "ID_Einst_SuHkrTG_zeit_2_13": (282, Unknown), + "ID_Einst_SuMk1_akt": (283, Unknown), + "ID_Einst_SuMk1W0_zeit_0_0": (284, Unknown), + "ID_Einst_SuMk1W0_zeit_0_1": (285, Unknown), + "ID_Einst_SuMk1W0_zeit_1_0": (286, Unknown), + "ID_Einst_SuMk1W0_zeit_1_1": (287, Unknown), + "ID_Einst_SuMk1W0_zeit_2_0": (288, Unknown), + "ID_Einst_SuMk1W0_zeit_2_1": (289, Unknown), + "ID_Einst_SuMk125_zeit_0_0": (290, Unknown), + "ID_Einst_SuMk125_zeit_0_1": (291, Unknown), + "ID_Einst_SuMk125_zeit_1_0": (292, Unknown), + "ID_Einst_SuMk125_zeit_1_1": (293, Unknown), + "ID_Einst_SuMk125_zeit_2_0": (294, Unknown), + "ID_Einst_SuMk125_zeit_2_1": (295, Unknown), + "ID_Einst_SuMk125_zeit_0_2": (296, Unknown), + "ID_Einst_SuMk125_zeit_0_3": (297, Unknown), + "ID_Einst_SuMk125_zeit_1_2": (298, Unknown), + "ID_Einst_SuMk125_zeit_1_3": (299, Unknown), + "ID_Einst_SuMk125_zeit_2_2": (300, Unknown), + "ID_Einst_SuMk125_zeit_2_3": (301, Unknown), + "ID_Einst_SuMk1TG_zeit_0_0": (302, Unknown), + "ID_Einst_SuMk1TG_zeit_0_1": (303, Unknown), + "ID_Einst_SuMk1TG_zeit_1_0": (304, Unknown), + "ID_Einst_SuMk1TG_zeit_1_1": (305, Unknown), + "ID_Einst_SuMk1TG_zeit_2_0": (306, Unknown), + "ID_Einst_SuMk1TG_zeit_2_1": (307, Unknown), + "ID_Einst_SuMk1TG_zeit_0_2": (308, Unknown), + "ID_Einst_SuMk1TG_zeit_0_3": (309, Unknown), + "ID_Einst_SuMk1TG_zeit_1_2": (310, Unknown), + "ID_Einst_SuMk1TG_zeit_1_3": (311, Unknown), + "ID_Einst_SuMk1TG_zeit_2_2": (312, Unknown), + "ID_Einst_SuMk1TG_zeit_2_3": (313, Unknown), + "ID_Einst_SuMk1TG_zeit_0_4": (314, Unknown), + "ID_Einst_SuMk1TG_zeit_0_5": (315, Unknown), + "ID_Einst_SuMk1TG_zeit_1_4": (316, Unknown), + "ID_Einst_SuMk1TG_zeit_1_5": (317, Unknown), + "ID_Einst_SuMk1TG_zeit_2_4": (318, Unknown), + "ID_Einst_SuMk1TG_zeit_2_5": (319, Unknown), + "ID_Einst_SuMk1TG_zeit_0_6": (320, Unknown), + "ID_Einst_SuMk1TG_zeit_0_7": (321, Unknown), + "ID_Einst_SuMk1TG_zeit_1_6": (322, Unknown), + "ID_Einst_SuMk1TG_zeit_1_7": (323, Unknown), + "ID_Einst_SuMk1TG_zeit_2_6": (324, Unknown), + "ID_Einst_SuMk1TG_zeit_2_7": (325, Unknown), + "ID_Einst_SuMk1TG_zeit_0_8": (326, Unknown), + "ID_Einst_SuMk1TG_zeit_0_9": (327, Unknown), + "ID_Einst_SuMk1TG_zeit_1_8": (328, Unknown), + "ID_Einst_SuMk1TG_zeit_1_9": (329, Unknown), + "ID_Einst_SuMk1TG_zeit_2_8": (330, Unknown), + "ID_Einst_SuMk1TG_zeit_2_9": (331, Unknown), + "ID_Einst_SuMk1TG_zeit_0_10": (332, Unknown), + "ID_Einst_SuMk1TG_zeit_0_11": (333, Unknown), + "ID_Einst_SuMk1TG_zeit_1_10": (334, Unknown), + "ID_Einst_SuMk1TG_zeit_1_11": (335, Unknown), + "ID_Einst_SuMk1TG_zeit_2_10": (336, Unknown), + "ID_Einst_SuMk1TG_zeit_2_11": (337, Unknown), + "ID_Einst_SuMk1TG_zeit_0_12": (338, Unknown), + "ID_Einst_SuMk1TG_zeit_0_13": (339, Unknown), + "ID_Einst_SuMk1TG_zeit_1_12": (340, Unknown), + "ID_Einst_SuMk1TG_zeit_1_13": (341, Unknown), + "ID_Einst_SuMk1TG_zeit_2_12": (342, Unknown), + "ID_Einst_SuMk1TG_zeit_2_13": (343, Unknown), + "ID_Einst_SuMk2_akt2": (344, Unknown), + "ID_Einst_SuMk2Wo_zeit_0_0": (345, Unknown), + "ID_Einst_SuMk2Wo_zeit_0_1": (346, Unknown), + "ID_Einst_SuMk2Wo_zeit_1_0": (347, Unknown), + "ID_Einst_SuMk2Wo_zeit_1_1": (348, Unknown), + "ID_Einst_SuMk2Wo_zeit_2_0": (349, Unknown), + "ID_Einst_SuMk2Wo_zeit_2_1": (350, Unknown), + "ID_Einst_SuMk225_zeit_0_0": (351, Unknown), + "ID_Einst_SuMk225_zeit_0_1": (352, Unknown), + "ID_Einst_SuMk225_zeit_1_0": (353, Unknown), + "ID_Einst_SuMk225_zeit_1_1": (354, Unknown), + "ID_Einst_SuMk225_zeit_2_0": (355, Unknown), + "ID_Einst_SuMk225_zeit_2_1": (356, Unknown), + "ID_Einst_SuMk225_zeit_0_2": (357, Unknown), + "ID_Einst_SuMk225_zeit_0_3": (358, Unknown), + "ID_Einst_SuMk225_zeit_1_2": (359, Unknown), + "ID_Einst_SuMk225_zeit_1_3": (360, Unknown), + "ID_Einst_SuMk225_zeit_2_2": (361, Unknown), + "ID_Einst_SuMk225_zeit_2_3": (362, Unknown), + "ID_Einst_SuMk2Tg_zeit_0_0": (363, Unknown), + "ID_Einst_SuMk2Tg_zeit_0_1": (364, Unknown), + "ID_Einst_SuMk2Tg_zeit_1_0": (365, Unknown), + "ID_Einst_SuMk2Tg_zeit_1_1": (366, Unknown), + "ID_Einst_SuMk2Tg_zeit_2_0": (367, Unknown), + "ID_Einst_SuMk2Tg_zeit_2_1": (368, Unknown), + "ID_Einst_SuMk2Tg_zeit_0_2": (369, Unknown), + "ID_Einst_SuMk2Tg_zeit_0_3": (370, Unknown), + "ID_Einst_SuMk2Tg_zeit_1_2": (371, Unknown), + "ID_Einst_SuMk2Tg_zeit_1_3": (372, Unknown), + "ID_Einst_SuMk2Tg_zeit_2_2": (373, Unknown), + "ID_Einst_SuMk2Tg_zeit_2_3": (374, Unknown), + "ID_Einst_SuMk2Tg_zeit_0_4": (375, Unknown), + "ID_Einst_SuMk2Tg_zeit_0_5": (376, Unknown), + "ID_Einst_SuMk2Tg_zeit_1_4": (377, Unknown), + "ID_Einst_SuMk2Tg_zeit_1_5": (378, Unknown), + "ID_Einst_SuMk2Tg_zeit_2_4": (379, Unknown), + "ID_Einst_SuMk2Tg_zeit_2_5": (380, Unknown), + "ID_Einst_SuMk2Tg_zeit_0_6": (381, Unknown), + "ID_Einst_SuMk2Tg_zeit_0_7": (382, Unknown), + "ID_Einst_SuMk2Tg_zeit_1_6": (383, Unknown), + "ID_Einst_SuMk2Tg_zeit_1_7": (384, Unknown), + "ID_Einst_SuMk2Tg_zeit_2_6": (385, Unknown), + "ID_Einst_SuMk2Tg_zeit_2_7": (386, Unknown), + "ID_Einst_SuMk2Tg_zeit_0_8": (387, Unknown), + "ID_Einst_SuMk2Tg_zeit_0_9": (388, Unknown), + "ID_Einst_SuMk2Tg_zeit_1_8": (389, Unknown), + "ID_Einst_SuMk2Tg_zeit_1_9": (390, Unknown), + "ID_Einst_SuMk2Tg_zeit_2_8": (391, Unknown), + "ID_Einst_SuMk2Tg_zeit_2_9": (392, Unknown), + "ID_Einst_SuMk2Tg_zeit_0_10": (393, Unknown), + "ID_Einst_SuMk2Tg_zeit_0_11": (394, Unknown), + "ID_Einst_SuMk2Tg_zeit_1_10": (395, Unknown), + "ID_Einst_SuMk2Tg_zeit_1_11": (396, Unknown), + "ID_Einst_SuMk2Tg_zeit_2_10": (397, Unknown), + "ID_Einst_SuMk2Tg_zeit_2_11": (398, Unknown), + "ID_Einst_SuMk2Tg_zeit_0_12": (399, Unknown), + "ID_Einst_SuMk2Tg_zeit_0_13": (400, Unknown), + "ID_Einst_SuMk2Tg_zeit_1_12": (401, Unknown), + "ID_Einst_SuMk2Tg_zeit_1_13": (402, Unknown), + "ID_Einst_SuMk2Tg_zeit_2_12": (403, Unknown), + "ID_Einst_SuMk2Tg_zeit_2_13": (404, Unknown), + "ID_Einst_SUBW_akt2": (405, Unknown), + "ID_Einst_SuBwWO_zeit_0_0": (406, Unknown), + "ID_Einst_SuBwWO_zeit_0_1": (407, Unknown), + "ID_Einst_SuBwWO_zeit_1_0": (408, Unknown), + "ID_Einst_SuBwWO_zeit_1_1": (409, Unknown), + "ID_Einst_SuBwWO_zeit_2_0": (410, Unknown), + "ID_Einst_SuBwWO_zeit_2_1": (411, Unknown), + "ID_Einst_SuBwWO_zeit_3_0": (412, Unknown), + "ID_Einst_SuBwWO_zeit_3_1": (413, Unknown), + "ID_Einst_SuBwWO_zeit_4_0": (414, Unknown), + "ID_Einst_SuBwWO_zeit_4_1": (415, Unknown), + "ID_Einst_SuBw25_zeit_0_0": (416, Unknown), + "ID_Einst_SuBw25_zeit_0_1": (417, Unknown), + "ID_Einst_SuBw25_zeit_1_0": (418, Unknown), + "ID_Einst_SuBw25_zeit_1_1": (419, Unknown), + "ID_Einst_SuBw25_zeit_2_0": (420, Unknown), + "ID_Einst_SuBw25_zeit_2_1": (421, Unknown), + "ID_Einst_SuBw25_zeit_3_0": (422, Unknown), + "ID_Einst_SuBw25_zeit_3_1": (423, Unknown), + "ID_Einst_SuBw25_zeit_4_0": (424, Unknown), + "ID_Einst_SuBw25_zeit_4_1": (425, Unknown), + "ID_Einst_SuBw25_zeit_0_2": (426, Unknown), + "ID_Einst_SuBw25_zeit_0_3": (427, Unknown), + "ID_Einst_SuBw25_zeit_1_2": (428, Unknown), + "ID_Einst_SuBw25_zeit_1_3": (429, Unknown), + "ID_Einst_SuBw25_zeit_2_2": (430, Unknown), + "ID_Einst_SuBw25_zeit_2_3": (431, Unknown), + "ID_Einst_SuBw25_zeit_3_2": (432, Unknown), + "ID_Einst_SuBw25_zeit_3_3": (433, Unknown), + "ID_Einst_SuBw25_zeit_4_2": (434, Unknown), + "ID_Einst_SuBw25_zeit_4_3": (435, Unknown), + "ID_Einst_SuBwTG_zeit_0_0": (436, Unknown), + "ID_Einst_SuBwTG_zeit_0_1": (437, Unknown), + "ID_Einst_SuBwTG_zeit_1_0": (438, Unknown), + "ID_Einst_SuBwTG_zeit_1_1": (439, Unknown), + "ID_Einst_SuBwTG_zeit_2_0": (440, Unknown), + "ID_Einst_SuBwTG_zeit_2_1": (441, Unknown), + "ID_Einst_SuBwTG_zeit_3_0": (442, Unknown), + "ID_Einst_SuBwTG_zeit_3_1": (443, Unknown), + "ID_Einst_SuBwTG_zeit_4_0": (444, Unknown), + "ID_Einst_SuBwTG_zeit_4_1": (445, Unknown), + "ID_Einst_SuBwTG_zeit_0_2": (446, Unknown), + "ID_Einst_SuBwTG_zeit_0_3": (447, Unknown), + "ID_Einst_SuBwTG_zeit_1_2": (448, Unknown), + "ID_Einst_SuBwTG_zeit_1_3": (449, Unknown), + "ID_Einst_SuBwTG_zeit_2_2": (450, Unknown), + "ID_Einst_SuBwTG_zeit_2_3": (451, Unknown), + "ID_Einst_SuBwTG_zeit_3_2": (452, Unknown), + "ID_Einst_SuBwTG_zeit_3_3": (453, Unknown), + "ID_Einst_SuBwTG_zeit_4_2": (454, Unknown), + "ID_Einst_SuBwTG_zeit_4_3": (455, Unknown), + "ID_Einst_SuBwTG_zeit_0_4": (456, Unknown), + "ID_Einst_SuBwTG_zeit_0_5": (457, Unknown), + "ID_Einst_SuBwTG_zeit_1_4": (458, Unknown), + "ID_Einst_SuBwTG_zeit_1_5": (459, Unknown), + "ID_Einst_SuBwTG_zeit_2_4": (460, Unknown), + "ID_Einst_SuBwTG_zeit_2_5": (461, Unknown), + "ID_Einst_SuBwTG_zeit_3_4": (462, Unknown), + "ID_Einst_SuBwTG_zeit_3_5": (463, Unknown), + "ID_Einst_SuBwTG_zeit_4_4": (464, Unknown), + "ID_Einst_SuBwTG_zeit_4_5": (465, Unknown), + "ID_Einst_SuBwTG_zeit_0_6": (466, Unknown), + "ID_Einst_SuBwTG_zeit_0_7": (467, Unknown), + "ID_Einst_SuBwTG_zeit_1_6": (468, Unknown), + "ID_Einst_SuBwTG_zeit_1_7": (469, Unknown), + "ID_Einst_SuBwTG_zeit_2_6": (470, Unknown), + "ID_Einst_SuBwTG_zeit_2_7": (471, Unknown), + "ID_Einst_SuBwTG_zeit_3_6": (472, Unknown), + "ID_Einst_SuBwTG_zeit_3_7": (473, Unknown), + "ID_Einst_SuBwTG_zeit_4_6": (474, Unknown), + "ID_Einst_SuBwTG_zeit_4_7": (475, Unknown), + "ID_Einst_SuBwTG_zeit_0_8": (476, Unknown), + "ID_Einst_SuBwTG_zeit_0_9": (477, Unknown), + "ID_Einst_SuBwTG_zeit_1_8": (478, Unknown), + "ID_Einst_SuBwTG_zeit_1_9": (479, Unknown), + "ID_Einst_SuBwTG_zeit_2_8": (480, Unknown), + "ID_Einst_SuBwTG_zeit_2_9": (481, Unknown), + "ID_Einst_SuBwTG_zeit_3_8": (482, Unknown), + "ID_Einst_SuBwTG_zeit_3_9": (483, Unknown), + "ID_Einst_SuBwTG_zeit_4_8": (484, Unknown), + "ID_Einst_SuBwTG_zeit_4_9": (485, Unknown), + "ID_Einst_SuBwTG_zeit_0_10": (486, Unknown), + "ID_Einst_SuBwTG_zeit_0_11": (487, Unknown), + "ID_Einst_SuBwTG_zeit_1_10": (488, Unknown), + "ID_Einst_SuBwTG_zeit_1_11": (489, Unknown), + "ID_Einst_SuBwTG_zeit_2_10": (490, Unknown), + "ID_Einst_SuBwTG_zeit_2_11": (491, Unknown), + "ID_Einst_SuBwTG_zeit_3_10": (492, Unknown), + "ID_Einst_SuBwTG_zeit_3_11": (493, Unknown), + "ID_Einst_SuBwTG_zeit_4_10": (494, Unknown), + "ID_Einst_SuBwTG_zeit_4_11": (495, Unknown), + "ID_Einst_SuBwTG_zeit_0_12": (496, Unknown), + "ID_Einst_SuBwTG_zeit_0_13": (497, Unknown), + "ID_Einst_SuBwTG_zeit_1_12": (498, Unknown), + "ID_Einst_SuBwTG_zeit_1_13": (499, Unknown), + "ID_Einst_SuBwTG_zeit_2_12": (500, Unknown), + "ID_Einst_SuBwTG_zeit_2_13": (501, Unknown), + "ID_Einst_SuBwTG_zeit_3_12": (502, Unknown), + "ID_Einst_SuBwTG_zeit_3_13": (503, Unknown), + "ID_Einst_SuBwTG_zeit_4_12": (504, Unknown), + "ID_Einst_SuBwTG_zeit_4_13": (505, Unknown), + "ID_Einst_SuZIP_akt": (506, Unknown), + "ID_Einst_SuZIPWo_zeit_0_0": (507, Unknown), + "ID_Einst_SuZIPWo_zeit_0_1": (508, Unknown), + "ID_Einst_SuZIPWo_zeit_1_0": (509, Unknown), + "ID_Einst_SuZIPWo_zeit_1_1": (510, Unknown), + "ID_Einst_SuZIPWo_zeit_2_0": (511, Unknown), + "ID_Einst_SuZIPWo_zeit_2_1": (512, Unknown), + "ID_Einst_SuZIPWo_zeit_3_0": (513, Unknown), + "ID_Einst_SuZIPWo_zeit_3_1": (514, Unknown), + "ID_Einst_SuZIPWo_zeit_4_0": (515, Unknown), + "ID_Einst_SuZIPWo_zeit_4_1": (516, Unknown), + "ID_Einst_SuZIP25_zeit_0_0": (517, Unknown), + "ID_Einst_SuZIP25_zeit_0_1": (518, Unknown), + "ID_Einst_SuZIP25_zeit_1_0": (519, Unknown), + "ID_Einst_SuZIP25_zeit_1_1": (520, Unknown), + "ID_Einst_SuZIP25_zeit_2_0": (521, Unknown), + "ID_Einst_SuZIP25_zeit_2_1": (522, Unknown), + "ID_Einst_SuZIP25_zeit_3_0": (523, Unknown), + "ID_Einst_SuZIP25_zeit_3_1": (524, Unknown), + "ID_Einst_SuZIP25_zeit_4_0": (525, Unknown), + "ID_Einst_SuZIP25_zeit_4_1": (526, Unknown), + "ID_Einst_SuZIP25_zeit_0_2": (527, Unknown), + "ID_Einst_SuZIP25_zeit_0_3": (528, Unknown), + "ID_Einst_SuZIP25_zeit_1_2": (529, Unknown), + "ID_Einst_SuZIP25_zeit_1_3": (530, Unknown), + "ID_Einst_SuZIP25_zeit_2_2": (531, Unknown), + "ID_Einst_SuZIP25_zeit_2_3": (532, Unknown), + "ID_Einst_SuZIP25_zeit_3_2": (533, Unknown), + "ID_Einst_SuZIP25_zeit_3_3": (534, Unknown), + "ID_Einst_SuZIP25_zeit_4_2": (535, Unknown), + "ID_Einst_SuZIP25_zeit_4_3": (536, Unknown), + "ID_Einst_SuZIPTg_zeit_0_0": (537, Unknown), + "ID_Einst_SuZIPTg_zeit_0_1": (538, Unknown), + "ID_Einst_SuZIPTg_zeit_1_0": (539, Unknown), + "ID_Einst_SuZIPTg_zeit_1_1": (540, Unknown), + "ID_Einst_SuZIPTg_zeit_2_0": (541, Unknown), + "ID_Einst_SuZIPTg_zeit_2_1": (542, Unknown), + "ID_Einst_SuZIPTg_zeit_3_0": (543, Unknown), + "ID_Einst_SuZIPTg_zeit_3_1": (544, Unknown), + "ID_Einst_SuZIPTg_zeit_4_0": (545, Unknown), + "ID_Einst_SuZIPTg_zeit_4_1": (546, Unknown), + "ID_Einst_SuZIPTg_zeit_0_2": (547, Unknown), + "ID_Einst_SuZIPTg_zeit_0_3": (548, Unknown), + "ID_Einst_SuZIPTg_zeit_1_2": (549, Unknown), + "ID_Einst_SuZIPTg_zeit_1_3": (550, Unknown), + "ID_Einst_SuZIPTg_zeit_2_2": (551, Unknown), + "ID_Einst_SuZIPTg_zeit_2_3": (552, Unknown), + "ID_Einst_SuZIPTg_zeit_3_2": (553, Unknown), + "ID_Einst_SuZIPTg_zeit_3_3": (554, Unknown), + "ID_Einst_SuZIPTg_zeit_4_2": (555, Unknown), + "ID_Einst_SuZIPTg_zeit_4_3": (556, Unknown), + "ID_Einst_SuZIPTg_zeit_0_4": (557, Unknown), + "ID_Einst_SuZIPTg_zeit_0_5": (558, Unknown), + "ID_Einst_SuZIPTg_zeit_1_4": (559, Unknown), + "ID_Einst_SuZIPTg_zeit_1_5": (560, Unknown), + "ID_Einst_SuZIPTg_zeit_2_4": (561, Unknown), + "ID_Einst_SuZIPTg_zeit_2_5": (562, Unknown), + "ID_Einst_SuZIPTg_zeit_3_4": (563, Unknown), + "ID_Einst_SuZIPTg_zeit_3_5": (564, Unknown), + "ID_Einst_SuZIPTg_zeit_4_4": (565, Unknown), + "ID_Einst_SuZIPTg_zeit_4_5": (566, Unknown), + "ID_Einst_SuZIPTg_zeit_0_6": (567, Unknown), + "ID_Einst_SuZIPTg_zeit_0_7": (568, Unknown), + "ID_Einst_SuZIPTg_zeit_1_6": (569, Unknown), + "ID_Einst_SuZIPTg_zeit_1_7": (570, Unknown), + "ID_Einst_SuZIPTg_zeit_2_6": (571, Unknown), + "ID_Einst_SuZIPTg_zeit_2_7": (572, Unknown), + "ID_Einst_SuZIPTg_zeit_3_6": (573, Unknown), + "ID_Einst_SuZIPTg_zeit_3_7": (574, Unknown), + "ID_Einst_SuZIPTg_zeit_4_6": (575, Unknown), + "ID_Einst_SuZIPTg_zeit_4_7": (576, Unknown), + "ID_Einst_SuZIPTg_zeit_0_8": (577, Unknown), + "ID_Einst_SuZIPTg_zeit_0_9": (578, Unknown), + "ID_Einst_SuZIPTg_zeit_1_8": (579, Unknown), + "ID_Einst_SuZIPTg_zeit_1_9": (580, Unknown), + "ID_Einst_SuZIPTg_zeit_2_8": (581, Unknown), + "ID_Einst_SuZIPTg_zeit_2_9": (582, Unknown), + "ID_Einst_SuZIPTg_zeit_3_8": (583, Unknown), + "ID_Einst_SuZIPTg_zeit_3_9": (584, Unknown), + "ID_Einst_SuZIPTg_zeit_4_8": (585, Unknown), + "ID_Einst_SuZIPTg_zeit_4_9": (586, Unknown), + "ID_Einst_SuZIPTg_zeit_0_10": (587, Unknown), + "ID_Einst_SuZIPTg_zeit_0_11": (588, Unknown), + "ID_Einst_SuZIPTg_zeit_1_10": (589, Unknown), + "ID_Einst_SuZIPTg_zeit_1_11": (590, Unknown), + "ID_Einst_SuZIPTg_zeit_2_10": (591, Unknown), + "ID_Einst_SuZIPTg_zeit_2_11": (592, Unknown), + "ID_Einst_SuZIPTg_zeit_3_10": (593, Unknown), + "ID_Einst_SuZIPTg_zeit_3_11": (594, Unknown), + "ID_Einst_SuZIPTg_zeit_4_10": (595, Unknown), + "ID_Einst_SuZIPTg_zeit_4_11": (596, Unknown), + "ID_Einst_SuZIPTg_zeit_0_12": (597, Unknown), + "ID_Einst_SuZIPTg_zeit_0_13": (598, Unknown), + "ID_Einst_SuZIPTg_zeit_1_12": (599, Unknown), + "ID_Einst_SuZIPTg_zeit_1_13": (600, Unknown), + "ID_Einst_SuZIPTg_zeit_2_12": (601, Unknown), + "ID_Einst_SuZIPTg_zeit_2_13": (602, Unknown), + "ID_Einst_SuZIPTg_zeit_3_12": (603, Unknown), + "ID_Einst_SuZIPTg_zeit_3_13": (604, Unknown), + "ID_Einst_SuZIPTg_zeit_4_12": (605, Unknown), + "ID_Einst_SuZIPTg_zeit_4_13": (606, Unknown), + "ID_Einst_SuSwb_akt": (607, Unknown), + "ID_Einst_SuSwbWo_zeit_0_0": (608, Unknown), + "ID_Einst_SuSwbWo_zeit_0_1": (609, Unknown), + "ID_Einst_SuSwbWo_zeit_1_0": (610, Unknown), + "ID_Einst_SuSwbWo_zeit_1_1": (611, Unknown), + "ID_Einst_SuSwbWo_zeit_2_0": (612, Unknown), + "ID_Einst_SuSwbWo_zeit_2_1": (613, Unknown), + "ID_Einst_SuSwb25_zeit_0_0": (614, Unknown), + "ID_Einst_SuSwb25_zeit_0_1": (615, Unknown), + "ID_Einst_SuSwb25_zeit_1_0": (616, Unknown), + "ID_Einst_SuSwb25_zeit_1_1": (617, Unknown), + "ID_Einst_SuSwb25_zeit_2_0": (618, Unknown), + "ID_Einst_SuSwb25_zeit_2_1": (619, Unknown), + "ID_Einst_SuSwb25_zeit_0_2": (620, Unknown), + "ID_Einst_SuSwb25_zeit_0_3": (621, Unknown), + "ID_Einst_SuSwb25_zeit_1_2": (622, Unknown), + "ID_Einst_SuSwb25_zeit_1_3": (623, Unknown), + "ID_Einst_SuSwb25_zeit_2_2": (624, Unknown), + "ID_Einst_SuSwb25_zeit_2_3": (625, Unknown), + "ID_Einst_SuSwbTg_zeit_0_0": (626, Unknown), + "ID_Einst_SuSwbTg_zeit_0_1": (627, Unknown), + "ID_Einst_SuSwbTg_zeit_1_0": (628, Unknown), + "ID_Einst_SuSwbTg_zeit_1_1": (629, Unknown), + "ID_Einst_SuSwbTg_zeit_2_0": (630, Unknown), + "ID_Einst_SuSwbTg_zeit_2_1": (631, Unknown), + "ID_Einst_SuSwbTg_zeit_0_2": (632, Unknown), + "ID_Einst_SuSwbTg_zeit_0_3": (633, Unknown), + "ID_Einst_SuSwbTg_zeit_1_2": (634, Unknown), + "ID_Einst_SuSwbTg_zeit_1_3": (635, Unknown), + "ID_Einst_SuSwbTg_zeit_2_2": (636, Unknown), + "ID_Einst_SuSwbTg_zeit_2_3": (637, Unknown), + "ID_Einst_SuSwbTg_zeit_0_4": (638, Unknown), + "ID_Einst_SuSwbTg_zeit_0_5": (639, Unknown), + "ID_Einst_SuSwbTg_zeit_1_4": (640, Unknown), + "ID_Einst_SuSwbTg_zeit_1_5": (641, Unknown), + "ID_Einst_SuSwbTg_zeit_2_4": (642, Unknown), + "ID_Einst_SuSwbTg_zeit_2_5": (643, Unknown), + "ID_Einst_SuSwbTg_zeit_0_6": (644, Unknown), + "ID_Einst_SuSwbTg_zeit_0_7": (645, Unknown), + "ID_Einst_SuSwbTg_zeit_1_6": (646, Unknown), + "ID_Einst_SuSwbTg_zeit_1_7": (647, Unknown), + "ID_Einst_SuSwbTg_zeit_2_6": (648, Unknown), + "ID_Einst_SuSwbTg_zeit_2_7": (649, Unknown), + "ID_Einst_SuSwbTg_zeit_0_8": (650, Unknown), + "ID_Einst_SuSwbTg_zeit_0_9": (651, Unknown), + "ID_Einst_SuSwbTg_zeit_1_8": (652, Unknown), + "ID_Einst_SuSwbTg_zeit_1_9": (653, Unknown), + "ID_Einst_SuSwbTg_zeit_2_8": (654, Unknown), + "ID_Einst_SuSwbTg_zeit_2_9": (655, Unknown), + "ID_Einst_SuSwbTg_zeit_0_10": (656, Unknown), + "ID_Einst_SuSwbTg_zeit_0_11": (657, Unknown), + "ID_Einst_SuSwbTg_zeit_1_10": (658, Unknown), + "ID_Einst_SuSwbTg_zeit_1_11": (659, Unknown), + "ID_Einst_SuSwbTg_zeit_2_10": (660, Unknown), + "ID_Einst_SuSwbTg_zeit_2_11": (661, Unknown), + "ID_Einst_SuSwbTg_zeit_0_12": (662, Unknown), + "ID_Einst_SuSwbTg_zeit_0_13": (663, Unknown), + "ID_Einst_SuSwbTg_zeit_1_12": (664, Unknown), + "ID_Einst_SuSwbTg_zeit_1_13": (665, Unknown), + "ID_Einst_SuSwbTg_zeit_2_12": (666, Unknown), + "ID_Einst_SuSwbTg_zeit_2_13": (667, Unknown), + "ID_Zaehler_BetrZeitWP": (668, Unknown), + "ID_Zaehler_BetrZeitVD1": (669, Unknown), + "ID_Zaehler_BetrZeitVD2": (670, Unknown), + "ID_Zaehler_BetrZeitZWE1": (671, Unknown), + "ID_Zaehler_BetrZeitZWE2": (672, Unknown), + "ID_Zaehler_BetrZeitZWE3": (673, Unknown), + "ID_Zaehler_BetrZeitImpVD1": (674, Unknown), + "ID_Zaehler_BetrZeitImpVD2": (675, Unknown), + "ID_Zaehler_BetrZeitEZMVD1": (676, Unknown), + "ID_Zaehler_BetrZeitEZMVD2": (677, Unknown), + "ID_Einst_Entl_Typ_0": (678, Unknown), + "ID_Einst_Entl_Typ_1": (679, Unknown), + "ID_Einst_Entl_Typ_2": (680, Unknown), + "ID_Einst_Entl_Typ_3": (681, Unknown), + "ID_Einst_Entl_Typ_4": (682, Unknown), + "ID_Einst_Entl_Typ_5": (683, Unknown), + "ID_Einst_Entl_Typ_6": (684, Unknown), + "ID_Einst_Entl_Typ_7": (685, Unknown), + "ID_Einst_Entl_Typ_8": (686, Unknown), + "ID_Einst_Entl_Typ_9": (687, Unknown), + "ID_Einst_Entl_Typ_10": (688, Unknown), + "ID_Einst_Entl_Typ_11": (689, Unknown), + "ID_Einst_Entl_Typ_12": (690, Unknown), + "ID_Einst_Vorl_max_MK1": (691, Unknown), + "ID_Einst_Vorl_max_MK2": (692, Unknown), + "ID_SU_FrkdMK1": (693, Unknown), + "ID_SU_FrkdMK2": (694, Unknown), + "ID_Ba_Hz_MK1_akt": (695, Unknown), + "ID_Ba_Hz_MK2_akt": (696, Unknown), + "ID_Einst_Zirk_Ein_akt": (697, Unknown), + "ID_Einst_Zirk_Aus_akt": (698, Unknown), + "ID_Einst_Heizgrenze": (699, Unknown), + "ID_Einst_Heizgrenze_Temp": (700, Celsius), + "ID_VariablenIBNgespeichert": (701, Unknown), + "ID_SchonIBNAssistant": (702, Unknown), + "ID_Heizgrenze_0": (703, Unknown), + "ID_Heizgrenze_1": (704, Unknown), + "ID_Heizgrenze_2": (705, Unknown), + "ID_Heizgrenze_3": (706, Unknown), + "ID_Heizgrenze_4": (707, Unknown), + "ID_Heizgrenze_5": (708, Unknown), + "ID_Heizgrenze_6": (709, Unknown), + "ID_Heizgrenze_7": (710, Unknown), + "ID_Heizgrenze_8": (711, Unknown), + "ID_Heizgrenze_9": (712, Unknown), + "ID_Heizgrenze_10": (713, Unknown), + "ID_Heizgrenze_11": (714, Unknown), + "ID_SchemenIBNgewahlt": (715, Unknown), + "ID_Switchoff_file_0_0": (716, Unknown), + "ID_Switchoff_file_1_0": (717, Unknown), + "ID_Switchoff_file_2_0": (718, Unknown), + "ID_Switchoff_file_3_0": (719, Unknown), + "ID_Switchoff_file_4_0": (720, Unknown), + "ID_Switchoff_file_0_1": (721, Unknown), + "ID_Switchoff_file_1_1": (722, Unknown), + "ID_Switchoff_file_2_1": (723, Unknown), + "ID_Switchoff_file_3_1": (724, Unknown), + "ID_Switchoff_file_4_1": (725, Unknown), + "ID_DauerDatenLoggerAktiv": (726, Unknown), + "ID_Laufvar_Heizgrenze": (727, Unknown), + "ID_Zaehler_BetrZeitHz": (728, Unknown), + "ID_Zaehler_BetrZeitBW": (729, Unknown), + "ID_Zaehler_BetrZeitKue": (730, Unknown), + "ID_SU_FstdHz": (731, Unknown), + "ID_SU_FstdBw": (732, Unknown), + "ID_SU_FstdSwb": (733, Unknown), + "ID_SU_FstdMK1": (734, Unknown), + "ID_SU_FstdMK2": (735, Unknown), + "ID_FerienAbsenkungHz": (736, Unknown), + "ID_FerienAbsenkungMK1": (737, Unknown), + "ID_FerienAbsenkungMK2": (738, Unknown), + "ID_FerienModusAktivHz": (739, Unknown), + "ID_FerienModusAktivBw": (740, Unknown), + "ID_FerienModusAktivSwb": (741, Unknown), + "ID_FerienModusAktivMk1": (742, Unknown), + "ID_FerienModusAktivMk2": (743, Unknown), + "ID_DisplayContrast_akt": (744, Unknown), + "ID_Ba_Hz_saved": (745, Unknown), + "ID_Ba_Bw_saved": (746, Unknown), + "ID_Ba_Sw_saved": (747, Unknown), + "ID_Ba_Hz_MK1_saved": (748, Unknown), + "ID_Ba_Hz_MK2_saved": (749, Unknown), + "ID_AdresseIP_akt": (750, Unknown), + "ID_SubNetMask_akt": (751, Unknown), + "ID_Add_Broadcast_akt": (752, Unknown), + "ID_Add_StdGateway_akt": (753, Unknown), + "ID_DHCPServerAktiv_akt": (754, Unknown), + "ID_WebserverPasswort_1_akt": (755, Unknown), + "ID_WebserverPasswort_2_akt": (756, Unknown), + "ID_WebserverPasswort_3_akt": (757, Unknown), + "ID_WebserverPasswort_4_akt": (758, Unknown), + "ID_WebserverPasswort_5_akt": (759, Unknown), + "ID_WebserverPasswort_6_akt": (760, Unknown), + "ID_WebServerWerteBekommen": (761, Unknown), + "ID_Einst_ParBetr_akt": (762, Unknown), + "ID_Einst_WpAnz_akt": (763, Unknown), + "ID_Einst_PhrTime_akt": (764, Unknown), + "ID_Einst_HysPar_akt": (765, Unknown), + "ID_IP_PB_Slave_0": (766, Unknown), + "ID_IP_PB_Slave_1": (767, Unknown), + "ID_IP_PB_Slave_2": (768, Unknown), + "ID_IP_PB_Slave_3": (769, Unknown), + "ID_IP_PB_Slave_4": (770, Unknown), + "ID_IP_PB_Slave_5": (771, Unknown), + "ID_Einst_BwHup_akt_backup": (772, Unknown), + "ID_Einst_SuMk3_akt": (773, Unknown), + "ID_Einst_HzMK3E_akt": (774, Unknown), + "ID_Einst_HzMK3ANH_akt": (775, Unknown), + "ID_Einst_HzMK3ABS_akt": (776, Unknown), + "ID_Einst_HzMK3Hgr_akt": (777, Unknown), + "ID_Einst_HzFtMK3Vl_akt": (778, Unknown), + "ID_Ba_Hz_MK3_akt": (779, MixedCircuitMode), + "ID_Einst_MK3Typ_akt": (780, Unknown), + "ID_Einst_RTypMK3_akt": (781, Unknown), + "ID_Einst_MK3LzFaktor_akt": (782, Unknown), + "ID_Einst_MK3PerFaktor_akt": (783, Unknown), + "ID_FerienModusAktivMk3": (784, Unknown), + "ID_SU_FrkdMK3": (785, Unknown), + "ID_FerienAbsenkungMK3": (786, Unknown), + "ID_SU_FstdMK3": (787, Unknown), + "ID_Einst_SuMk3_akt2": (788, Unknown), + "ID_Einst_SuMk3Wo_zeit_0_0": (789, Unknown), + "ID_Einst_SuMk3Wo_zeit_0_1": (790, Unknown), + "ID_Einst_SuMk3Wo_zeit_1_0": (791, Unknown), + "ID_Einst_SuMk3Wo_zeit_1_1": (792, Unknown), + "ID_Einst_SuMk3Wo_zeit_2_0": (793, Unknown), + "ID_Einst_SuMk3Wo_zeit_2_1": (794, Unknown), + "ID_Einst_SuMk325_zeit_0_0": (795, Unknown), + "ID_Einst_SuMk325_zeit_0_1": (796, Unknown), + "ID_Einst_SuMk325_zeit_1_0": (797, Unknown), + "ID_Einst_SuMk325_zeit_1_1": (798, Unknown), + "ID_Einst_SuMk325_zeit_2_0": (799, Unknown), + "ID_Einst_SuMk325_zeit_2_1": (800, Unknown), + "ID_Einst_SuMk325_zeit_0_2": (801, Unknown), + "ID_Einst_SuMk325_zeit_0_3": (802, Unknown), + "ID_Einst_SuMk325_zeit_1_2": (803, Unknown), + "ID_Einst_SuMk325_zeit_1_3": (804, Unknown), + "ID_Einst_SuMk325_zeit_2_2": (805, Unknown), + "ID_Einst_SuMk325_zeit_2_3": (806, Unknown), + "ID_Einst_SuMk3Tg_zeit_0_0": (807, Unknown), + "ID_Einst_SuMk3Tg_zeit_0_1": (808, Unknown), + "ID_Einst_SuMk3Tg_zeit_1_0": (809, Unknown), + "ID_Einst_SuMk3Tg_zeit_1_1": (810, Unknown), + "ID_Einst_SuMk3Tg_zeit_2_0": (811, Unknown), + "ID_Einst_SuMk3Tg_zeit_2_1": (812, Unknown), + "ID_Einst_SuMk3Tg_zeit_0_2": (813, Unknown), + "ID_Einst_SuMk3Tg_zeit_0_3": (814, Unknown), + "ID_Einst_SuMk3Tg_zeit_1_2": (815, Unknown), + "ID_Einst_SuMk3Tg_zeit_1_3": (816, Unknown), + "ID_Einst_SuMk3Tg_zeit_2_2": (817, Unknown), + "ID_Einst_SuMk3Tg_zeit_2_3": (818, Unknown), + "ID_Einst_SuMk3Tg_zeit_0_4": (819, Unknown), + "ID_Einst_SuMk3Tg_zeit_0_5": (820, Unknown), + "ID_Einst_SuMk3Tg_zeit_1_4": (821, Unknown), + "ID_Einst_SuMk3Tg_zeit_1_5": (822, Unknown), + "ID_Einst_SuMk3Tg_zeit_2_4": (823, Unknown), + "ID_Einst_SuMk3Tg_zeit_2_5": (824, Unknown), + "ID_Einst_SuMk3Tg_zeit_0_6": (825, Unknown), + "ID_Einst_SuMk3Tg_zeit_0_7": (826, Unknown), + "ID_Einst_SuMk3Tg_zeit_1_6": (827, Unknown), + "ID_Einst_SuMk3Tg_zeit_1_7": (828, Unknown), + "ID_Einst_SuMk3Tg_zeit_2_6": (829, Unknown), + "ID_Einst_SuMk3Tg_zeit_2_7": (830, Unknown), + "ID_Einst_SuMk3Tg_zeit_0_8": (831, Unknown), + "ID_Einst_SuMk3Tg_zeit_0_9": (832, Unknown), + "ID_Einst_SuMk3Tg_zeit_1_8": (833, Unknown), + "ID_Einst_SuMk3Tg_zeit_1_9": (834, Unknown), + "ID_Einst_SuMk3Tg_zeit_2_8": (835, Unknown), + "ID_Einst_SuMk3Tg_zeit_2_9": (836, Unknown), + "ID_Einst_SuMk3Tg_zeit_0_10": (837, Unknown), + "ID_Einst_SuMk3Tg_zeit_0_11": (838, Unknown), + "ID_Einst_SuMk3Tg_zeit_1_10": (839, Unknown), + "ID_Einst_SuMk3Tg_zeit_1_11": (840, Unknown), + "ID_Einst_SuMk3Tg_zeit_2_10": (841, Unknown), + "ID_Einst_SuMk3Tg_zeit_2_11": (842, Unknown), + "ID_Einst_SuMk3Tg_zeit_0_12": (843, Unknown), + "ID_Einst_SuMk3Tg_zeit_0_13": (844, Unknown), + "ID_Einst_SuMk3Tg_zeit_1_12": (845, Unknown), + "ID_Einst_SuMk3Tg_zeit_1_13": (846, Unknown), + "ID_Einst_SuMk3Tg_zeit_2_12": (847, Unknown), + "ID_Einst_SuMk3Tg_zeit_2_13": (848, Unknown), + "ID_Ba_Hz_MK3_saved": (849, Unknown), + "ID_Einst_Kuhl_Zeit_Ein_akt": (850, Hours), + "ID_Einst_Kuhl_Zeit_Aus_akt": (851, Hours), + "ID_Waermemenge_Seit": (852, Unknown), + "ID_Waermemenge_WQ": (853, Unknown), + "ID_Waermemenge_Hz": (854, Unknown), + "ID_Waermemenge_WQ_ges": (855, Unknown), + "ID_Einst_Entl_Typ_13": (856, Unknown), + "ID_Einst_Entl_Typ_14": (857, Unknown), + "ID_Einst_Entl_Typ_15": (858, Unknown), + "ID_Zaehler_BetrZeitSW": (859, Unknown), + "ID_Einst_Fernwartung_akt": (860, Unknown), + "ID_AdresseIPServ_akt": (861, Unknown), + "ID_Einst_TA_EG_akt": (862, Unknown), + "ID_Einst_TVLmax_EG_akt": (863, Unknown), + "ID_Einst_Popt_Nachlauf_akt": (864, Unknown), + "ID_FernwartungVertrag_akt": (865, Unknown), + "ID_FernwartungAktuZeit": (866, Unknown), + "ID_Einst_Effizienzpumpe_Nominal_akt": (867, Unknown), + "ID_Einst_Effizienzpumpe_Minimal_akt": (868, Unknown), + "ID_Einst_Effizienzpumpe_akt": (869, Unknown), + "ID_Einst_Waermemenge_akt": (870, Unknown), + "ID_Einst_Wm_Versorgung_Korrektur_akt": (871, Unknown), + "ID_Einst_Wm_Auswertung_Korrektur_akt": (872, Unknown), + "ID_SoftwareUpdateJetztGemacht_akt": (873, Unknown), + "ID_WP_SerienNummer_DATUM": (874, Unknown), + "ID_WP_SerienNummer_HEX": (875, Unknown), + "ID_WP_SerienNummer_INDEX": (876, Unknown), + "ID_ProgWerteWebSrvBeobarten": (877, Unknown), + "ID_Waermemenge_BW": (878, Unknown), + "ID_Waermemenge_SW": (879, Unknown), + "ID_Waermemenge_Datum": (880, Unknown), + "ID_Einst_Solar_akt": (881, SolarMode), + "ID_BSTD_Solar": (882, Unknown), + "ID_Einst_TDC_Koll_Max_akt": (883, Celsius), + "ID_Einst_Akt_Kuehlung_akt": (884, Unknown), + "ID_Einst_Vorlauf_VBO_akt": (885, Unknown), + "ID_Einst_KRHyst_akt": (886, Unknown), + "ID_Einst_Akt_Kuehl_Speicher_min_akt": (887, Unknown), + "ID_Einst_Akt_Kuehl_Freig_WQE_akt": (888, Unknown), + "ID_NDAB_WW_Anzahl": (889, Unknown), + "ID_NDS_WW_KD_Quitt": (890, Unknown), + "ID_Einst_AbtZykMin_akt": (891, Unknown), + "ID_Einst_VD2_Zeit_Min_akt": (892, Unknown), + "ID_Einst_Hysterese_HR_verkuerzt_akt": (893, Unknown), + "ID_Einst_BA_Lueftung_akt": (894, VentilationMode), + "ID_Einst_SuLuf_akt": (895, Unknown), + "ID_Einst_SuLufWo_zeit_0_0_0": (896, Unknown), + "ID_Einst_SuLufWo_zeit_0_1_0": (897, Unknown), + "ID_Einst_SuLufWo_zeit_0_2_0": (898, Unknown), + "ID_Einst_SuLuf25_zeit_0_0_0": (899, Unknown), + "ID_Einst_SuLuf25_zeit_0_1_0": (900, Unknown), + "ID_Einst_SuLuf25_zeit_0_2_0": (901, Unknown), + "ID_Einst_SuLuf25_zeit_0_0_2": (902, Unknown), + "ID_Einst_SuLuf25_zeit_0_1_2": (903, Unknown), + "ID_Einst_SuLuf25_zeit_0_2_2": (904, Unknown), + "ID_Einst_SuLufTg_zeit_0_0_0": (905, Unknown), + "ID_Einst_SuLufTg_zeit_0_1_0": (906, Unknown), + "ID_Einst_SuLufTg_zeit_0_2_0": (907, Unknown), + "ID_Einst_SuLufTg_zeit_0_0_2": (908, Unknown), + "ID_Einst_SuLufTg_zeit_0_1_2": (909, Unknown), + "ID_Einst_SuLufTg_zeit_0_2_2": (910, Unknown), + "ID_Einst_SuLufTg_zeit_0_0_4": (911, Unknown), + "ID_Einst_SuLufTg_zeit_0_1_4": (912, Unknown), + "ID_Einst_SuLufTg_zeit_0_2_4": (913, Unknown), + "ID_Einst_SuLufTg_zeit_0_0_6": (914, Unknown), + "ID_Einst_SuLufTg_zeit_0_1_6": (915, Unknown), + "ID_Einst_SuLufTg_zeit_0_2_6": (916, Unknown), + "ID_Einst_SuLufTg_zeit_0_0_8": (917, Unknown), + "ID_Einst_SuLufTg_zeit_0_1_8": (918, Unknown), + "ID_Einst_SuLufTg_zeit_0_2_8": (919, Unknown), + "ID_Einst_SuLufTg_zeit_0_0_10": (920, Unknown), + "ID_Einst_SuLufTg_zeit_0_1_10": (921, Unknown), + "ID_Einst_SuLufTg_zeit_0_2_10": (922, Unknown), + "ID_Einst_SuLufTg_zeit_0_0_12": (923, Unknown), + "ID_Einst_SuLufTg_zeit_0_1_12": (924, Unknown), + "ID_Einst_SuLufTg_zeit_0_2_12": (925, Unknown), + "ID_Einst_SuLufWo_zeit_1_0_0": (926, Unknown), + "ID_Einst_SuLufWo_zeit_1_1_0": (927, Unknown), + "ID_Einst_SuLufWo_zeit_1_2_0": (928, Unknown), + "ID_Einst_SuLuf25_zeit_1_0_0": (929, Unknown), + "ID_Einst_SuLuf25_zeit_1_1_0": (930, Unknown), + "ID_Einst_SuLuf25_zeit_1_2_0": (931, Unknown), + "ID_Einst_SuLuf25_zeit_1_0_2": (932, Unknown), + "ID_Einst_SuLuf25_zeit_1_1_2": (933, Unknown), + "ID_Einst_SuLuf25_zeit_1_2_2": (934, Unknown), + "ID_Einst_SuLufTg_zeit_1_0_0": (935, Unknown), + "ID_Einst_SuLufTg_zeit_1_1_0": (936, Unknown), + "ID_Einst_SuLufTg_zeit_1_2_0": (937, Unknown), + "ID_Einst_SuLufTg_zeit_1_0_2": (938, Unknown), + "ID_Einst_SuLufTg_zeit_1_1_2": (939, Unknown), + "ID_Einst_SuLufTg_zeit_1_2_2": (940, Unknown), + "ID_Einst_SuLufTg_zeit_1_0_4": (941, Unknown), + "ID_Einst_SuLufTg_zeit_1_1_4": (942, Unknown), + "ID_Einst_SuLufTg_zeit_1_2_4": (943, Unknown), + "ID_Einst_SuLufTg_zeit_1_0_6": (944, Unknown), + "ID_Einst_SuLufTg_zeit_1_1_6": (945, Unknown), + "ID_Einst_SuLufTg_zeit_1_2_6": (946, Unknown), + "ID_Einst_SuLufTg_zeit_1_0_8": (947, Unknown), + "ID_Einst_SuLufTg_zeit_1_1_8": (948, Unknown), + "ID_Einst_SuLufTg_zeit_1_2_8": (949, Unknown), + "ID_Einst_SuLufTg_zeit_1_0_10": (950, Unknown), + "ID_Einst_SuLufTg_zeit_1_1_10": (951, Unknown), + "ID_Einst_SuLufTg_zeit_1_2_10": (952, Unknown), + "ID_Einst_SuLufTg_zeit_1_0_12": (953, Unknown), + "ID_Einst_SuLufTg_zeit_1_1_12": (954, Unknown), + "ID_Einst_SuLufTg_zeit_1_2_12": (955, Unknown), + "ID_FerienModusAktivLueftung": (956, Unknown), + "ID_Einst_BA_Lueftung_saved": (957, Unknown), + "ID_SU_FrkdLueftung": (958, Unknown), + "ID_SU_FstdLueftung": (959, Unknown), + "ID_Einst_Luf_Feuchteschutz_akt": (960, Unknown), + "ID_Einst_Luf_Reduziert_akt": (961, Unknown), + "ID_Einst_Luf_Nennlueftung_akt": (962, Unknown), + "ID_Einst_Luf_Intensivlueftung_akt": (963, Unknown), + "ID_Timer_Fil_4Makt": (964, Unknown), + "ID_Timer_Fil_WoAkt": (965, Unknown), + "ID_Sollwert_KuCft3_akt": (966, Celsius), + "ID_Sollwert_AtDif3_akt": (967, Celsius), + "ID_Bitmaske_0": (968, Unknown), + "ID_Einst_Lueftungsstufen": (969, Unknown), + "ID_SysEin_Meldung_TDI": (970, Unknown), + "ID_SysEin_Typ_WZW": (971, Unknown), + "ID_Einst_GLT_aktiviert": (972, Unknown), + "ID_Einst_BW_max": (973, Unknown), + "ID_Einst_Sollwert_TRL_Kuehlen": (974, Unknown), + "ID_Einst_Medium_Waermequelle": (975, Unknown), + "ID_Einst_Photovoltaik_akt": (976, Unknown), + "ID_Einst_Multispeicher_akt": (977, Unknown), + "ID_Einst_PKuehlTime_akt": (978, Unknown), + "ID_Einst_Minimale_Ruecklaufsolltemperatur": (979, Unknown), + "ID_RBE_Einflussfaktor_RT_akt": (980, Unknown), + "ID_RBE_Freigabe_Kuehlung_akt": (981, Unknown), + "ID_RBE_Waermeverteilsystem_akt": (982, Unknown), + "ID_RBE_Zeit_Heizstab_aktiv": (983, Unknown), + "ID_SEC_ND_Alarmgrenze": (984, Unknown), + "ID_SEC_HD_Alarmgrenze": (985, Unknown), + "ID_SEC_Abtauendtemperatur": (986, Unknown), + "ID_Einst_Min_RPM_BW": (987, Unknown), + "ID_Einst_Luf_Feuchteschutz_Faktor_akt": (988, Unknown), + "ID_Einst_Luf_Reduziert_Faktor_akt": (989, Unknown), + "ID_Einst_Luf_Nennlueftung_Faktor_akt": (990, Unknown), + "ID_Einst_Luf_Intensivlueftung_Faktor_akt": (991, Unknown), + "ID_Einst_Freigabe_Zeit_ZWE": (992, Unknown), + "ID_Einst_min_VL_Kuehl": (993, Unknown), + "ID_Einst_Warmwasser_Nachheizung": (994, Unknown), + "ID_Switchoff_file_LWD2_0_0": (995, Unknown), + "ID_Switchoff_file_LWD2_1_0": (996, Unknown), + "ID_Switchoff_file_LWD2_2_0": (997, Unknown), + "ID_Switchoff_file_LWD2_3_0": (998, Unknown), + "ID_Switchoff_file_LWD2_4_0": (999, Unknown), + "ID_Switchoff_file_LWD2_0_1": (1000, Unknown), + "ID_Switchoff_file_LWD2_1_1": (1001, Unknown), + "ID_Switchoff_file_LWD2_2_1": (1002, Unknown), + "ID_Switchoff_file_LWD2_3_1": (1003, Unknown), + "ID_Switchoff_file_LWD2_4_1": (1004, Unknown), + "ID_Switchoff_index_LWD2": (1005, Unknown), + "ID_Einst_Effizienzpumpe_Nominal_2": (1006, Unknown), + "ID_Einst_Effizienzpumpe_Minimal_2": (1007, Unknown), + "ID_Einst_Wm_Versorgung_Korrektur_2": (1008, Unknown), + "ID_Einst_Wm_Auswertung_Korrektur_2": (1009, Unknown), + "ID_Einst_isTwin": (1010, Unknown), + "ID_Einst_TAmin_2": (1011, Unknown), + "ID_Einst_TVLmax_2": (1012, Unknown), + "ID_Einst_TA_EG_2": (1013, Unknown), + "ID_Einst_TVLmax_EG_2": (1014, Unknown), + "ID_Waermemenge_Hz_2": (1015, Unknown), + "ID_Waermemenge_BW_2": (1016, Unknown), + "ID_Waermemenge_SW_2": (1017, Unknown), + "ID_Waermemenge_Seit_2": (1018, Unknown), + "ID_Einst_Entl_Typ_15_2": (1019, Unknown), + "ID_Einst_WW_Nachheizung_max": (1020, Unknown), + "ID_Einst_Kuhl_Zeit_Ein_RT": (1021, Unknown), + "ID_Einst_ZWE1_Pos": (1022, Unknown), + "ID_Einst_ZWE2_Pos": (1023, Unknown), + "ID_Einst_ZWE3_Pos": (1024, Unknown), + "ID_Einst_Leistung_ZWE": (1025, Unknown), + "ID_WP_SN2_DATUM": (1026, Unknown), + "ID_WP_SN2_HEX": (1027, Unknown), + "ID_WP_SN2_INDEX": (1028, Unknown), + "ID_CWP_saved2": (1029, Unknown), + "ID_Einst_SmartGrid": (1030, Unknown), + "ID_Einst_P155_HDS": (1031, Unknown), + "ID_Einst_P155_PumpHeat_Max": (1032, Unknown), + "ID_Einst_P155_PumpHeatCtrl": (1033, Unknown), + "ID_Einst_P155_PumpDHWCtrl": (1034, Unknown), + "ID_Einst_P155_PumpDHW_RPM": (1035, Unknown), + "ID_Einst_P155_PumpPoolCtrl": (1036, Unknown), + "ID_Einst_P155_PumpPool_RPM": (1037, Unknown), + "ID_Einst_P155_PumpCool_RPM": (1038, Unknown), + "ID_Einst_P155_PumpVBOCtrl": (1039, Unknown), + "ID_Einst_P155_PumpVBO_RPM_C": (1040, Unknown), + "ID_Einst_P155_PumpDHW_Max": (1041, Unknown), + "ID_Einst_P155_PumpPool_Max": (1042, Unknown), + "ID_Einst_P155_Sperrband_1": (1043, Unknown), + "ID_Einst_P155_Leistungsfreigabe": (1044, Unknown), + "ID_Einst_P155_DHW_Freq": (1045, Unknown), + "ID_Einst_SWHUP": (1046, Unknown), + "ID_Einst_P155_SWB_Freq": (1047, Unknown), + "ID_Einst_MK1_Regelung": (1048, Unknown), + "ID_Einst_MK2_Regelung": (1049, Unknown), + "ID_Einst_MK3_Regelung": (1050, Unknown), + "ID_Einst_PV_WW_Sperrzeit": (1051, Unknown), + "ID_Einst_Warmwasser_extra": (1052, Unknown), + "ID_Einst_Vorl_akt_Kuehl": (1053, Unknown), + "ID_WP_SN3_DATUM": (1054, Unknown), + "ID_WP_SN3_HEX": (1055, Unknown), + "ID_WP_SN3_INDEX": (1056, Unknown), + "ID_Einst_Vorlauf_ZUP": (1057, Unknown), + "ID_Einst_Abtauen_im_Warmwasser": (1058, Unknown), + "ID_Waermemenge_ZWE": (1059, Unknown), + "ID_Waermemenge_Reset": (1060, Unknown), + "ID_Waermemenge_Reset_2": (1061, Unknown), + "ID_Einst_Brunnenpumpe_min": (1062, Unknown), + "ID_Einst_Brunnenpumpe_max": (1063, Unknown), + "ID_Einst_SmartHomeID": (1064, Unknown), + "ID_Einst_SmartHK": (1065, Unknown), + "ID_Einst_SmartMK1": (1066, Unknown), + "ID_Einst_SmartMK2": (1067, Unknown), + "ID_Einst_SmartMK3": (1068, Unknown), + "ID_Einst_SmartWW": (1069, Unknown), + "ID_Einst_SmartDefrost": (1070, Unknown), + "ID_Einst_Empty1071": (1071, Unknown), + "ID_Einst_MinVLMK1": (1072, Unknown), + "ID_Einst_MinVLMK2": (1073, Unknown), + "ID_Einst_MinVLMK3": (1074, Unknown), + "ID_Einst_MaxVLMK1": (1075, Unknown), + "ID_Einst_MaxVLMK2": (1076, Unknown), + "ID_Einst_MaxVLMK3": (1077, Unknown), + "ID_Einst_SmartPlusHz": (1078, Unknown), + "ID_Einst_SmartMinusHz": (1079, Unknown), + "ID_Einst_SmartPlusMK1": (1080, Unknown), + "ID_Einst_SmartMinusMK1": (1081, Unknown), + "ID_Einst_SmartPlusMK2": (1082, Unknown), + "ID_Einst_SmartMinusMK2": (1083, Unknown), + "ID_Einst_SmartPlusMK3": (1084, Unknown), + "ID_Einst_SmartMinusMK3": (1085, Unknown), + "Unknown_Parameter_1086": (1086, Unknown), + "Unknown_Parameter_1087": (1087, Unknown), + "Unknown_Parameter_1088": (1088, Unknown), + "Unknown_Parameter_1089": (1089, Unknown), + "Unknown_Parameter_1090": (1090, Unknown), + "Unknown_Parameter_1091": (1091, Unknown), + "Unknown_Parameter_1092": (1092, Unknown), + "Unknown_Parameter_1093": (1093, Unknown), + "Unknown_Parameter_1094": (1094, Unknown), + "Unknown_Parameter_1095": (1095, Unknown), + "Unknown_Parameter_1096": (1096, Unknown), + "Unknown_Parameter_1097": (1097, Unknown), + "Unknown_Parameter_1098": (1098, Unknown), + "Unknown_Parameter_1099": (1099, Unknown), + "Unknown_Parameter_1100": (1100, Unknown), + "Unknown_Parameter_1101": (1101, Unknown), + "Unknown_Parameter_1102": (1102, Unknown), + "Unknown_Parameter_1103": (1103, Unknown), + "Unknown_Parameter_1104": (1104, Unknown), + "Unknown_Parameter_1105": (1105, Unknown), + "Unknown_Parameter_1106": (1106, Unknown), + "Unknown_Parameter_1107": (1107, Unknown), + "Unknown_Parameter_1108": (1108, Unknown), + "Unknown_Parameter_1109": (1109, Unknown), + "Unknown_Parameter_1110": (1110, Unknown), + "Unknown_Parameter_1111": (1111, Unknown), + "Unknown_Parameter_1112": (1112, Unknown), + "Unknown_Parameter_1113": (1113, Unknown), + "Unknown_Parameter_1114": (1114, Unknown), + "Unknown_Parameter_1115": (1115, Unknown), + "Unknown_Parameter_1116": (1116, Unknown), + "Unknown_Parameter_1117": (1117, Unknown), + "Unknown_Parameter_1118": (1118, Unknown), + "Unknown_Parameter_1119": (1119, Unknown), + "Unknown_Parameter_1120": (1120, Unknown), + "Unknown_Parameter_1121": (1121, Unknown), + "Unknown_Parameter_1122": (1122, Unknown), + "Unknown_Parameter_1123": (1123, Unknown), + "Unknown_Parameter_1124": (1124, Unknown), + "Unknown_Parameter_1125": (1125, Unknown), # New in 'main' branch: "SILENT_MODE": 1087, "ID_Einst_SuSilence": 1092, @@ -1210,256 +1210,257 @@ def test_compatibilities(self): calcs = { # Status of 0.3.14: - "Unknown_Calculation_0": 0, - "Unknown_Calculation_1": 1, - "Unknown_Calculation_2": 2, - "Unknown_Calculation_3": 3, - "Unknown_Calculation_4": 4, - "Unknown_Calculation_5": 5, - "Unknown_Calculation_6": 6, - "Unknown_Calculation_7": 7, - "Unknown_Calculation_8": 8, - "Unknown_Calculation_9": 9, - "ID_WEB_Temperatur_TVL": 10, - "ID_WEB_Temperatur_TRL": 11, - "ID_WEB_Sollwert_TRL_HZ": 12, - "ID_WEB_Temperatur_TRL_ext": 13, - "ID_WEB_Temperatur_THG": 14, - "ID_WEB_Temperatur_TA": 15, - "ID_WEB_Mitteltemperatur": 16, - "ID_WEB_Temperatur_TBW": 17, - "ID_WEB_Einst_BWS_akt": 18, - "ID_WEB_Temperatur_TWE": 19, - "ID_WEB_Temperatur_TWA": 20, - "ID_WEB_Temperatur_TFB1": 21, - "ID_WEB_Sollwert_TVL_MK1": 22, - "ID_WEB_Temperatur_RFV": 23, - "ID_WEB_Temperatur_TFB2": 24, - "ID_WEB_Sollwert_TVL_MK2": 25, - "ID_WEB_Temperatur_TSK": 26, - "ID_WEB_Temperatur_TSS": 27, - "ID_WEB_Temperatur_TEE": 28, - "ID_WEB_ASDin": 29, - "ID_WEB_BWTin": 30, - "ID_WEB_EVUin": 31, - "ID_WEB_HDin": 32, - "ID_WEB_MOTin": 33, - "ID_WEB_NDin": 34, - "ID_WEB_PEXin": 35, - "ID_WEB_SWTin": 36, - "ID_WEB_AVout": 37, - "ID_WEB_BUPout": 38, - "ID_WEB_HUPout": 39, - "ID_WEB_MA1out": 40, - "ID_WEB_MZ1out": 41, - "ID_WEB_VENout": 42, - "ID_WEB_VBOout": 43, - "ID_WEB_VD1out": 44, - "ID_WEB_VD2out": 45, - "ID_WEB_ZIPout": 46, - "ID_WEB_ZUPout": 47, - "ID_WEB_ZW1out": 48, - "ID_WEB_ZW2SSTout": 49, - "ID_WEB_ZW3SSTout": 50, - "ID_WEB_FP2out": 51, - "ID_WEB_SLPout": 52, - "ID_WEB_SUPout": 53, - "ID_WEB_MZ2out": 54, - "ID_WEB_MA2out": 55, - "ID_WEB_Zaehler_BetrZeitVD1": 56, - "ID_WEB_Zaehler_BetrZeitImpVD1": 57, - "ID_WEB_Zaehler_BetrZeitVD2": 58, - "ID_WEB_Zaehler_BetrZeitImpVD2": 59, - "ID_WEB_Zaehler_BetrZeitZWE1": 60, - "ID_WEB_Zaehler_BetrZeitZWE2": 61, - "ID_WEB_Zaehler_BetrZeitZWE3": 62, - "ID_WEB_Zaehler_BetrZeitWP": 63, - "ID_WEB_Zaehler_BetrZeitHz": 64, - "ID_WEB_Zaehler_BetrZeitBW": 65, - "ID_WEB_Zaehler_BetrZeitKue": 66, - "ID_WEB_Time_WPein_akt": 67, - "ID_WEB_Time_ZWE1_akt": 68, - "ID_WEB_Time_ZWE2_akt": 69, - "ID_WEB_Timer_EinschVerz": 70, - "ID_WEB_Time_SSPAUS_akt": 71, - "ID_WEB_Time_SSPEIN_akt": 72, - "ID_WEB_Time_VDStd_akt": 73, - "ID_WEB_Time_HRM_akt": 74, - "ID_WEB_Time_HRW_akt": 75, - "ID_WEB_Time_LGS_akt": 76, - "ID_WEB_Time_SBW_akt": 77, - "ID_WEB_Code_WP_akt": 78, - "ID_WEB_BIV_Stufe_akt": 79, - "ID_WEB_WP_BZ_akt": 80, - "ID_WEB_AdresseIP_akt": 91, - "ID_WEB_SubNetMask_akt": 92, - "ID_WEB_Add_Broadcast": 93, - "ID_WEB_Add_StdGateway": 94, - "ID_WEB_ERROR_Time0": 95, - "ID_WEB_ERROR_Time1": 96, - "ID_WEB_ERROR_Time2": 97, - "ID_WEB_ERROR_Time3": 98, - "ID_WEB_ERROR_Time4": 99, - "ID_WEB_ERROR_Nr0": 100, - "ID_WEB_ERROR_Nr1": 101, - "ID_WEB_ERROR_Nr2": 102, - "ID_WEB_ERROR_Nr3": 103, - "ID_WEB_ERROR_Nr4": 104, - "ID_WEB_AnzahlFehlerInSpeicher": 105, - "ID_WEB_Switchoff_file_Nr0": 106, - "ID_WEB_Switchoff_file_Nr1": 107, - "ID_WEB_Switchoff_file_Nr2": 108, - "ID_WEB_Switchoff_file_Nr3": 109, - "ID_WEB_Switchoff_file_Nr4": 110, - "ID_WEB_Switchoff_file_Time0": 111, - "ID_WEB_Switchoff_file_Time1": 112, - "ID_WEB_Switchoff_file_Time2": 113, - "ID_WEB_Switchoff_file_Time3": 114, - "ID_WEB_Switchoff_file_Time4": 115, - "ID_WEB_Comfort_exists": 116, - "ID_WEB_HauptMenuStatus_Zeile1": 117, - "ID_WEB_HauptMenuStatus_Zeile2": 118, - "ID_WEB_HauptMenuStatus_Zeile3": 119, - "ID_WEB_HauptMenuStatus_Zeit": 120, - "ID_WEB_HauptMenuAHP_Stufe": 121, - "ID_WEB_HauptMenuAHP_Temp": 122, - "ID_WEB_HauptMenuAHP_Zeit": 123, - "ID_WEB_SH_BWW": 124, - "ID_WEB_SH_HZ": 125, - "ID_WEB_SH_MK1": 126, - "ID_WEB_SH_MK2": 127, - "ID_WEB_Einst_Kurzrpgramm": 128, - "ID_WEB_StatusSlave_1": 129, - "ID_WEB_StatusSlave_2": 130, - "ID_WEB_StatusSlave_3": 131, - "ID_WEB_StatusSlave_4": 132, - "ID_WEB_StatusSlave_5": 133, - "ID_WEB_AktuelleTimeStamp": 134, - "ID_WEB_SH_MK3": 135, - "ID_WEB_Sollwert_TVL_MK3": 136, - "ID_WEB_Temperatur_TFB3": 137, - "ID_WEB_MZ3out": 138, - "ID_WEB_MA3out": 139, - "ID_WEB_FP3out": 140, - "ID_WEB_Time_AbtIn": 141, - "ID_WEB_Temperatur_RFV2": 142, - "ID_WEB_Temperatur_RFV3": 143, - "ID_WEB_SH_SW": 144, - "ID_WEB_Zaehler_BetrZeitSW": 145, - "ID_WEB_FreigabKuehl": 146, - "ID_WEB_AnalogIn": 147, - "ID_WEB_SonderZeichen": 148, - "ID_WEB_SH_ZIP": 149, - "ID_WEB_WebsrvProgrammWerteBeobarten": 150, - "ID_WEB_WMZ_Heizung": 151, - "ID_WEB_WMZ_Brauchwasser": 152, - "ID_WEB_WMZ_Schwimmbad": 153, - "ID_WEB_WMZ_Seit": 154, - "ID_WEB_WMZ_Durchfluss": 155, - "ID_WEB_AnalogOut1": 156, - "ID_WEB_AnalogOut2": 157, - "ID_WEB_Time_Heissgas": 158, - "ID_WEB_Temp_Lueftung_Zuluft": 159, - "ID_WEB_Temp_Lueftung_Abluft": 160, - "ID_WEB_Zaehler_BetrZeitSolar": 161, - "ID_WEB_AnalogOut3": 162, - "ID_WEB_AnalogOut4": 163, - "ID_WEB_Out_VZU": 164, - "ID_WEB_Out_VAB": 165, - "ID_WEB_Out_VSK": 166, - "ID_WEB_Out_FRH": 167, - "ID_WEB_AnalogIn2": 168, - "ID_WEB_AnalogIn3": 169, - "ID_WEB_SAXin": 170, - "ID_WEB_SPLin": 171, - "ID_WEB_Compact_exists": 172, - "ID_WEB_Durchfluss_WQ": 173, - "ID_WEB_LIN_exists": 174, - "ID_WEB_LIN_ANSAUG_VERDAMPFER": 175, - "ID_WEB_LIN_ANSAUG_VERDICHTER": 176, - "ID_WEB_LIN_VDH": 177, - "ID_WEB_LIN_UH": 178, - "ID_WEB_LIN_UH_Soll": 179, - "ID_WEB_LIN_HD": 180, - "ID_WEB_LIN_ND": 181, - "ID_WEB_LIN_VDH_out": 182, - "ID_WEB_HZIO_PWM": 183, - "ID_WEB_HZIO_VEN": 184, - "ID_WEB_HZIO_EVU2": 185, - "ID_WEB_HZIO_STB": 186, - "ID_WEB_SEC_Qh_Soll": 187, - "ID_WEB_SEC_Qh_Ist": 188, - "ID_WEB_SEC_TVL_Soll": 189, - "ID_WEB_SEC_Software": 190, - "ID_WEB_SEC_BZ": 191, - "ID_WEB_SEC_VWV": 192, - "ID_WEB_SEC_VD": 193, - "ID_WEB_SEC_VerdEVI": 194, - "ID_WEB_SEC_AnsEVI": 195, - "ID_WEB_SEC_UEH_EVI": 196, - "ID_WEB_SEC_UEH_EVI_S": 197, - "ID_WEB_SEC_KondTemp": 198, - "ID_WEB_SEC_FlussigEx": 199, - "ID_WEB_SEC_UK_EEV": 200, - "ID_WEB_SEC_EVI_Druck": 201, - "ID_WEB_SEC_U_Inv": 202, - "ID_WEB_Temperatur_THG_2": 203, - "ID_WEB_Temperatur_TWE_2": 204, - "ID_WEB_LIN_ANSAUG_VERDAMPFER_2": 205, - "ID_WEB_LIN_ANSAUG_VERDICHTER_2": 206, - "ID_WEB_LIN_VDH_2": 207, - "ID_WEB_LIN_UH_2": 208, - "ID_WEB_LIN_UH_Soll_2": 209, - "ID_WEB_LIN_HD_2": 210, - "ID_WEB_LIN_ND_2": 211, - "ID_WEB_HDin_2": 212, - "ID_WEB_AVout_2": 213, - "ID_WEB_VBOout_2": 214, - "ID_WEB_VD1out_2": 215, - "ID_WEB_LIN_VDH_out_2": 216, - "ID_WEB_Switchoff2_file_Nr0": 217, - "ID_WEB_Switchoff2_file_Nr1": 218, - "ID_WEB_Switchoff2_file_Nr2": 219, - "ID_WEB_Switchoff2_file_Nr3": 220, - "ID_WEB_Switchoff2_file_Nr4": 221, - "ID_WEB_Switchoff2_file_Time0": 222, - "ID_WEB_Switchoff2_file_Time1": 223, - "ID_WEB_Switchoff2_file_Time2": 224, - "ID_WEB_Switchoff2_file_Time3": 225, - "ID_WEB_Switchoff2_file_Time4": 226, - "ID_WEB_RBE_RT_Ist": 227, - "ID_WEB_RBE_RT_Soll": 228, - "ID_WEB_Temperatur_BW_oben": 229, - "ID_WEB_Code_WP_akt_2": 230, - "ID_WEB_Freq_VD": 231, - "Unknown_Calculation_232": 232, - "Unknown_Calculation_233": 233, - "Unknown_Calculation_234": 234, - "Unknown_Calculation_235": 235, - "Unknown_Calculation_236": 236, - "Unknown_Calculation_237": 237, - "Unknown_Calculation_238": 238, - "Unknown_Calculation_239": 239, - "Unknown_Calculation_240": 240, - "Circulation_Pump": 241, - "Unknown_Calculation_242": 242, - "Unknown_Calculation_243": 243, - "Unknown_Calculation_244": 244, - "Unknown_Calculation_245": 245, - "Unknown_Calculation_246": 246, - "Unknown_Calculation_247": 247, - "Unknown_Calculation_248": 248, - "Unknown_Calculation_249": 249, - "Unknown_Calculation_250": 250, - "Unknown_Calculation_251": 251, - "Unknown_Calculation_252": 252, - "Unknown_Calculation_253": 253, - "Flow_Rate_254": 254, - "Unknown_Calculation_255": 255, - "Unknown_Calculation_256": 256, - "Heat_Output": 257, - "Unknown_Calculation_258": 258, - "Unknown_Calculation_259": 259, + "Unknown_Calculation_0": (0, Unknown), + "Unknown_Calculation_1": (1, Unknown), + "Unknown_Calculation_2": (2, Unknown), + "Unknown_Calculation_3": (3, Unknown), + "Unknown_Calculation_4": (4, Unknown), + "Unknown_Calculation_5": (5, Unknown), + "Unknown_Calculation_6": (6, Unknown), + "Unknown_Calculation_7": (7, Unknown), + "Unknown_Calculation_8": (8, Unknown), + "Unknown_Calculation_9": (9, Unknown), + "ID_WEB_Temperatur_TVL": (10, Celsius), + "ID_WEB_Temperatur_TRL": (11, Celsius), + "ID_WEB_Sollwert_TRL_HZ": (12, Celsius), + "ID_WEB_Temperatur_TRL_ext": (13, Celsius), + "ID_WEB_Temperatur_THG": (14, Celsius), + "ID_WEB_Temperatur_TA": (15, Celsius), + "ID_WEB_Mitteltemperatur": (16, Celsius), + "ID_WEB_Temperatur_TBW": (17, Celsius), + "ID_WEB_Einst_BWS_akt": (18, Celsius), + "ID_WEB_Temperatur_TWE": (19, Celsius), + "ID_WEB_Temperatur_TWA": (20, Celsius), + "ID_WEB_Temperatur_TFB1": (21, Celsius), + "ID_WEB_Sollwert_TVL_MK1": (22, Celsius), + "ID_WEB_Temperatur_RFV": (23, Celsius), + "ID_WEB_Temperatur_TFB2": (24, Celsius), + "ID_WEB_Sollwert_TVL_MK2": (25, Celsius), + "ID_WEB_Temperatur_TSK": (26, Celsius), + "ID_WEB_Temperatur_TSS": (27, Celsius), + "ID_WEB_Temperatur_TEE": (28, Celsius), + "ID_WEB_ASDin": (29, Bool), + "ID_WEB_BWTin": (30, Bool), + "ID_WEB_EVUin": (31, Bool), + "ID_WEB_HDin": (32, Bool), + "ID_WEB_MOTin": (33, Bool), + "ID_WEB_NDin": (34, Bool), + "ID_WEB_PEXin": (35, Bool), + "ID_WEB_SWTin": (36, Bool), + "ID_WEB_AVout": (37, Bool), + "ID_WEB_BUPout": (38, Bool), + "ID_WEB_HUPout": (39, Bool), + "ID_WEB_MA1out": (40, Bool), + "ID_WEB_MZ1out": (41, Bool), + "ID_WEB_VENout": (42, Bool), + "ID_WEB_VBOout": (43, Bool), + "ID_WEB_VD1out": (44, Bool), + "ID_WEB_VD2out": (45, Bool), + "ID_WEB_ZIPout": (46, Bool), + "ID_WEB_ZUPout": (47, Bool), + "ID_WEB_ZW1out": (48, Bool), + "ID_WEB_ZW2SSTout": (49, Bool), + "ID_WEB_ZW3SSTout": (50, Bool), + "ID_WEB_FP2out": (51, Bool), + "ID_WEB_SLPout": (52, Bool), + "ID_WEB_SUPout": (53, Bool), + "ID_WEB_MZ2out": (54, Bool), + "ID_WEB_MA2out": (55, Bool), + "ID_WEB_Zaehler_BetrZeitVD1": (56, Seconds), + "ID_WEB_Zaehler_BetrZeitImpVD1": (57, Pulses), + "ID_WEB_Zaehler_BetrZeitVD2": (58, Seconds), + "ID_WEB_Zaehler_BetrZeitImpVD2": (59, Pulses), + "ID_WEB_Zaehler_BetrZeitZWE1": (60, Seconds), + "ID_WEB_Zaehler_BetrZeitZWE2": (61, Seconds), + "ID_WEB_Zaehler_BetrZeitZWE3": (62, Seconds), + "ID_WEB_Zaehler_BetrZeitWP": (63, Seconds), + "ID_WEB_Zaehler_BetrZeitHz": (64, Seconds), + "ID_WEB_Zaehler_BetrZeitBW": (65, Seconds), + "ID_WEB_Zaehler_BetrZeitKue": (66, Seconds), + "ID_WEB_Time_WPein_akt": (67, Seconds), + "ID_WEB_Time_ZWE1_akt": (68, Seconds), + "ID_WEB_Time_ZWE2_akt": (69, Seconds), + "ID_WEB_Timer_EinschVerz": (70, Seconds), + "ID_WEB_Time_SSPAUS_akt": (71, Seconds), + "ID_WEB_Time_SSPEIN_akt": (72, Seconds), + "ID_WEB_Time_VDStd_akt": (73, Seconds), + "ID_WEB_Time_HRM_akt": (74, Seconds), + "ID_WEB_Time_HRW_akt": (75, Seconds), + "ID_WEB_Time_LGS_akt": (76, Seconds), + "ID_WEB_Time_SBW_akt": (77, Seconds), + "ID_WEB_Code_WP_akt": (78, HeatpumpCode), + "ID_WEB_BIV_Stufe_akt": (79, BivalenceLevel), + "ID_WEB_WP_BZ_akt": (80, OperationMode), + "ID_WEB_SoftStand": (81, Version), + "ID_WEB_AdresseIP_akt": (91, IPAddress), + "ID_WEB_SubNetMask_akt": (92, IPAddress), + "ID_WEB_Add_Broadcast": (93, IPAddress), + "ID_WEB_Add_StdGateway": (94, IPAddress), + "ID_WEB_ERROR_Time0": (95, Timestamp), + "ID_WEB_ERROR_Time1": (96, Timestamp), + "ID_WEB_ERROR_Time2": (97, Timestamp), + "ID_WEB_ERROR_Time3": (98, Timestamp), + "ID_WEB_ERROR_Time4": (99, Timestamp), + "ID_WEB_ERROR_Nr0": (100, Errorcode), + "ID_WEB_ERROR_Nr1": (101, Errorcode), + "ID_WEB_ERROR_Nr2": (102, Errorcode), + "ID_WEB_ERROR_Nr3": (103, Errorcode), + "ID_WEB_ERROR_Nr4": (104, Errorcode), + "ID_WEB_AnzahlFehlerInSpeicher": (105, Count), + "ID_WEB_Switchoff_file_Nr0": (106, SwitchoffFile), + "ID_WEB_Switchoff_file_Nr1": (107, SwitchoffFile), + "ID_WEB_Switchoff_file_Nr2": (108, SwitchoffFile), + "ID_WEB_Switchoff_file_Nr3": (109, SwitchoffFile), + "ID_WEB_Switchoff_file_Nr4": (110, SwitchoffFile), + "ID_WEB_Switchoff_file_Time0": (111, Timestamp), + "ID_WEB_Switchoff_file_Time1": (112, Timestamp), + "ID_WEB_Switchoff_file_Time2": (113, Timestamp), + "ID_WEB_Switchoff_file_Time3": (114, Timestamp), + "ID_WEB_Switchoff_file_Time4": (115, Timestamp), + "ID_WEB_Comfort_exists": (116, Bool), + "ID_WEB_HauptMenuStatus_Zeile1": (117, MainMenuStatusLine1), + "ID_WEB_HauptMenuStatus_Zeile2": (118, MainMenuStatusLine2), + "ID_WEB_HauptMenuStatus_Zeile3": (119, MainMenuStatusLine3), + "ID_WEB_HauptMenuStatus_Zeit": (120, Seconds), + "ID_WEB_HauptMenuAHP_Stufe": (121, Level), + "ID_WEB_HauptMenuAHP_Temp": (122, Celsius), + "ID_WEB_HauptMenuAHP_Zeit": (123, Seconds), + "ID_WEB_SH_BWW": (124, Bool), + "ID_WEB_SH_HZ": (125, Icon), + "ID_WEB_SH_MK1": (126, Icon), + "ID_WEB_SH_MK2": (127, Icon), + "ID_WEB_Einst_Kurzrpgramm": (128, Unknown), + "ID_WEB_StatusSlave_1": (129, Unknown), + "ID_WEB_StatusSlave_2": (130, Unknown), + "ID_WEB_StatusSlave_3": (131, Unknown), + "ID_WEB_StatusSlave_4": (132, Unknown), + "ID_WEB_StatusSlave_5": (133, Unknown), + "ID_WEB_AktuelleTimeStamp": (134, Timestamp), + "ID_WEB_SH_MK3": (135, Icon), + "ID_WEB_Sollwert_TVL_MK3": (136, Celsius), + "ID_WEB_Temperatur_TFB3": (137, Celsius), + "ID_WEB_MZ3out": (138, Bool), + "ID_WEB_MA3out": (139, Bool), + "ID_WEB_FP3out": (140, Bool), + "ID_WEB_Time_AbtIn": (141, Seconds), + "ID_WEB_Temperatur_RFV2": (142, Celsius), + "ID_WEB_Temperatur_RFV3": (143, Celsius), + "ID_WEB_SH_SW": (144, Icon), + "ID_WEB_Zaehler_BetrZeitSW": (145, Unknown), + "ID_WEB_FreigabKuehl": (146, Bool), + "ID_WEB_AnalogIn": (147, Voltage), + "ID_WEB_SonderZeichen": (148, Unknown), + "ID_WEB_SH_ZIP": (149, Icon), + "ID_WEB_WebsrvProgrammWerteBeobarten": (150, Icon), + "ID_WEB_WMZ_Heizung": (151, Energy), + "ID_WEB_WMZ_Brauchwasser": (152, Energy), + "ID_WEB_WMZ_Schwimmbad": (153, Energy), + "ID_WEB_WMZ_Seit": (154, Energy), + "ID_WEB_WMZ_Durchfluss": (155, Flow), + "ID_WEB_AnalogOut1": (156, Voltage), + "ID_WEB_AnalogOut2": (157, Voltage), + "ID_WEB_Time_Heissgas": (158, Seconds), + "ID_WEB_Temp_Lueftung_Zuluft": (159, Celsius), + "ID_WEB_Temp_Lueftung_Abluft": (160, Celsius), + "ID_WEB_Zaehler_BetrZeitSolar": (161, Seconds), + "ID_WEB_AnalogOut3": (162, Voltage), + "ID_WEB_AnalogOut4": (163, Voltage), + "ID_WEB_Out_VZU": (164, Voltage), + "ID_WEB_Out_VAB": (165, Voltage), + "ID_WEB_Out_VSK": (166, Bool), + "ID_WEB_Out_FRH": (167, Bool), + "ID_WEB_AnalogIn2": (168, Voltage), + "ID_WEB_AnalogIn3": (169, Voltage), + "ID_WEB_SAXin": (170, Bool), + "ID_WEB_SPLin": (171, Bool), + "ID_WEB_Compact_exists": (172, Bool), + "ID_WEB_Durchfluss_WQ": (173, Flow), + "ID_WEB_LIN_exists": (174, Bool), + "ID_WEB_LIN_ANSAUG_VERDAMPFER": (175, Celsius), + "ID_WEB_LIN_ANSAUG_VERDICHTER": (176, Celsius), + "ID_WEB_LIN_VDH": (177, Celsius), + "ID_WEB_LIN_UH": (178, Kelvin), + "ID_WEB_LIN_UH_Soll": (179, Kelvin), + "ID_WEB_LIN_HD": (180, Pressure), + "ID_WEB_LIN_ND": (181, Pressure), + "ID_WEB_LIN_VDH_out": (182, Bool), + "ID_WEB_HZIO_PWM": (183, Percent2), + "ID_WEB_HZIO_VEN": (184, Speed), + "ID_WEB_HZIO_EVU2": (185, Unknown), + "ID_WEB_HZIO_STB": (186, Bool), + "ID_WEB_SEC_Qh_Soll": (187, Energy), + "ID_WEB_SEC_Qh_Ist": (188, Energy), + "ID_WEB_SEC_TVL_Soll": (189, Celsius), + "ID_WEB_SEC_Software": (190, Unknown), + "ID_WEB_SEC_BZ": (191, SecOperationMode), + "ID_WEB_SEC_VWV": (192, Unknown), + "ID_WEB_SEC_VD": (193, Speed), + "ID_WEB_SEC_VerdEVI": (194, Celsius), + "ID_WEB_SEC_AnsEVI": (195, Celsius), + "ID_WEB_SEC_UEH_EVI": (196, Kelvin), + "ID_WEB_SEC_UEH_EVI_S": (197, Kelvin), + "ID_WEB_SEC_KondTemp": (198, Celsius), + "ID_WEB_SEC_FlussigEx": (199, Celsius), + "ID_WEB_SEC_UK_EEV": (200, Celsius), + "ID_WEB_SEC_EVI_Druck": (201, Pressure), + "ID_WEB_SEC_U_Inv": (202, Voltage), + "ID_WEB_Temperatur_THG_2": (203, Celsius), + "ID_WEB_Temperatur_TWE_2": (204, Celsius), + "ID_WEB_LIN_ANSAUG_VERDAMPFER_2": (205, Celsius), + "ID_WEB_LIN_ANSAUG_VERDICHTER_2": (206, Celsius), + "ID_WEB_LIN_VDH_2": (207, Celsius), + "ID_WEB_LIN_UH_2": (208, Kelvin), + "ID_WEB_LIN_UH_Soll_2": (209, Kelvin), + "ID_WEB_LIN_HD_2": (210, Pressure), + "ID_WEB_LIN_ND_2": (211, Pressure), + "ID_WEB_HDin_2": (212, Bool), + "ID_WEB_AVout_2": (213, Bool), + "ID_WEB_VBOout_2": (214, Bool), + "ID_WEB_VD1out_2": (215, Bool), + "ID_WEB_LIN_VDH_out_2": (216, Bool), + "ID_WEB_Switchoff2_file_Nr0": (217, SwitchoffFile), + "ID_WEB_Switchoff2_file_Nr1": (218, SwitchoffFile), + "ID_WEB_Switchoff2_file_Nr2": (219, SwitchoffFile), + "ID_WEB_Switchoff2_file_Nr3": (220, SwitchoffFile), + "ID_WEB_Switchoff2_file_Nr4": (221, SwitchoffFile), + "ID_WEB_Switchoff2_file_Time0": (222, Timestamp), + "ID_WEB_Switchoff2_file_Time1": (223, Timestamp), + "ID_WEB_Switchoff2_file_Time2": (224, Timestamp), + "ID_WEB_Switchoff2_file_Time3": (225, Timestamp), + "ID_WEB_Switchoff2_file_Time4": (226, Timestamp), + "ID_WEB_RBE_RT_Ist": (227, Celsius), + "ID_WEB_RBE_RT_Soll": (228, Celsius), + "ID_WEB_Temperatur_BW_oben": (229, Celsius), + "ID_WEB_Code_WP_akt_2": (230, HeatpumpCode), + "ID_WEB_Freq_VD": (231, Frequency), + "Unknown_Calculation_232": (232, Unknown), + "Unknown_Calculation_233": (233, Unknown), + "Unknown_Calculation_234": (234, Unknown), + "Unknown_Calculation_235": (235, Unknown), + "Unknown_Calculation_236": (236, Unknown), + "Unknown_Calculation_237": (237, Unknown), + "Unknown_Calculation_238": (238, Unknown), + "Unknown_Calculation_239": (239, Unknown), + "Unknown_Calculation_240": (240, Unknown), + "Circulation_Pump": (241, Percent2), + "Unknown_Calculation_242": (242, Unknown), + "Unknown_Calculation_243": (243, Unknown), + "Unknown_Calculation_244": (244, Unknown), + "Unknown_Calculation_245": (245, Unknown), + "Unknown_Calculation_246": (246, Unknown), + "Unknown_Calculation_247": (247, Unknown), + "Unknown_Calculation_248": (248, Unknown), + "Unknown_Calculation_249": (249, Unknown), + "Unknown_Calculation_250": (250, Unknown), + "Unknown_Calculation_251": (251, Unknown), + "Unknown_Calculation_252": (252, Unknown), + "Unknown_Calculation_253": (253, Unknown), + "Flow_Rate_254": (254, Flow), + "Unknown_Calculation_255": (255, Unknown), + "Unknown_Calculation_256": (256, Unknown), + "Heat_Output": (257, Power), + "Unknown_Calculation_258": (258, Unknown), + "Unknown_Calculation_259": (259, Unknown), # New in 'main' branch: "ID_WEB_SoftStand_0": 81, "ID_WEB_SoftStand_1": 82, @@ -1496,361 +1497,361 @@ def test_compatibilities(self): visis = { # Status of 0.3.14: - "ID_Visi_NieAnzeigen": 0, - "ID_Visi_ImmerAnzeigen": 1, - "ID_Visi_Heizung": 2, - "ID_Visi_Brauwasser": 3, - "ID_Visi_Schwimmbad": 4, - "ID_Visi_Kuhlung": 5, - "ID_Visi_Lueftung": 6, - "ID_Visi_MK1": 7, - "ID_Visi_MK2": 8, - "ID_Visi_ThermDesinfekt": 9, - "ID_Visi_Zirkulation": 10, - "ID_Visi_KuhlTemp_SolltempMK1": 11, - "ID_Visi_KuhlTemp_SolltempMK2": 12, - "ID_Visi_KuhlTemp_ATDiffMK1": 13, - "ID_Visi_KuhlTemp_ATDiffMK2": 14, - "ID_Visi_Service_Information": 15, - "ID_Visi_Service_Einstellung": 16, - "ID_Visi_Service_Sprache": 17, - "ID_Visi_Service_DatumUhrzeit": 18, - "ID_Visi_Service_Ausheiz": 19, - "ID_Visi_Service_Anlagenkonfiguration": 20, - "ID_Visi_Service_IBNAssistant": 21, - "ID_Visi_Service_ParameterIBNZuruck": 22, - "ID_Visi_Temp_Vorlauf": 23, - "ID_Visi_Temp_Rucklauf": 24, - "ID_Visi_Temp_RL_Soll": 25, - "ID_Visi_Temp_Ruecklext": 26, - "ID_Visi_Temp_Heissgas": 27, - "ID_Visi_Temp_Aussent": 28, - "ID_Visi_Temp_BW_Ist": 29, - "ID_Visi_Temp_BW_Soll": 30, - "ID_Visi_Temp_WQ_Ein": 31, - "ID_Visi_Temp_Kaltekreis": 32, - "ID_Visi_Temp_MK1_Vorlauf": 33, - "ID_Visi_Temp_MK1VL_Soll": 34, - "ID_Visi_Temp_Raumstation": 35, - "ID_Visi_Temp_MK2_Vorlauf": 36, - "ID_Visi_Temp_MK2VL_Soll": 37, - "ID_Visi_Temp_Solarkoll": 38, - "ID_Visi_Temp_Solarsp": 39, - "ID_Visi_Temp_Ext_Energ": 40, - "ID_Visi_IN_ASD": 41, - "ID_Visi_IN_BWT": 42, - "ID_Visi_IN_EVU": 43, - "ID_Visi_IN_HD": 44, - "ID_Visi_IN_MOT": 45, - "ID_Visi_IN_ND": 46, - "ID_Visi_IN_PEX": 47, - "ID_Visi_IN_SWT": 48, - "ID_Visi_OUT_Abtauventil": 49, - "ID_Visi_OUT_BUP": 50, - "ID_Visi_OUT_FUP1": 51, - "ID_Visi_OUT_HUP": 52, - "ID_Visi_OUT_Mischer1Auf": 53, - "ID_Visi_OUT_Mischer1Zu": 54, - "ID_Visi_OUT_Ventilation": 55, - "ID_Visi_OUT_Ventil_BOSUP": 56, - "ID_Visi_OUT_Verdichter1": 57, - "ID_Visi_OUT_Verdichter2": 58, - "ID_Visi_OUT_ZIP": 59, - "ID_Visi_OUT_ZUP": 60, - "ID_Visi_OUT_ZWE1": 61, - "ID_Visi_OUT_ZWE2_SST": 62, - "ID_Visi_OUT_ZWE3": 63, - "ID_Visi_OUT_FUP2": 64, - "ID_Visi_OUT_SLP": 65, - "ID_Visi_OUT_SUP": 66, - "ID_Visi_OUT_Mischer2Auf": 67, - "ID_Visi_OUT_Mischer2Zu": 68, - "ID_Visi_AblaufZ_WP_Seit": 69, - "ID_Visi_AblaufZ_ZWE1_seit": 70, - "ID_Visi_AblaufZ_ZWE2_seit": 71, - "ID_Visi_AblaufZ_ZWE3_seit": 72, - "ID_Visi_AblaufZ_Netzeinv": 73, - "ID_Visi_AblaufZ_SSP_Zeit1": 74, - "ID_Visi_AblaufZ_VD_Stand": 75, - "ID_Visi_AblaufZ_HRM_Zeit": 76, - "ID_Visi_AblaufZ_HRW_Zeit": 77, - "ID_Visi_AblaufZ_TDI_seit": 78, - "ID_Visi_AblaufZ_Sperre_BW": 79, - "ID_Visi_Bst_BStdVD1": 80, - "ID_Visi_Bst_ImpVD1": 81, - "ID_Visi_Bst_dEZVD1": 82, - "ID_Visi_Bst_BStdVD2": 83, - "ID_Visi_Bst_ImpVD2": 84, - "ID_Visi_Bst_dEZVD2": 85, - "ID_Visi_Bst_BStdZWE1": 86, - "ID_Visi_Bst_BStdZWE2": 87, - "ID_Visi_Bst_BStdZWE3": 88, - "ID_Visi_Bst_BStdWP": 89, - "ID_Visi_Text_Kurzprogramme": 90, - "ID_Visi_Text_Zwangsheizung": 91, - "ID_Visi_Text_Zwangsbrauchwasser": 92, - "ID_Visi_Text_Abtauen": 93, - "ID_Visi_EinstTemp_RucklBegr": 94, - "ID_Visi_EinstTemp_HystereseHR": 95, - "ID_Visi_EinstTemp_TRErhmax": 96, - "ID_Visi_EinstTemp_Freig2VD": 97, - "ID_Visi_EinstTemp_FreigZWE": 98, - "ID_Visi_EinstTemp_Tluftabt": 99, - "ID_Visi_EinstTemp_TDISolltemp": 100, - "ID_Visi_EinstTemp_HystereseBW": 101, - "ID_Visi_EinstTemp_Vorl2VDBW": 102, - "ID_Visi_EinstTemp_TAussenmax": 103, - "ID_Visi_EinstTemp_TAussenmin": 104, - "ID_Visi_EinstTemp_TWQmin": 105, - "ID_Visi_EinstTemp_THGmax": 106, - "ID_Visi_EinstTemp_TLABTEnde": 107, - "ID_Visi_EinstTemp_Absenkbis": 108, - "ID_Visi_EinstTemp_Vorlaufmax": 109, - "ID_Visi_EinstTemp_TDiffEin": 110, - "ID_Visi_EinstTemp_TDiffAus": 111, - "ID_Visi_EinstTemp_TDiffmax": 112, - "ID_Visi_EinstTemp_TEEHeizung": 113, - "ID_Visi_EinstTemp_TEEBrauchw": 114, - "ID_Visi_EinstTemp_Vorl2VDSW": 115, - "ID_Visi_EinstTemp_VLMaxMk1": 116, - "ID_Visi_EinstTemp_VLMaxMk2": 117, - "ID_Visi_Priori_Brauchwasser": 118, - "ID_Visi_Priori_Heizung": 119, - "ID_Visi_Priori_Schwimmbad": 120, - "ID_Visi_SysEin_EVUSperre": 121, - "ID_Visi_SysEin_Raumstation": 122, - "ID_Visi_SysEin_Einbindung": 123, - "ID_Visi_SysEin_Mischkreis1": 124, - "ID_Visi_SysEin_Mischkreis2": 125, - "ID_Visi_SysEin_ZWE1Art": 126, - "ID_Visi_SysEin_ZWE1Fkt": 127, - "ID_Visi_SysEin_ZWE2Art": 128, - "ID_Visi_SysEin_ZWE2Fkt": 129, - "ID_Visi_SysEin_ZWE3Art": 130, - "ID_Visi_SysEin_ZWE3Fkt": 131, - "ID_Visi_SysEin_Stoerung": 132, - "ID_Visi_SysEin_Brauchwasser1": 133, - "ID_Visi_SysEin_Brauchwasser2": 134, - "ID_Visi_SysEin_Brauchwasser3": 135, - "ID_Visi_SysEin_Brauchwasser4": 136, - "ID_Visi_SysEin_Brauchwasser5": 137, - "ID_Visi_SysEin_BWWPmax": 138, - "ID_Visi_SysEin_Abtzykmax": 139, - "ID_Visi_SysEin_Luftabt": 140, - "ID_Visi_SysEin_LuftAbtmax": 141, - "ID_Visi_SysEin_Abtauen1": 142, - "ID_Visi_SysEin_Abtauen2": 143, - "ID_Visi_SysEin_Pumpenoptim": 144, - "ID_Visi_SysEin_Zusatzpumpe": 145, - "ID_Visi_SysEin_Zugang": 146, - "ID_Visi_SysEin_SoledrDurchf": 147, - "ID_Visi_SysEin_UberwachungVD": 148, - "ID_Visi_SysEin_RegelungHK": 149, - "ID_Visi_SysEin_RegelungMK1": 150, - "ID_Visi_SysEin_RegelungMK2": 151, - "ID_Visi_SysEin_Kuhlung": 152, - "ID_Visi_SysEin_Ausheizen": 153, - "ID_Visi_SysEin_ElektrAnode": 154, - "ID_Visi_SysEin_SWBBer": 155, - "ID_Visi_SysEin_SWBMin": 156, - "ID_Visi_SysEin_Heizung": 157, - "ID_Visi_SysEin_PeriodeMk1": 158, - "ID_Visi_SysEin_LaufzeitMk1": 159, - "ID_Visi_SysEin_PeriodeMk2": 160, - "ID_Visi_SysEin_LaufzeitMk2": 161, - "ID_Visi_SysEin_Heizgrenze": 162, - "ID_Visi_Enlt_HUP": 163, - "ID_Visi_Enlt_ZUP": 164, - "ID_Visi_Enlt_BUP": 165, - "ID_Visi_Enlt_Ventilator_BOSUP": 166, - "ID_Visi_Enlt_MA1": 167, - "ID_Visi_Enlt_MZ1": 168, - "ID_Visi_Enlt_ZIP": 169, - "ID_Visi_Enlt_MA2": 170, - "ID_Visi_Enlt_MZ2": 171, - "ID_Visi_Enlt_SUP": 172, - "ID_Visi_Enlt_SLP": 173, - "ID_Visi_Enlt_FP2": 174, - "ID_Visi_Enlt_Laufzeit": 175, - "ID_Visi_Anlgkonf_Heizung": 176, - "ID_Visi_Anlgkonf_Brauchwarmwasser": 177, - "ID_Visi_Anlgkonf_Schwimmbad": 178, - "ID_Visi_Heizung_Betriebsart": 179, - "ID_Visi_Heizung_TemperaturPlusMinus": 180, - "ID_Visi_Heizung_Heizkurven": 181, - "ID_Visi_Heizung_Zeitschlaltprogramm": 182, - "ID_Visi_Heizung_Heizgrenze": 183, - "ID_Visi_Mitteltemperatur": 184, - "ID_Visi_Dataenlogger": 185, - "ID_Visi_Sprachen_DEUTSCH": 186, - "ID_Visi_Sprachen_ENGLISH": 187, - "ID_Visi_Sprachen_FRANCAIS": 188, - "ID_Visi_Sprachen_NORWAY": 189, - "ID_Visi_Sprachen_TCHECH": 190, - "ID_Visi_Sprachen_ITALIANO": 191, - "ID_Visi_Sprachen_NEDERLANDS": 192, - "ID_Visi_Sprachen_SVENSKA": 193, - "ID_Visi_Sprachen_POLSKI": 194, - "ID_Visi_Sprachen_MAGYARUL": 195, - "ID_Visi_ErrorUSBspeichern": 196, - "ID_Visi_Bst_BStdHz": 197, - "ID_Visi_Bst_BStdBW": 198, - "ID_Visi_Bst_BStdKue": 199, - "ID_Visi_Service_Systemsteuerung": 200, - "ID_Visi_Service_Systemsteuerung_Contrast": 201, - "ID_Visi_Service_Systemsteuerung_Webserver": 202, - "ID_Visi_Service_Systemsteuerung_IPAdresse": 203, - "ID_Visi_Service_Systemsteuerung_Fernwartung": 204, - "ID_Visi_Paralleleschaltung": 205, - "ID_Visi_SysEin_Paralleleschaltung": 206, - "ID_Visi_Sprachen_DANSK": 207, - "ID_Visi_Sprachen_PORTUGES": 208, - "ID_Visi_Heizkurve_Heizung": 209, - "ID_Visi_SysEin_Mischkreis3": 210, - "ID_Visi_MK3": 211, - "ID_Visi_Temp_MK3_Vorlauf": 212, - "ID_Visi_Temp_MK3VL_Soll": 213, - "ID_Visi_OUT_Mischer3Auf": 214, - "ID_Visi_OUT_Mischer3Zu": 215, - "ID_Visi_SysEin_RegelungMK3": 216, - "ID_Visi_SysEin_PeriodeMk3": 217, - "ID_Visi_SysEin_LaufzeitMk3": 218, - "ID_Visi_SysEin_Kuhl_Zeit_Ein": 219, - "ID_Visi_SysEin_Kuhl_Zeit_Aus": 220, - "ID_Visi_AblaufZ_AbtauIn": 221, - "ID_Visi_Waermemenge_WS": 222, - "ID_Visi_Waermemenge_WQ": 223, - "ID_Visi_Enlt_MA3": 224, - "ID_Visi_Enlt_MZ3": 225, - "ID_Visi_Enlt_FP3": 226, - "ID_Visi_OUT_FUP3": 227, - "ID_Visi_Temp_Raumstation2": 228, - "ID_Visi_Temp_Raumstation3": 229, - "ID_Visi_Bst_BStdSW": 230, - "ID_Visi_Sprachen_LITAUISCH": 231, - "ID_Visi_Sprachen_ESTNICH": 232, - "ID_Visi_SysEin_Fernwartung": 233, - "ID_Visi_Sprachen_SLOVENISCH": 234, - "ID_Visi_EinstTemp_TA_EG": 235, - "ID_Visi_Einst_TVLmax_EG": 236, - "ID_Visi_SysEin_PoptNachlauf": 237, - "ID_Visi_RFV_K_Kuehlin": 238, - "ID_Visi_SysEin_EffizienzpumpeNom": 239, - "ID_Visi_SysEin_EffizienzpumpeMin": 240, - "ID_Visi_SysEin_Effizienzpumpe": 241, - "ID_Visi_SysEin_Waermemenge": 242, - "ID_Visi_Service_WMZ_Effizienz": 243, - "ID_Visi_SysEin_Wm_Versorgung_Korrektur": 244, - "ID_Visi_SysEin_Wm_Auswertung_Korrektur": 245, - "ID_Visi_IN_AnalogIn": 246, - "ID_Visi_Eins_SN_Eingabe": 247, - "ID_Visi_OUT_Analog_1": 248, - "ID_Visi_OUT_Analog_2": 249, - "ID_Visi_Solar": 250, - "ID_Visi_SysEin_Solar": 251, - "ID_Visi_EinstTemp_TDiffKollmax": 252, - "ID_Visi_AblaufZ_HG_Sperre": 253, - "ID_Visi_SysEin_Akt_Kuehlung": 254, - "ID_Visi_SysEin_Vorlauf_VBO": 255, - "ID_Visi_Einst_KRHyst": 256, - "ID_Visi_Einst_Akt_Kuehl_Speicher_min": 257, - "ID_Visi_Einst_Akt_Kuehl_Freig_WQE": 258, - "ID_Visi_SysEin_AbtZykMin": 259, - "ID_Visi_SysEin_VD2_Zeit_Min": 260, - "ID_Visi_EinstTemp_Hysterese_HR_verkuerzt": 261, - "ID_Visi_Einst_Luf_Feuchteschutz_akt": 262, - "ID_Visi_Einst_Luf_Reduziert_akt": 263, - "ID_Visi_Einst_Luf_Nennlueftung_akt": 264, - "ID_Visi_Einst_Luf_Intensivlueftung_akt": 265, - "ID_Visi_Temperatur_Lueftung_Zuluft": 266, - "ID_Visi_Temperatur_Lueftung_Abluft": 267, - "ID_Visi_OUT_Analog_3": 268, - "ID_Visi_OUT_Analog_4": 269, - "ID_Visi_IN_Analog_2": 270, - "ID_Visi_IN_Analog_3": 271, - "ID_Visi_IN_SAX": 272, - "ID_Visi_OUT_VZU": 273, - "ID_Visi_OUT_VAB": 274, - "ID_Visi_OUT_VSK": 275, - "ID_Visi_OUT_FRH": 276, - "ID_Visi_KuhlTemp_SolltempMK3": 277, - "ID_Visi_KuhlTemp_ATDiffMK3": 278, - "ID_Visi_IN_SPL": 279, - "ID_Visi_SysEin_Lueftungsstufen": 280, - "ID_Visi_SysEin_Meldung_TDI": 281, - "ID_Visi_SysEin_Typ_WZW": 282, - "ID_Visi_BACnet": 283, - "ID_Visi_Sprachen_SLOWAKISCH": 284, - "ID_Visi_Sprachen_LETTISCH": 285, - "ID_Visi_Sprachen_FINNISCH": 286, - "ID_Visi_Kalibrierung_LWD": 287, - "ID_Visi_IN_Durchfluss": 288, - "ID_Visi_LIN_ANSAUG_VERDICHTER": 289, - "ID_Visi_LIN_VDH": 290, - "ID_Visi_LIN_UH": 291, - "ID_Visi_LIN_Druck": 292, - "ID_Visi_Einst_Sollwert_TRL_Kuehlen": 293, - "ID_Visi_Entl_ExVentil": 294, - "ID_Visi_Einst_Medium_Waermequelle": 295, - "ID_Visi_Einst_Multispeicher": 296, - "ID_Visi_Einst_Minimale_Ruecklaufsolltemperatur": 297, - "ID_Visi_Einst_PKuehlTime": 298, - "ID_Visi_Sprachen_TUERKISCH": 299, - "ID_Visi_RBE": 300, - "ID_Visi_Einst_Luf_Stufen_Faktor": 301, - "ID_Visi_Freigabe_Zeit_ZWE": 302, - "ID_Visi_Einst_min_VL_Kuehl": 303, - "ID_Visi_ZWE1": 304, - "ID_Visi_ZWE2": 305, - "ID_Visi_ZWE3": 306, - "ID_Visi_SEC": 307, - "ID_Visi_HZIO": 308, - "ID_Visi_WPIO": 309, - "ID_Visi_LIN_ANSAUG_VERDAMPFER": 310, - "ID_Visi_LIN_MULTI1": 311, - "ID_Visi_LIN_MULTI2": 312, - "ID_Visi_Einst_Leistung_ZWE": 313, - "ID_Visi_Sprachen_ESPANOL": 314, - "ID_Visi_Temp_BW_oben": 315, - "ID_Visi_MAXIO": 316, - "ID_Visi_OUT_Abtauwunsch": 317, - "ID_Visi_SmartGrid": 318, - "ID_Visi_Drehzahlgeregelt": 319, - "ID_Visi_P155_Inverter": 320, - "ID_Visi_Leistungsfreigabe": 321, - "ID_Visi_Einst_Vorl_akt_Kuehl": 322, - "ID_Visi_Einst_Abtauen_im_Warmwasser": 323, - "ID_Visi_Waermemenge_ZWE": 324, - "Unknown_Visibility_325": 325, - "Unknown_Visibility_326": 326, - "Unknown_Visibility_327": 327, - "Unknown_Visibility_328": 328, - "Unknown_Visibility_329": 329, - "Unknown_Visibility_330": 330, - "Unknown_Visibility_331": 331, - "Unknown_Visibility_332": 332, - "Unknown_Visibility_333": 333, - "Unknown_Visibility_334": 334, - "Unknown_Visibility_335": 335, - "Unknown_Visibility_336": 336, - "Unknown_Visibility_337": 337, - "Unknown_Visibility_338": 338, - "Unknown_Visibility_339": 339, - "Unknown_Visibility_340": 340, - "Unknown_Visibility_341": 341, - "Unknown_Visibility_342": 342, - "Unknown_Visibility_343": 343, - "Unknown_Visibility_344": 344, - "Unknown_Visibility_345": 345, - "Unknown_Visibility_346": 346, - "Unknown_Visibility_347": 347, - "Unknown_Visibility_348": 348, - "Unknown_Visibility_349": 349, - "Unknown_Visibility_350": 350, - "Unknown_Visibility_351": 351, - "Unknown_Visibility_352": 352, - "Unknown_Visibility_353": 353, - "Unknown_Visibility_354": 354, + "ID_Visi_NieAnzeigen": (0, Unknown), + "ID_Visi_ImmerAnzeigen": (1, Unknown), + "ID_Visi_Heizung": (2, Unknown), + "ID_Visi_Brauwasser": (3, Unknown), + "ID_Visi_Schwimmbad": (4, Unknown), + "ID_Visi_Kuhlung": (5, Unknown), + "ID_Visi_Lueftung": (6, Unknown), + "ID_Visi_MK1": (7, Unknown), + "ID_Visi_MK2": (8, Unknown), + "ID_Visi_ThermDesinfekt": (9, Unknown), + "ID_Visi_Zirkulation": (10, Unknown), + "ID_Visi_KuhlTemp_SolltempMK1": (11, Unknown), + "ID_Visi_KuhlTemp_SolltempMK2": (12, Unknown), + "ID_Visi_KuhlTemp_ATDiffMK1": (13, Unknown), + "ID_Visi_KuhlTemp_ATDiffMK2": (14, Unknown), + "ID_Visi_Service_Information": (15, Unknown), + "ID_Visi_Service_Einstellung": (16, Unknown), + "ID_Visi_Service_Sprache": (17, Unknown), + "ID_Visi_Service_DatumUhrzeit": (18, Unknown), + "ID_Visi_Service_Ausheiz": (19, Unknown), + "ID_Visi_Service_Anlagenkonfiguration": (20, Unknown), + "ID_Visi_Service_IBNAssistant": (21, Unknown), + "ID_Visi_Service_ParameterIBNZuruck": (22, Unknown), + "ID_Visi_Temp_Vorlauf": (23, Unknown), + "ID_Visi_Temp_Rucklauf": (24, Unknown), + "ID_Visi_Temp_RL_Soll": (25, Unknown), + "ID_Visi_Temp_Ruecklext": (26, Unknown), + "ID_Visi_Temp_Heissgas": (27, Unknown), + "ID_Visi_Temp_Aussent": (28, Unknown), + "ID_Visi_Temp_BW_Ist": (29, Unknown), + "ID_Visi_Temp_BW_Soll": (30, Unknown), + "ID_Visi_Temp_WQ_Ein": (31, Unknown), + "ID_Visi_Temp_Kaltekreis": (32, Unknown), + "ID_Visi_Temp_MK1_Vorlauf": (33, Unknown), + "ID_Visi_Temp_MK1VL_Soll": (34, Unknown), + "ID_Visi_Temp_Raumstation": (35, Unknown), + "ID_Visi_Temp_MK2_Vorlauf": (36, Unknown), + "ID_Visi_Temp_MK2VL_Soll": (37, Unknown), + "ID_Visi_Temp_Solarkoll": (38, Unknown), + "ID_Visi_Temp_Solarsp": (39, Unknown), + "ID_Visi_Temp_Ext_Energ": (40, Unknown), + "ID_Visi_IN_ASD": (41, Unknown), + "ID_Visi_IN_BWT": (42, Unknown), + "ID_Visi_IN_EVU": (43, Unknown), + "ID_Visi_IN_HD": (44, Unknown), + "ID_Visi_IN_MOT": (45, Unknown), + "ID_Visi_IN_ND": (46, Unknown), + "ID_Visi_IN_PEX": (47, Unknown), + "ID_Visi_IN_SWT": (48, Unknown), + "ID_Visi_OUT_Abtauventil": (49, Unknown), + "ID_Visi_OUT_BUP": (50, Unknown), + "ID_Visi_OUT_FUP1": (51, Unknown), + "ID_Visi_OUT_HUP": (52, Unknown), + "ID_Visi_OUT_Mischer1Auf": (53, Unknown), + "ID_Visi_OUT_Mischer1Zu": (54, Unknown), + "ID_Visi_OUT_Ventilation": (55, Unknown), + "ID_Visi_OUT_Ventil_BOSUP": (56, Unknown), + "ID_Visi_OUT_Verdichter1": (57, Unknown), + "ID_Visi_OUT_Verdichter2": (58, Unknown), + "ID_Visi_OUT_ZIP": (59, Unknown), + "ID_Visi_OUT_ZUP": (60, Unknown), + "ID_Visi_OUT_ZWE1": (61, Unknown), + "ID_Visi_OUT_ZWE2_SST": (62, Unknown), + "ID_Visi_OUT_ZWE3": (63, Unknown), + "ID_Visi_OUT_FUP2": (64, Unknown), + "ID_Visi_OUT_SLP": (65, Unknown), + "ID_Visi_OUT_SUP": (66, Unknown), + "ID_Visi_OUT_Mischer2Auf": (67, Unknown), + "ID_Visi_OUT_Mischer2Zu": (68, Unknown), + "ID_Visi_AblaufZ_WP_Seit": (69, Unknown), + "ID_Visi_AblaufZ_ZWE1_seit": (70, Unknown), + "ID_Visi_AblaufZ_ZWE2_seit": (71, Unknown), + "ID_Visi_AblaufZ_ZWE3_seit": (72, Unknown), + "ID_Visi_AblaufZ_Netzeinv": (73, Unknown), + "ID_Visi_AblaufZ_SSP_Zeit1": (74, Unknown), + "ID_Visi_AblaufZ_VD_Stand": (75, Unknown), + "ID_Visi_AblaufZ_HRM_Zeit": (76, Unknown), + "ID_Visi_AblaufZ_HRW_Zeit": (77, Unknown), + "ID_Visi_AblaufZ_TDI_seit": (78, Unknown), + "ID_Visi_AblaufZ_Sperre_BW": (79, Unknown), + "ID_Visi_Bst_BStdVD1": (80, Unknown), + "ID_Visi_Bst_ImpVD1": (81, Unknown), + "ID_Visi_Bst_dEZVD1": (82, Unknown), + "ID_Visi_Bst_BStdVD2": (83, Unknown), + "ID_Visi_Bst_ImpVD2": (84, Unknown), + "ID_Visi_Bst_dEZVD2": (85, Unknown), + "ID_Visi_Bst_BStdZWE1": (86, Unknown), + "ID_Visi_Bst_BStdZWE2": (87, Unknown), + "ID_Visi_Bst_BStdZWE3": (88, Unknown), + "ID_Visi_Bst_BStdWP": (89, Unknown), + "ID_Visi_Text_Kurzprogramme": (90, Unknown), + "ID_Visi_Text_Zwangsheizung": (91, Unknown), + "ID_Visi_Text_Zwangsbrauchwasser": (92, Unknown), + "ID_Visi_Text_Abtauen": (93, Unknown), + "ID_Visi_EinstTemp_RucklBegr": (94, Unknown), + "ID_Visi_EinstTemp_HystereseHR": (95, Unknown), + "ID_Visi_EinstTemp_TRErhmax": (96, Unknown), + "ID_Visi_EinstTemp_Freig2VD": (97, Unknown), + "ID_Visi_EinstTemp_FreigZWE": (98, Unknown), + "ID_Visi_EinstTemp_Tluftabt": (99, Unknown), + "ID_Visi_EinstTemp_TDISolltemp": (100, Unknown), + "ID_Visi_EinstTemp_HystereseBW": (101, Unknown), + "ID_Visi_EinstTemp_Vorl2VDBW": (102, Unknown), + "ID_Visi_EinstTemp_TAussenmax": (103, Unknown), + "ID_Visi_EinstTemp_TAussenmin": (104, Unknown), + "ID_Visi_EinstTemp_TWQmin": (105, Unknown), + "ID_Visi_EinstTemp_THGmax": (106, Unknown), + "ID_Visi_EinstTemp_TLABTEnde": (107, Unknown), + "ID_Visi_EinstTemp_Absenkbis": (108, Unknown), + "ID_Visi_EinstTemp_Vorlaufmax": (109, Unknown), + "ID_Visi_EinstTemp_TDiffEin": (110, Unknown), + "ID_Visi_EinstTemp_TDiffAus": (111, Unknown), + "ID_Visi_EinstTemp_TDiffmax": (112, Unknown), + "ID_Visi_EinstTemp_TEEHeizung": (113, Unknown), + "ID_Visi_EinstTemp_TEEBrauchw": (114, Unknown), + "ID_Visi_EinstTemp_Vorl2VDSW": (115, Unknown), + "ID_Visi_EinstTemp_VLMaxMk1": (116, Unknown), + "ID_Visi_EinstTemp_VLMaxMk2": (117, Unknown), + "ID_Visi_Priori_Brauchwasser": (118, Unknown), + "ID_Visi_Priori_Heizung": (119, Unknown), + "ID_Visi_Priori_Schwimmbad": (120, Unknown), + "ID_Visi_SysEin_EVUSperre": (121, Unknown), + "ID_Visi_SysEin_Raumstation": (122, Unknown), + "ID_Visi_SysEin_Einbindung": (123, Unknown), + "ID_Visi_SysEin_Mischkreis1": (124, Unknown), + "ID_Visi_SysEin_Mischkreis2": (125, Unknown), + "ID_Visi_SysEin_ZWE1Art": (126, Unknown), + "ID_Visi_SysEin_ZWE1Fkt": (127, Unknown), + "ID_Visi_SysEin_ZWE2Art": (128, Unknown), + "ID_Visi_SysEin_ZWE2Fkt": (129, Unknown), + "ID_Visi_SysEin_ZWE3Art": (130, Unknown), + "ID_Visi_SysEin_ZWE3Fkt": (131, Unknown), + "ID_Visi_SysEin_Stoerung": (132, Unknown), + "ID_Visi_SysEin_Brauchwasser1": (133, Unknown), + "ID_Visi_SysEin_Brauchwasser2": (134, Unknown), + "ID_Visi_SysEin_Brauchwasser3": (135, Unknown), + "ID_Visi_SysEin_Brauchwasser4": (136, Unknown), + "ID_Visi_SysEin_Brauchwasser5": (137, Unknown), + "ID_Visi_SysEin_BWWPmax": (138, Unknown), + "ID_Visi_SysEin_Abtzykmax": (139, Unknown), + "ID_Visi_SysEin_Luftabt": (140, Unknown), + "ID_Visi_SysEin_LuftAbtmax": (141, Unknown), + "ID_Visi_SysEin_Abtauen1": (142, Unknown), + "ID_Visi_SysEin_Abtauen2": (143, Unknown), + "ID_Visi_SysEin_Pumpenoptim": (144, Unknown), + "ID_Visi_SysEin_Zusatzpumpe": (145, Unknown), + "ID_Visi_SysEin_Zugang": (146, Unknown), + "ID_Visi_SysEin_SoledrDurchf": (147, Unknown), + "ID_Visi_SysEin_UberwachungVD": (148, Unknown), + "ID_Visi_SysEin_RegelungHK": (149, Unknown), + "ID_Visi_SysEin_RegelungMK1": (150, Unknown), + "ID_Visi_SysEin_RegelungMK2": (151, Unknown), + "ID_Visi_SysEin_Kuhlung": (152, Unknown), + "ID_Visi_SysEin_Ausheizen": (153, Unknown), + "ID_Visi_SysEin_ElektrAnode": (154, Unknown), + "ID_Visi_SysEin_SWBBer": (155, Unknown), + "ID_Visi_SysEin_SWBMin": (156, Unknown), + "ID_Visi_SysEin_Heizung": (157, Unknown), + "ID_Visi_SysEin_PeriodeMk1": (158, Unknown), + "ID_Visi_SysEin_LaufzeitMk1": (159, Unknown), + "ID_Visi_SysEin_PeriodeMk2": (160, Unknown), + "ID_Visi_SysEin_LaufzeitMk2": (161, Unknown), + "ID_Visi_SysEin_Heizgrenze": (162, Unknown), + "ID_Visi_Enlt_HUP": (163, Unknown), + "ID_Visi_Enlt_ZUP": (164, Unknown), + "ID_Visi_Enlt_BUP": (165, Unknown), + "ID_Visi_Enlt_Ventilator_BOSUP": (166, Unknown), + "ID_Visi_Enlt_MA1": (167, Unknown), + "ID_Visi_Enlt_MZ1": (168, Unknown), + "ID_Visi_Enlt_ZIP": (169, Unknown), + "ID_Visi_Enlt_MA2": (170, Unknown), + "ID_Visi_Enlt_MZ2": (171, Unknown), + "ID_Visi_Enlt_SUP": (172, Unknown), + "ID_Visi_Enlt_SLP": (173, Unknown), + "ID_Visi_Enlt_FP2": (174, Unknown), + "ID_Visi_Enlt_Laufzeit": (175, Unknown), + "ID_Visi_Anlgkonf_Heizung": (176, Unknown), + "ID_Visi_Anlgkonf_Brauchwarmwasser": (177, Unknown), + "ID_Visi_Anlgkonf_Schwimmbad": (178, Unknown), + "ID_Visi_Heizung_Betriebsart": (179, Unknown), + "ID_Visi_Heizung_TemperaturPlusMinus": (180, Unknown), + "ID_Visi_Heizung_Heizkurven": (181, Unknown), + "ID_Visi_Heizung_Zeitschlaltprogramm": (182, Unknown), + "ID_Visi_Heizung_Heizgrenze": (183, Unknown), + "ID_Visi_Mitteltemperatur": (184, Unknown), + "ID_Visi_Dataenlogger": (185, Unknown), + "ID_Visi_Sprachen_DEUTSCH": (186, Unknown), + "ID_Visi_Sprachen_ENGLISH": (187, Unknown), + "ID_Visi_Sprachen_FRANCAIS": (188, Unknown), + "ID_Visi_Sprachen_NORWAY": (189, Unknown), + "ID_Visi_Sprachen_TCHECH": (190, Unknown), + "ID_Visi_Sprachen_ITALIANO": (191, Unknown), + "ID_Visi_Sprachen_NEDERLANDS": (192, Unknown), + "ID_Visi_Sprachen_SVENSKA": (193, Unknown), + "ID_Visi_Sprachen_POLSKI": (194, Unknown), + "ID_Visi_Sprachen_MAGYARUL": (195, Unknown), + "ID_Visi_ErrorUSBspeichern": (196, Unknown), + "ID_Visi_Bst_BStdHz": (197, Unknown), + "ID_Visi_Bst_BStdBW": (198, Unknown), + "ID_Visi_Bst_BStdKue": (199, Unknown), + "ID_Visi_Service_Systemsteuerung": (200, Unknown), + "ID_Visi_Service_Systemsteuerung_Contrast": (201, Unknown), + "ID_Visi_Service_Systemsteuerung_Webserver": (202, Unknown), + "ID_Visi_Service_Systemsteuerung_IPAdresse": (203, Unknown), + "ID_Visi_Service_Systemsteuerung_Fernwartung": (204, Unknown), + "ID_Visi_Paralleleschaltung": (205, Unknown), + "ID_Visi_SysEin_Paralleleschaltung": (206, Unknown), + "ID_Visi_Sprachen_DANSK": (207, Unknown), + "ID_Visi_Sprachen_PORTUGES": (208, Unknown), + "ID_Visi_Heizkurve_Heizung": (209, Unknown), + "ID_Visi_SysEin_Mischkreis3": (210, Unknown), + "ID_Visi_MK3": (211, Unknown), + "ID_Visi_Temp_MK3_Vorlauf": (212, Unknown), + "ID_Visi_Temp_MK3VL_Soll": (213, Unknown), + "ID_Visi_OUT_Mischer3Auf": (214, Unknown), + "ID_Visi_OUT_Mischer3Zu": (215, Unknown), + "ID_Visi_SysEin_RegelungMK3": (216, Unknown), + "ID_Visi_SysEin_PeriodeMk3": (217, Unknown), + "ID_Visi_SysEin_LaufzeitMk3": (218, Unknown), + "ID_Visi_SysEin_Kuhl_Zeit_Ein": (219, Unknown), + "ID_Visi_SysEin_Kuhl_Zeit_Aus": (220, Unknown), + "ID_Visi_AblaufZ_AbtauIn": (221, Unknown), + "ID_Visi_Waermemenge_WS": (222, Unknown), + "ID_Visi_Waermemenge_WQ": (223, Unknown), + "ID_Visi_Enlt_MA3": (224, Unknown), + "ID_Visi_Enlt_MZ3": (225, Unknown), + "ID_Visi_Enlt_FP3": (226, Unknown), + "ID_Visi_OUT_FUP3": (227, Unknown), + "ID_Visi_Temp_Raumstation2": (228, Unknown), + "ID_Visi_Temp_Raumstation3": (229, Unknown), + "ID_Visi_Bst_BStdSW": (230, Unknown), + "ID_Visi_Sprachen_LITAUISCH": (231, Unknown), + "ID_Visi_Sprachen_ESTNICH": (232, Unknown), + "ID_Visi_SysEin_Fernwartung": (233, Unknown), + "ID_Visi_Sprachen_SLOVENISCH": (234, Unknown), + "ID_Visi_EinstTemp_TA_EG": (235, Unknown), + "ID_Visi_Einst_TVLmax_EG": (236, Unknown), + "ID_Visi_SysEin_PoptNachlauf": (237, Unknown), + "ID_Visi_RFV_K_Kuehlin": (238, Unknown), + "ID_Visi_SysEin_EffizienzpumpeNom": (239, Unknown), + "ID_Visi_SysEin_EffizienzpumpeMin": (240, Unknown), + "ID_Visi_SysEin_Effizienzpumpe": (241, Unknown), + "ID_Visi_SysEin_Waermemenge": (242, Unknown), + "ID_Visi_Service_WMZ_Effizienz": (243, Unknown), + "ID_Visi_SysEin_Wm_Versorgung_Korrektur": (244, Unknown), + "ID_Visi_SysEin_Wm_Auswertung_Korrektur": (245, Unknown), + "ID_Visi_IN_AnalogIn": (246, Unknown), + "ID_Visi_Eins_SN_Eingabe": (247, Unknown), + "ID_Visi_OUT_Analog_1": (248, Unknown), + "ID_Visi_OUT_Analog_2": (249, Unknown), + "ID_Visi_Solar": (250, Unknown), + "ID_Visi_SysEin_Solar": (251, Unknown), + "ID_Visi_EinstTemp_TDiffKollmax": (252, Unknown), + "ID_Visi_AblaufZ_HG_Sperre": (253, Unknown), + "ID_Visi_SysEin_Akt_Kuehlung": (254, Unknown), + "ID_Visi_SysEin_Vorlauf_VBO": (255, Unknown), + "ID_Visi_Einst_KRHyst": (256, Unknown), + "ID_Visi_Einst_Akt_Kuehl_Speicher_min": (257, Unknown), + "ID_Visi_Einst_Akt_Kuehl_Freig_WQE": (258, Unknown), + "ID_Visi_SysEin_AbtZykMin": (259, Unknown), + "ID_Visi_SysEin_VD2_Zeit_Min": (260, Unknown), + "ID_Visi_EinstTemp_Hysterese_HR_verkuerzt": (261, Unknown), + "ID_Visi_Einst_Luf_Feuchteschutz_akt": (262, Unknown), + "ID_Visi_Einst_Luf_Reduziert_akt": (263, Unknown), + "ID_Visi_Einst_Luf_Nennlueftung_akt": (264, Unknown), + "ID_Visi_Einst_Luf_Intensivlueftung_akt": (265, Unknown), + "ID_Visi_Temperatur_Lueftung_Zuluft": (266, Unknown), + "ID_Visi_Temperatur_Lueftung_Abluft": (267, Unknown), + "ID_Visi_OUT_Analog_3": (268, Unknown), + "ID_Visi_OUT_Analog_4": (269, Unknown), + "ID_Visi_IN_Analog_2": (270, Unknown), + "ID_Visi_IN_Analog_3": (271, Unknown), + "ID_Visi_IN_SAX": (272, Unknown), + "ID_Visi_OUT_VZU": (273, Unknown), + "ID_Visi_OUT_VAB": (274, Unknown), + "ID_Visi_OUT_VSK": (275, Unknown), + "ID_Visi_OUT_FRH": (276, Unknown), + "ID_Visi_KuhlTemp_SolltempMK3": (277, Unknown), + "ID_Visi_KuhlTemp_ATDiffMK3": (278, Unknown), + "ID_Visi_IN_SPL": (279, Unknown), + "ID_Visi_SysEin_Lueftungsstufen": (280, Unknown), + "ID_Visi_SysEin_Meldung_TDI": (281, Unknown), + "ID_Visi_SysEin_Typ_WZW": (282, Unknown), + "ID_Visi_BACnet": (283, Unknown), + "ID_Visi_Sprachen_SLOWAKISCH": (284, Unknown), + "ID_Visi_Sprachen_LETTISCH": (285, Unknown), + "ID_Visi_Sprachen_FINNISCH": (286, Unknown), + "ID_Visi_Kalibrierung_LWD": (287, Unknown), + "ID_Visi_IN_Durchfluss": (288, Unknown), + "ID_Visi_LIN_ANSAUG_VERDICHTER": (289, Unknown), + "ID_Visi_LIN_VDH": (290, Unknown), + "ID_Visi_LIN_UH": (291, Unknown), + "ID_Visi_LIN_Druck": (292, Unknown), + "ID_Visi_Einst_Sollwert_TRL_Kuehlen": (293, Unknown), + "ID_Visi_Entl_ExVentil": (294, Unknown), + "ID_Visi_Einst_Medium_Waermequelle": (295, Unknown), + "ID_Visi_Einst_Multispeicher": (296, Unknown), + "ID_Visi_Einst_Minimale_Ruecklaufsolltemperatur": (297, Unknown), + "ID_Visi_Einst_PKuehlTime": (298, Unknown), + "ID_Visi_Sprachen_TUERKISCH": (299, Unknown), + "ID_Visi_RBE": (300, Unknown), + "ID_Visi_Einst_Luf_Stufen_Faktor": (301, Unknown), + "ID_Visi_Freigabe_Zeit_ZWE": (302, Unknown), + "ID_Visi_Einst_min_VL_Kuehl": (303, Unknown), + "ID_Visi_ZWE1": (304, Unknown), + "ID_Visi_ZWE2": (305, Unknown), + "ID_Visi_ZWE3": (306, Unknown), + "ID_Visi_SEC": (307, Unknown), + "ID_Visi_HZIO": (308, Unknown), + "ID_Visi_WPIO": (309, Unknown), + "ID_Visi_LIN_ANSAUG_VERDAMPFER": (310, Unknown), + "ID_Visi_LIN_MULTI1": (311, Unknown), + "ID_Visi_LIN_MULTI2": (312, Unknown), + "ID_Visi_Einst_Leistung_ZWE": (313, Unknown), + "ID_Visi_Sprachen_ESPANOL": (314, Unknown), + "ID_Visi_Temp_BW_oben": (315, Unknown), + "ID_Visi_MAXIO": (316, Unknown), + "ID_Visi_OUT_Abtauwunsch": (317, Unknown), + "ID_Visi_SmartGrid": (318, Unknown), + "ID_Visi_Drehzahlgeregelt": (319, Unknown), + "ID_Visi_P155_Inverter": (320, Unknown), + "ID_Visi_Leistungsfreigabe": (321, Unknown), + "ID_Visi_Einst_Vorl_akt_Kuehl": (322, Unknown), + "ID_Visi_Einst_Abtauen_im_Warmwasser": (323, Unknown), + "ID_Visi_Waermemenge_ZWE": (324, Unknown), + "Unknown_Visibility_325": (325, Unknown), + "Unknown_Visibility_326": (326, Unknown), + "Unknown_Visibility_327": (327, Unknown), + "Unknown_Visibility_328": (328, Unknown), + "Unknown_Visibility_329": (329, Unknown), + "Unknown_Visibility_330": (330, Unknown), + "Unknown_Visibility_331": (331, Unknown), + "Unknown_Visibility_332": (332, Unknown), + "Unknown_Visibility_333": (333, Unknown), + "Unknown_Visibility_334": (334, Unknown), + "Unknown_Visibility_335": (335, Unknown), + "Unknown_Visibility_336": (336, Unknown), + "Unknown_Visibility_337": (337, Unknown), + "Unknown_Visibility_338": (338, Unknown), + "Unknown_Visibility_339": (339, Unknown), + "Unknown_Visibility_340": (340, Unknown), + "Unknown_Visibility_341": (341, Unknown), + "Unknown_Visibility_342": (342, Unknown), + "Unknown_Visibility_343": (343, Unknown), + "Unknown_Visibility_344": (344, Unknown), + "Unknown_Visibility_345": (345, Unknown), + "Unknown_Visibility_346": (346, Unknown), + "Unknown_Visibility_347": (347, Unknown), + "Unknown_Visibility_348": (348, Unknown), + "Unknown_Visibility_349": (349, Unknown), + "Unknown_Visibility_350": (350, Unknown), + "Unknown_Visibility_351": (351, Unknown), + "Unknown_Visibility_352": (352, Unknown), + "Unknown_Visibility_353": (353, Unknown), + "Unknown_Visibility_354": (354, Unknown), # New in 'main' branch: "ID_Visi_Heizung_Zeitschaltprogramm": 182, "Unknown_Visibility_355": 355, From b06e8a7c42bf76f189870799898a22eb0556bc2a Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sat, 24 Jan 2026 21:02:46 +0100 Subject: [PATCH 26/61] wip --- luxtronik/cfi/calculations.py | 6 +- luxtronik/datatypes.py | 2 + luxtronik/definitions/calculations.py | 71 +++- luxtronik/definitions/parameters.py | 158 ++++++-- luxtronik/definitions/visibilities.py | 2 +- tests/cfi/test_cfi_interface.py | 21 +- tests/test_compatibility.py | 560 ++++++++++++++++++-------- 7 files changed, 578 insertions(+), 242 deletions(-) diff --git a/luxtronik/cfi/calculations.py b/luxtronik/cfi/calculations.py index f793869b..5c2868af 100644 --- a/luxtronik/cfi/calculations.py +++ b/luxtronik/cfi/calculations.py @@ -30,13 +30,9 @@ class Calculations(DataVectorConfig): name = CALCULATIONS_FIELD_NAME definitions = CALCULATIONS_DEFINITIONS - _obsolete = { - "ID_WEB_SoftStand": "get_firmware_version()" - } - 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.""" diff --git a/luxtronik/datatypes.py b/luxtronik/datatypes.py index d060eea7..a2862298 100755 --- a/luxtronik/datatypes.py +++ b/luxtronik/datatypes.py @@ -621,6 +621,8 @@ class Version(Base): datatype_class = "version" + concatenate_multiple_data_chunks = False + def from_heatpump(self, value): return "".join([chr(c) for c in value]).strip("\x00") diff --git a/luxtronik/definitions/calculations.py b/luxtronik/definitions/calculations.py index 0530ab85..fcc7bd24 100644 --- a/luxtronik/definitions/calculations.py +++ b/luxtronik/definitions/calculations.py @@ -41,6 +41,7 @@ Timestamp, Unknown, MajorMinorVersion, + Version, Voltage, ) @@ -860,6 +861,16 @@ "unit": 'enum', "description": '', }, + { + "index": 81, + "count": 10, + "names": ['ID_WEB_SoftStand'], + "type": Version, + "writeable": False, + "datatype": 'UINT32', + "unit": '', + "description": '', + }, { "index": 81, "count": 1, @@ -2370,20 +2381,28 @@ "unit": 'Hz', "description": '', }, + { + "index": 232, + "names": ['Unknown_Calculation_232'], + }, { "index": 232, "count": 1, - "names": ['Vapourisation_Temperature', 'Unknown_Calculation_232'], + "names": ['Vapourisation_Temperature'], "type": Celsius, "writeable": False, "datatype": 'INT32', "unit": '°C/10', "description": '', }, + { + "index": 233, + "names": ['Unknown_Calculation_233'], + }, { "index": 233, "count": 1, - "names": ['Liquefaction_Temperature', 'Unknown_Calculation_233'], + "names": ['Liquefaction_Temperature'], "type": Celsius, "writeable": False, "datatype": 'INT32', @@ -2410,50 +2429,70 @@ "unit": '', "description": '', }, + { + "index": 236, + "names": ['Unknown_Calculation_236'], + }, { "index": 236, "count": 1, - "names": ['ID_WEB_Freq_VD_Soll', 'Unknown_Calculation_236'], + "names": ['ID_WEB_Freq_VD_Soll'], "type": Frequency, "writeable": False, "datatype": 'UINT32', "unit": 'Hz', "description": '', }, + { + "index": 237, + "names": ['Unknown_Calculation_237'], + }, { "index": 237, "count": 1, - "names": ['ID_WEB_Freq_VD_Min', 'Unknown_Calculation_237'], + "names": ['ID_WEB_Freq_VD_Min'], "type": Frequency, "writeable": False, "datatype": 'UINT32', "unit": 'Hz', "description": '', }, + { + "index": 238, + "names": ['Unknown_Calculation_238'], + }, { "index": 238, "count": 1, - "names": ['ID_WEB_Freq_VD_Max', 'Unknown_Calculation_238'], + "names": ['ID_WEB_Freq_VD_Max'], "type": Frequency, "writeable": False, "datatype": 'UINT32', "unit": 'Hz', "description": '', }, + { + "index": 239, + "names": ['Unknown_Calculation_239'], + }, { "index": 239, "count": 1, - "names": ['VBO_Temp_Spread_Soll', 'Unknown_Calculation_239'], + "names": ['VBO_Temp_Spread_Soll'], "type": Kelvin, "writeable": False, "datatype": 'INT32', "unit": 'K/10', "description": '', }, + { + "index": 240, + "names": ['Unknown_Calculation_240'], + }, { "index": 240, "count": 1, - "names": ['VBO_Temp_Spread_Ist', 'Unknown_Calculation_240'], + "names": ['VBO_Temp_Spread_Ist'], "type": Kelvin, "writeable": False, "datatype": 'INT32', @@ -2470,20 +2509,28 @@ "unit": '%', "description": '', }, + { + "index": 242, + "names": ['Unknown_Calculation_242'], + }, { "index": 242, "count": 1, - "names": ['HUP_Temp_Spread_Soll', 'Unknown_Calculation_242'], + "names": ['HUP_Temp_Spread_Soll'], "type": Kelvin, "writeable": False, "datatype": 'INT32', "unit": 'K/10', "description": '', }, + { + "index": 243, + "names": ['Unknown_Calculation_243'], + }, { "index": 243, "count": 1, - "names": ['HUP_Temp_Spread_Ist', 'Unknown_Calculation_243'], + "names": ['HUP_Temp_Spread_Ist'], "type": Kelvin, "writeable": False, "datatype": 'INT32', @@ -2630,10 +2677,14 @@ "unit": 'W', "description": '', }, + { + "index": 258, + "names": ['Unknown_Calculation_258'], + }, { "index": 258, "count": 1, - "names": ['RBE_Version', 'Unknown_Calculation_258'], + "names": ['RBE_Version'], "type": MajorMinorVersion, "writeable": False, "datatype": 'UINT32', diff --git a/luxtronik/definitions/parameters.py b/luxtronik/definitions/parameters.py index d5b5040e..49a3f2fd 100644 --- a/luxtronik/definitions/parameters.py +++ b/luxtronik/definitions/parameters.py @@ -10914,10 +10914,14 @@ "unit": '', "description": '', }, + { + "index": 1087, + "names": ['Unknown_Parameter_1087'], + }, { "index": 1087, "count": 1, - "names": ['SILENT_MODE', 'Unknown_Parameter_1087'], + "names": ['SILENT_MODE'], "type": OnOffMode, "writeable": False, "datatype": 'UINT32', @@ -10964,220 +10968,308 @@ "unit": '', "description": '', }, + { + "index": 1092, + "names": ['Unknown_Parameter_1092'], + }, { "index": 1092, "count": 1, - "names": ['ID_Einst_SuSilence', 'Unknown_Parameter_1092'], + "names": ['ID_Einst_SuSilence'], "type": TimerProgram, "writeable": False, "datatype": 'UINT32', "unit": 'enum', "description": '', }, + { + "index": 1093, + "names": ['Unknown_Parameter_1093'], + }, { "index": 1093, "count": 1, - "names": ['ID_Einst_SilenceTimer_0', 'Unknown_Parameter_1093'], + "names": ['ID_Einst_SilenceTimer_0'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1094, + "names": ['Unknown_Parameter_1094'], + }, { "index": 1094, "count": 1, - "names": ['ID_Einst_SilenceTimer_1', 'Unknown_Parameter_1094'], + "names": ['ID_Einst_SilenceTimer_1'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1095, + "names": ['Unknown_Parameter_1095'], + }, { "index": 1095, "count": 1, - "names": ['ID_Einst_SilenceTimer_2', 'Unknown_Parameter_1095'], + "names": ['ID_Einst_SilenceTimer_2'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1096, + "names": ['Unknown_Parameter_1096'], + }, { "index": 1096, "count": 1, - "names": ['ID_Einst_SilenceTimer_3', 'Unknown_Parameter_1096'], + "names": ['ID_Einst_SilenceTimer_3'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1097, + "names": ['Unknown_Parameter_1097'], + }, { "index": 1097, "count": 1, - "names": ['ID_Einst_SilenceTimer_4', 'Unknown_Parameter_1097'], + "names": ['ID_Einst_SilenceTimer_4'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1098, + "names": ['Unknown_Parameter_1098'], + }, { "index": 1098, "count": 1, - "names": ['ID_Einst_SilenceTimer_5', 'Unknown_Parameter_1098'], + "names": ['ID_Einst_SilenceTimer_5'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1099, + "names": ['Unknown_Parameter_1099'], + }, { "index": 1099, "count": 1, - "names": ['ID_Einst_SilenceTimer_6', 'Unknown_Parameter_1099'], + "names": ['ID_Einst_SilenceTimer_6'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1100, + "names": ['Unknown_Parameter_1100'], + }, { "index": 1100, "count": 1, - "names": ['ID_Einst_SilenceTimer_7', 'Unknown_Parameter_1100'], + "names": ['ID_Einst_SilenceTimer_7'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1101, + "names": ['Unknown_Parameter_1101'], + }, { "index": 1101, "count": 1, - "names": ['ID_Einst_SilenceTimer_8', 'Unknown_Parameter_1101'], + "names": ['ID_Einst_SilenceTimer_8'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1102, + "names": ['Unknown_Parameter_1102'], + }, { "index": 1102, "count": 1, - "names": ['ID_Einst_SilenceTimer_9', 'Unknown_Parameter_1102'], + "names": ['ID_Einst_SilenceTimer_9'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1103, + "names": ['Unknown_Parameter_1103'], + }, { "index": 1103, "count": 1, - "names": ['ID_Einst_SilenceTimer_10', 'Unknown_Parameter_1103'], + "names": ['ID_Einst_SilenceTimer_10'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1104, + "names": ['Unknown_Parameter_1104'], + }, { "index": 1104, "count": 1, - "names": ['ID_Einst_SilenceTimer_11', 'Unknown_Parameter_1104'], + "names": ['ID_Einst_SilenceTimer_11'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1105, + "names": ['Unknown_Parameter_1105'], + }, { "index": 1105, "count": 1, - "names": ['ID_Einst_SilenceTimer_12', 'Unknown_Parameter_1105'], + "names": ['ID_Einst_SilenceTimer_12'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1106, + "names": ['Unknown_Parameter_1106'], + }, { "index": 1106, "count": 1, - "names": ['ID_Einst_SilenceTimer_13', 'Unknown_Parameter_1106'], + "names": ['ID_Einst_SilenceTimer_13'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1107, + "names": ['Unknown_Parameter_1107'], + }, { "index": 1107, "count": 1, - "names": ['ID_Einst_SilenceTimer_14', 'Unknown_Parameter_1107'], + "names": ['ID_Einst_SilenceTimer_14'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1108, + "names": ['Unknown_Parameter_1108'], + }, { "index": 1108, "count": 1, - "names": ['ID_Einst_SilenceTimer_15', 'Unknown_Parameter_1108'], + "names": ['ID_Einst_SilenceTimer_15'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1109, + "names": ['Unknown_Parameter_1109'], + }, { "index": 1109, "count": 1, - "names": ['ID_Einst_SilenceTimer_16', 'Unknown_Parameter_1109'], + "names": ['ID_Einst_SilenceTimer_16'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1110, + "names": ['Unknown_Parameter_1110'], + }, { "index": 1110, "count": 1, - "names": ['ID_Einst_SilenceTimer_17', 'Unknown_Parameter_1110'], + "names": ['ID_Einst_SilenceTimer_17'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1111, + "names": ['Unknown_Parameter_1111'], + }, { "index": 1111, "count": 1, - "names": ['ID_Einst_SilenceTimer_18', 'Unknown_Parameter_1111'], + "names": ['ID_Einst_SilenceTimer_18'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1112, + "names": ['Unknown_Parameter_1112'], + }, { "index": 1112, "count": 1, - "names": ['ID_Einst_SilenceTimer_19', 'Unknown_Parameter_1112'], + "names": ['ID_Einst_SilenceTimer_19'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', "unit": '', "description": '', }, + { + "index": 1113, + "names": ['Unknown_Parameter_1113'], + }, { "index": 1113, "count": 1, - "names": ['ID_Einst_SilenceTimer_20', 'Unknown_Parameter_1113'], + "names": ['ID_Einst_SilenceTimer_20'], "type": TimeOfDay2, "writeable": False, "datatype": 'UINT32', @@ -11234,10 +11326,14 @@ "unit": '', "description": '', }, + { + "index": 1119, + "names": ['Unknown_Parameter_1119'], + }, { "index": 1119, "count": 1, - "names": ['LAST_DEFROST_TIMESTAMP', 'Unknown_Parameter_1119'], + "names": ['LAST_DEFROST_TIMESTAMP'], "type": Timestamp, "writeable": False, "datatype": 'UINT32', @@ -11407,7 +11503,7 @@ { "index": 1136, "count": 1, - "names": ['HEAT_ENERGY_INPUT', 'Unknown_Parameter_1136'], + "names": ['HEAT_ENERGY_INPUT'], "type": Energy, "writeable": False, "datatype": 'UINT32', @@ -11417,7 +11513,7 @@ { "index": 1137, "count": 1, - "names": ['DHW_ENERGY_INPUT', 'Unknown_Parameter_1137'], + "names": ['DHW_ENERGY_INPUT'], "type": Energy, "writeable": False, "datatype": 'UINT32', @@ -11437,7 +11533,7 @@ { "index": 1139, "count": 1, - "names": ['COOLING_ENERGY_INPUT', 'Unknown_Parameter_1139'], + "names": ['COOLING_ENERGY_INPUT'], "type": Energy, "writeable": False, "datatype": 'UINT32', @@ -11447,7 +11543,7 @@ { "index": 1140, "count": 1, - "names": ['SECOND_HEAT_GENERATOR_AMOUNT_COUNTER', 'Unknown_Parameter_1140'], + "names": ['SECOND_HEAT_GENERATOR_AMOUNT_COUNTER'], "type": Unknown, "writeable": False, "datatype": 'UINT32', @@ -11527,7 +11623,7 @@ { "index": 1148, "count": 1, - "names": ['HEATING_TARGET_TEMP_ROOM_THERMOSTAT', 'Unknown_Parameter_1148'], + "names": ['HEATING_TARGET_TEMP_ROOM_THERMOSTAT'], "type": Celsius, "writeable": True, "datatype": 'INT32', @@ -11627,7 +11723,7 @@ { "index": 1158, "count": 1, - "names": ['POWER_LIMIT_SWITCH', 'Unknown_Parameter_1158'], + "names": ['POWER_LIMIT_SWITCH'], "type": Unknown, "writeable": False, "datatype": 'UINT32', @@ -11637,7 +11733,7 @@ { "index": 1159, "count": 1, - "names": ['POWER_LIMIT_VALUE', 'Unknown_Parameter_1159'], + "names": ['POWER_LIMIT_VALUE'], "type": Unknown, "writeable": False, "datatype": 'UINT32', diff --git a/luxtronik/definitions/visibilities.py b/luxtronik/definitions/visibilities.py index d8df6bbe..83596572 100644 --- a/luxtronik/definitions/visibilities.py +++ b/luxtronik/definitions/visibilities.py @@ -3594,7 +3594,7 @@ { "index": 357, "count": 1, - "names": ['ELECTRICAL_POWER_LIMITATION_SWITCH', 'Unknown_Visibility_357', 'Unknown_Parameter_357'], + "names": ['ELECTRICAL_POWER_LIMITATION_SWITCH', 'Unknown_Parameter_357'], "type": Unknown, "writeable": False, "datatype": 'UINT32', diff --git a/tests/cfi/test_cfi_interface.py b/tests/cfi/test_cfi_interface.py index 755b9e59..a46a2ad4 100644 --- a/tests/cfi/test_cfi_interface.py +++ b/tests/cfi/test_cfi_interface.py @@ -9,6 +9,7 @@ class TestLuxtronikSocketInterface: + def test_parse(self): lux = LuxtronikSocketInterface('host') parameters = Parameters() @@ -31,22 +32,4 @@ def test_parse(self): lux._parse(visibilities, t) v = visibilities.get(n) assert v.name == f"unknown_visibility_{n}" - assert v.raw == n - - n = 10 - t = list(range(0, n + 1)) - - lux._parse(parameters, t) - for definition, field in parameters.data.pairs(): - if definition.index > n: - assert field.raw is None - - lux._parse(calculations, t) - for definition, field in calculations.data.pairs(): - if definition.index > n: - assert field.raw is None - - lux._parse(visibilities, t) - for definition, field in visibilities.data.pairs(): - if definition.index > n: - assert field.raw is None \ No newline at end of file + assert v.raw == n \ No newline at end of file diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index 4829a295..c4b42e4f 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -2,11 +2,21 @@ # pylint: disable=too-few-public-methods,invalid-name,too-many-lines +import logging +logging.disable(logging.CRITICAL) + from luxtronik import ( Calculations, Parameters, Visibilities, + Inputs, + Holdings, ) +from luxtronik.datatypes import * + + +class Pulses(Base): + pass class TestCompatibility: """Test suite for compatibilities""" @@ -1143,69 +1153,64 @@ def test_compatibilities(self): "Unknown_Parameter_1124": (1124, Unknown), "Unknown_Parameter_1125": (1125, Unknown), # New in 'main' branch: - "SILENT_MODE": 1087, - "ID_Einst_SuSilence": 1092, - "ID_Einst_SilenceTimer_0": 1093, - "ID_Einst_SilenceTimer_1": 1094, - "ID_Einst_SilenceTimer_2": 1095, - "ID_Einst_SilenceTimer_3": 1096, - "ID_Einst_SilenceTimer_4": 1097, - "ID_Einst_SilenceTimer_5": 1098, - "ID_Einst_SilenceTimer_6": 1099, - "ID_Einst_SilenceTimer_7": 1100, - "ID_Einst_SilenceTimer_8": 1101, - "ID_Einst_SilenceTimer_9": 1102, - "ID_Einst_SilenceTimer_10": 1103, - "ID_Einst_SilenceTimer_11": 1104, - "ID_Einst_SilenceTimer_12": 1105, - "ID_Einst_SilenceTimer_13": 1106, - "ID_Einst_SilenceTimer_14": 1107, - "ID_Einst_SilenceTimer_15": 1108, - "ID_Einst_SilenceTimer_16": 1109, - "ID_Einst_SilenceTimer_17": 1110, - "ID_Einst_SilenceTimer_18": 1111, - "ID_Einst_SilenceTimer_19": 1112, - "ID_Einst_SilenceTimer_20": 1113, - "LAST_DEFROST_TIMESTAMP": 1119, - "Unknown_Parameter_1126": 1126, - "Unknown_Parameter_1127": 1127, - "Unknown_Parameter_1128": 1128, - "Unknown_Parameter_1129": 1129, - "Unknown_Parameter_1130": 1130, - "Unknown_Parameter_1131": 1131, - "Unknown_Parameter_1132": 1132, - "Unknown_Parameter_1133": 1133, - "Unknown_Parameter_1134": 1134, - "Unknown_Parameter_1135": 1135, - "Unknown_Parameter_1136": 1136, - "HEAT_ENERGY_INPUT": 1136, - "Unknown_Parameter_1137": 1137, - "DHW_ENERGY_INPUT": 1137, - "Unknown_Parameter_1138": 1138, - "Unknown_Parameter_1139": 1139, - "COOLING_ENERGY_INPUT": 1139, - "Unknown_Parameter_1140": 1140, - "SECOND_HEAT_GENERATOR_AMOUNT_COUNTER": 1140, - "Unknown_Parameter_1141": 1141, - "Unknown_Parameter_1142": 1142, - "Unknown_Parameter_1143": 1143, - "Unknown_Parameter_1144": 1144, - "Unknown_Parameter_1145": 1145, - "Unknown_Parameter_1146": 1146, - "Unknown_Parameter_1147": 1147, - "Unknown_Parameter_1148": 1148, - "HEATING_TARGET_TEMP_ROOM_THERMOSTAT": 1148, - "Unknown_Parameter_1149": 1149, - "Unknown_Parameter_1150": 1150, - "Unknown_Parameter_1151": 1151, - "Unknown_Parameter_1152": 1152, - "Unknown_Parameter_1153": 1153, - "Unknown_Parameter_1154": 1154, - "Unknown_Parameter_1155": 1155, - "Unknown_Parameter_1156": 1156, - "Unknown_Parameter_1157": 1157, - "POWER_LIMIT_SWITCH": 1158, - "POWER_LIMIT_VALUE": 1159, + "SILENT_MODE": (1087, OnOffMode), + "ID_Einst_SuSilence": (1092, TimerProgram), + "ID_Einst_SilenceTimer_0": (1093, TimeOfDay2), + "ID_Einst_SilenceTimer_1": (1094, TimeOfDay2), + "ID_Einst_SilenceTimer_2": (1095, TimeOfDay2), + "ID_Einst_SilenceTimer_3": (1096, TimeOfDay2), + "ID_Einst_SilenceTimer_4": (1097, TimeOfDay2), + "ID_Einst_SilenceTimer_5": (1098, TimeOfDay2), + "ID_Einst_SilenceTimer_6": (1099, TimeOfDay2), + "ID_Einst_SilenceTimer_7": (1100, TimeOfDay2), + "ID_Einst_SilenceTimer_8": (1101, TimeOfDay2), + "ID_Einst_SilenceTimer_9": (1102, TimeOfDay2), + "ID_Einst_SilenceTimer_10": (1103, TimeOfDay2), + "ID_Einst_SilenceTimer_11": (1104, TimeOfDay2), + "ID_Einst_SilenceTimer_12": (1105, TimeOfDay2), + "ID_Einst_SilenceTimer_13": (1106, TimeOfDay2), + "ID_Einst_SilenceTimer_14": (1107, TimeOfDay2), + "ID_Einst_SilenceTimer_15": (1108, TimeOfDay2), + "ID_Einst_SilenceTimer_16": (1109, TimeOfDay2), + "ID_Einst_SilenceTimer_17": (1110, TimeOfDay2), + "ID_Einst_SilenceTimer_18": (1111, TimeOfDay2), + "ID_Einst_SilenceTimer_19": (1112, TimeOfDay2), + "ID_Einst_SilenceTimer_20": (1113, TimeOfDay2), + "LAST_DEFROST_TIMESTAMP": (1119, Timestamp), + "Unknown_Parameter_1126": (1126, Unknown), + "Unknown_Parameter_1127": (1127, Unknown), + "Unknown_Parameter_1128": (1128, Unknown), + "Unknown_Parameter_1129": (1129, Unknown), + "Unknown_Parameter_1130": (1130, Unknown), + "Unknown_Parameter_1131": (1131, Unknown), + "Unknown_Parameter_1132": (1132, Unknown), + "Unknown_Parameter_1133": (1133, Unknown), + "Unknown_Parameter_1134": (1134, Unknown), + "Unknown_Parameter_1135": (1135, Unknown), + "HEAT_ENERGY_INPUT": (1136, Energy), + "DHW_ENERGY_INPUT": (1137, Energy), + "Unknown_Parameter_1138": (1138, Unknown), + "COOLING_ENERGY_INPUT": (1139, Energy), + "SECOND_HEAT_GENERATOR_AMOUNT_COUNTER": (1140, Unknown), + "Unknown_Parameter_1141": (1141, Unknown), + "Unknown_Parameter_1142": (1142, Unknown), + "Unknown_Parameter_1143": (1143, Unknown), + "Unknown_Parameter_1144": (1144, Unknown), + "Unknown_Parameter_1145": (1145, Unknown), + "Unknown_Parameter_1146": (1146, Unknown), + "Unknown_Parameter_1147": (1147, Unknown), + "HEATING_TARGET_TEMP_ROOM_THERMOSTAT": (1148, Celsius), + "Unknown_Parameter_1149": (1149, Unknown), + "Unknown_Parameter_1150": (1150, Unknown), + "Unknown_Parameter_1151": (1151, Unknown), + "Unknown_Parameter_1152": (1152, Unknown), + "Unknown_Parameter_1153": (1153, Unknown), + "Unknown_Parameter_1154": (1154, Unknown), + "Unknown_Parameter_1155": (1155, Unknown), + "Unknown_Parameter_1156": (1156, Unknown), + "Unknown_Parameter_1157": (1157, Unknown), + "POWER_LIMIT_SWITCH": (1158, Unknown), + "POWER_LIMIT_VALUE": (1159, Unknown), } calcs = { @@ -1462,38 +1467,37 @@ def test_compatibilities(self): "Unknown_Calculation_258": (258, Unknown), "Unknown_Calculation_259": (259, Unknown), # New in 'main' branch: - "ID_WEB_SoftStand_0": 81, - "ID_WEB_SoftStand_1": 82, - "ID_WEB_SoftStand_2": 83, - "ID_WEB_SoftStand_3": 84, - "ID_WEB_SoftStand_4": 85, - "ID_WEB_SoftStand_5": 86, - "ID_WEB_SoftStand_6": 87, - "ID_WEB_SoftStand_7": 88, - "ID_WEB_SoftStand_8": 89, - "ID_WEB_SoftStand_9": 90, - "Vapourisation_Temperature": 232, - "Liquefaction_Temperature": 233, - "ID_WEB_Freq_VD_Soll": 236, - "ID_WEB_Freq_VD_Min": 237, - "ID_WEB_Freq_VD_Max": 238, - "VBO_Temp_Spread_Soll": 239, - "VBO_Temp_Spread_Ist": 240, - "HUP_PWM": 241, - "HUP_Temp_Spread_Soll": 242, - "HUP_Temp_Spread_Ist": 243, - "RBE_Version": 258, - "Unknown_Calculation_260": 260, - "Unknown_Calculation_261": 261, - "Unknown_Calculation_262": 262, - "Unknown_Calculation_263": 263, - "Unknown_Calculation_264": 264, - "Unknown_Calculation_265": 265, - "Unknown_Calculation_266": 266, - "Desired_Room_Temperature": 267, - "AC_Power_Input": 268, + "ID_WEB_SoftStand_0": (81, Character), + "ID_WEB_SoftStand_1": (82, Character), + "ID_WEB_SoftStand_2": (83, Character), + "ID_WEB_SoftStand_3": (84, Character), + "ID_WEB_SoftStand_4": (85, Character), + "ID_WEB_SoftStand_5": (86, Character), + "ID_WEB_SoftStand_6": (87, Character), + "ID_WEB_SoftStand_7": (88, Character), + "ID_WEB_SoftStand_8": (89, Character), + "ID_WEB_SoftStand_9": (90, Character), + "Vapourisation_Temperature": (232, Celsius), + "Liquefaction_Temperature": (233, Celsius), + "ID_WEB_Freq_VD_Soll": (236, Frequency), + "ID_WEB_Freq_VD_Min": (237, Frequency), + "ID_WEB_Freq_VD_Max": (238, Frequency), + "VBO_Temp_Spread_Soll": (239, Kelvin), + "VBO_Temp_Spread_Ist": (240, Kelvin), + "HUP_PWM": (241, Percent2), + "HUP_Temp_Spread_Soll": (242, Kelvin), + "HUP_Temp_Spread_Ist": (243, Kelvin), + "RBE_Version": (258, MajorMinorVersion), + "Unknown_Calculation_260": (260, Unknown), + "Unknown_Calculation_261": (261, Unknown), + "Unknown_Calculation_262": (262, Unknown), + "Unknown_Calculation_263": (263, Unknown), + "Unknown_Calculation_264": (264, Unknown), + "Unknown_Calculation_265": (265, Unknown), + "Unknown_Calculation_266": (266, Unknown), + "Desired_Room_Temperature": (267, Celsius), + "AC_Power_Input": (268, Power), } - # Note: "ID_WEB_SoftStand" tested in "test_get_firmware_version()" visis = { # Status of 0.3.14: @@ -1852,95 +1856,299 @@ def test_compatibilities(self): "Unknown_Visibility_352": (352, Unknown), "Unknown_Visibility_353": (353, Unknown), "Unknown_Visibility_354": (354, Unknown), - # New in 'main' branch: - "ID_Visi_Heizung_Zeitschaltprogramm": 182, - "Unknown_Visibility_355": 355, - "Unknown_Visibility_356": 356, - "Unknown_Visibility_357": 357, - "ELECTRICAL_POWER_LIMITATION_SWITCH": 357, - "Unknown_Visibility_358": 358, - "Unknown_Visibility_359": 359, - "Unknown_Visibility_360": 360, - "Unknown_Visibility_361": 361, - "Unknown_Visibility_362": 362, - "Unknown_Visibility_363": 363, - "Unknown_Visibility_364": 364, - "Unknown_Visibility_365": 365, - "Unknown_Visibility_366": 366, - "Unknown_Visibility_367": 367, - "Unknown_Visibility_368": 368, - "Unknown_Visibility_369": 369, - "Unknown_Visibility_370": 370, - "Unknown_Visibility_371": 371, - "Unknown_Visibility_372": 372, - "Unknown_Visibility_373": 373, - "Unknown_Visibility_374": 374, - "Unknown_Visibility_375": 375, - "Unknown_Visibility_376": 376, - "Unknown_Visibility_377": 377, - "Unknown_Visibility_378": 378, - "Unknown_Visibility_379": 379, # Bug in v0.3.14 visibilities parse method - "Unknown_Parameter_355": 355, - "Unknown_Parameter_356": 356, - "Unknown_Parameter_357": 357, - "Unknown_Parameter_358": 358, - "Unknown_Parameter_359": 359, - "Unknown_Parameter_360": 360, - "Unknown_Parameter_361": 361, - "Unknown_Parameter_362": 362, - "Unknown_Parameter_363": 363, - "Unknown_Parameter_364": 364, - "Unknown_Parameter_365": 365, - "Unknown_Parameter_366": 366, - "Unknown_Parameter_367": 367, - "Unknown_Parameter_368": 368, - "Unknown_Parameter_369": 369, - "Unknown_Parameter_370": 370, - "Unknown_Parameter_371": 371, - "Unknown_Parameter_372": 372, - "Unknown_Parameter_373": 373, - "Unknown_Parameter_374": 374, - "Unknown_Parameter_375": 375, - "Unknown_Parameter_376": 376, - "Unknown_Parameter_377": 377, - "Unknown_Parameter_378": 378, - "Unknown_Parameter_379": 379, + "Unknown_Parameter_355": (355, Unknown), + "Unknown_Parameter_356": (356, Unknown), + "Unknown_Parameter_357": (357, Unknown), + "Unknown_Parameter_358": (358, Unknown), + "Unknown_Parameter_359": (359, Unknown), + "Unknown_Parameter_360": (360, Unknown), + "Unknown_Parameter_361": (361, Unknown), + "Unknown_Parameter_362": (362, Unknown), + "Unknown_Parameter_363": (363, Unknown), + "Unknown_Parameter_364": (364, Unknown), + "Unknown_Parameter_365": (365, Unknown), + "Unknown_Parameter_366": (366, Unknown), + "Unknown_Parameter_367": (367, Unknown), + "Unknown_Parameter_368": (368, Unknown), + "Unknown_Parameter_369": (369, Unknown), + "Unknown_Parameter_370": (370, Unknown), + "Unknown_Parameter_371": (371, Unknown), + "Unknown_Parameter_372": (372, Unknown), + "Unknown_Parameter_373": (373, Unknown), + "Unknown_Parameter_374": (374, Unknown), + "Unknown_Parameter_375": (375, Unknown), + "Unknown_Parameter_376": (376, Unknown), + "Unknown_Parameter_377": (377, Unknown), + "Unknown_Parameter_378": (378, Unknown), + "Unknown_Parameter_379": (379, Unknown), + # New in 'main' branch: + "ID_Visi_Heizung_Zeitschaltprogramm": (182, Unknown), + "Unknown_Visibility_355": (355, Unknown), + "Unknown_Visibility_356": (356, Unknown), + "ELECTRICAL_POWER_LIMITATION_SWITCH": (357, Unknown), + "Unknown_Visibility_358": (358, Unknown), + "Unknown_Visibility_359": (359, Unknown), + "Unknown_Visibility_360": (360, Unknown), + "Unknown_Visibility_361": (361, Unknown), + "Unknown_Visibility_362": (362, Unknown), + "Unknown_Visibility_363": (363, Unknown), + "Unknown_Visibility_364": (364, Unknown), + "Unknown_Visibility_365": (365, Unknown), + "Unknown_Visibility_366": (366, Unknown), + "Unknown_Visibility_367": (367, Unknown), + "Unknown_Visibility_368": (368, Unknown), + "Unknown_Visibility_369": (369, Unknown), + "Unknown_Visibility_370": (370, Unknown), + "Unknown_Visibility_371": (371, Unknown), + "Unknown_Visibility_372": (372, Unknown), + "Unknown_Visibility_373": (373, Unknown), + "Unknown_Visibility_374": (374, Unknown), + "Unknown_Visibility_375": (375, Unknown), + "Unknown_Visibility_376": (376, Unknown), + "Unknown_Visibility_377": (377, Unknown), + "Unknown_Visibility_378": (378, Unknown), + "Unknown_Visibility_379": (379, Unknown), } - values = [[paras, Parameters(), "paras"], [calcs, Calculations(), "calcs"], [visis, Visibilities(), "visis"]] + inputs = { + "heatpump_status": (0, HeatPumpStatus), + "operation_mode": (2, OperationMode), + "heating_status": (3, ModeStatus), + "hot_water_status": (4, ModeStatus), + "dhw_status": (4, ModeStatus), + "cooling_status": (6, ModeStatus), + "pool_heating_status": (7, ModeStatus), + "return_line_temp": (100, CelsiusUInt16), + "return_line_target": (101, CelsiusUInt16), + "return_line_ext": (102, CelsiusUInt16), + "return_line_limit": (103, CelsiusInt16), + "return_line_min_target": (104, CelsiusInt16), + "flow_line_temp": (105, CelsiusUInt16), + "room_temperature": (106, CelsiusInt16), + "heating_limit": (107, CelsiusInt16), + "outside_temp": (108, CelsiusInt16), + "outside_temp_average": (109, CelsiusInt16), + "heat_source_input": (110, CelsiusInt16), + "heat_source_output": (111, CelsiusInt16), + "max_flow_temp": (112, CelsiusUInt16), + "unknown_input_113": (113, Unknown), + "hot_water_temp": (120, CelsiusInt16), + "dhw_temp": (120, CelsiusInt16), + "hot_water_target": (121, CelsiusUInt16), + "dhw_target": (121, CelsiusUInt16), + "hot_water_min": (122, CelsiusInt16), + "dhw_min": (122, CelsiusInt16), + "hot_water_max": (123, CelsiusInt16), + "dhw_max": (123, CelsiusInt16), + "hot_water_limit": (124, CelsiusInt16), + "dhw_limit": (124, CelsiusInt16), + "mc1_temp": (140, CelsiusInt16), + "mc1_target": (141, CelsiusInt16), + "mc1_min": (142, CelsiusInt16), + "mc1_max": (143, CelsiusInt16), + "mc2_temp": (150, CelsiusInt16), + "mc2_target": (151, CelsiusInt16), + "mc2_min": (152, CelsiusInt16), + "mc2_max": (153, CelsiusInt16), + "mc3_temp": (160, CelsiusInt16), + "mc3_target": (161, CelsiusInt16), + "mc3_min": (162, CelsiusInt16), + "mc3_max": (163, CelsiusInt16), + "error_number": (201, Errorcode), + "buffer_type": (202, BufferType), + "min_off_time": (203, Minutes), + "min_run_time": (204, Minutes), + "cooling_configured": (205, OnOffMode), + "pool_heating_configured": (206, OnOffMode), + "cooling_release": (207, OnOffMode), + "heating_power_actual": (300, PowerKW), + "electric_power_actual": (301, PowerKW), + "electric_power_min_predicted": (302, PowerKW), + "electric_energy_total": (310, Energy), + "electric_energy_heating": (312, Energy), + "electric_energy_dhw": (314, Energy), + "electric_energy_cooling": (316, Energy), + "electric_energy_pool": (318, Energy), + "thermal_energy_total": (320, Energy), + "thermal_energy_heating": (322, Energy), + "thermal_energy_dhw": (324, Energy), + "thermal_energy_cooling": (326, Energy), + "thermal_energy_pool": (328, Energy), + "unknown_input_350": (350, Unknown), + "unknown_input_351": (351, Unknown), + "unknown_input_352": (352, Unknown), + "unknown_input_353": (353, Unknown), + "unknown_input_354": (354, Unknown), + "unknown_input_355": (355, Unknown), + "unknown_input_356": (356, Unknown), + "unknown_input_360": (360, Unknown), + "unknown_input_361": (361, Unknown), + "version": (400, FullVersion), + "unknown_input_404": (404, Unknown), + "unknown_input_405": (405, Unknown), + "unknown_input_406": (406, Unknown), + "unknown_input_407": (407, Unknown), + "unknown_input_408": (408, Unknown), + "unknown_input_409": (409, Unknown), + "unknown_input_410": (410, Unknown), + "unknown_input_411": (411, Unknown), + "unknown_input_412": (412, Unknown), + "unknown_input_413": (413, Unknown), + "unknown_input_416": (416, Unknown), + "unknown_input_417": (417, Unknown), + "unknown_input_500": (500, Unknown), + "unknown_input_501": (501, Unknown), + "unknown_input_502": (502, Unknown), + } + + holdings = { + # New in 'main' branch: + "heating_mode": (0, ControlMode), + "heating_setpoint": (1, CelsiusUInt16), + "heating_offset": (2, KelvinInt16), + "heating_level": (3, LevelMode), + "hot_water_mode": (5, ControlMode), + "dhw_mode": (5, ControlMode), + "hot_water_setpoint": (6, CelsiusUInt16), + "dhw_setpoint": (6, CelsiusUInt16), + "hot_water_offset": (7, KelvinInt16), + "dhw_offset": (7, KelvinInt16), + "hot_water_level": (8, LevelMode), + "dhw_level": (8, LevelMode), + "mc1_heat_mode": (10, ControlMode), + "mc1_heat_setpoint": (11, CelsiusUInt16), + "mc1_heat_offset": (12, KelvinInt16), + "mc1_heat_level": (13, LevelMode), + "mc1_cool_mode": (15, ControlMode), + "mc1_cool_setpoint": (16, CelsiusUInt16), + "mc1_cool_offset": (17, KelvinInt16), + "mc2_heat_mode": (20, ControlMode), + "mc2_heat_setpoint": (21, CelsiusUInt16), + "mc2_heat_offset": (22, KelvinInt16), + "mc2_heat_level": (23, LevelMode), + "mc2_cool_mode": (25, ControlMode), + "mc2_cool_setpoint": (26, CelsiusUInt16), + "mc2_cool_offset": (27, KelvinInt16), + "mc3_heat_mode": (30, ControlMode), + "mc3_heat_setpoint": (31, CelsiusUInt16), + "mc3_heat_offset": (32, KelvinInt16), + "mc3_heat_level": (33, LevelMode), + "mc3_cool_mode": (35, ControlMode), + "mc3_cool_setpoint": (36, CelsiusUInt16), + "mc3_cool_offset": (37, KelvinInt16), + "lpc_mode": (40, LpcMode), + "pc_limit": (41, PowerKW), + "lock_heating": (50, LockMode), + "lock_hot_water": (51, LockMode), + "lock_cooling": (52, LockMode), + "lock_swimming_pool": (53, LockMode), + "unknown_holding_60": (60, Unknown), + "heat_overall_mode": (65, ControlMode), + "heat_overall_offset": (66, KelvinInt16), + "heat_overall_mode": (65, ControlMode), + "heat_overall_offset": (66, KelvinInt16), + "heat_overall_level": (67, LevelMode), + "circulation": (70, OnOffMode), + "hot_water_extra": (71, OnOffMode), + } + + values = [ + [paras, Parameters(), "paras"], + [calcs, Calculations(), "calcs"], + [visis, Visibilities(), "visis"], + [inputs, Inputs(), "inputs"], + [holdings, Holdings(), "holdings"], + ] # First, we check if we can find all entries of the above dicts. - ok = True - for mapping, obj, caption in values: - print_caption = True - for old_name, old_idx in mapping.items(): - def_by_name = obj.definitions.get(old_name) - field_by_name = obj.get(old_name) - if (def_by_name is None) \ - or (field_by_name is None) \ - or (def_by_name.index != old_idx) \ - or (def_by_name.name != field_by_name.name) \ - or (old_name not in def_by_name.names): - # We do not use assert here, in order to catch all incompatibilities at once. - if print_caption: - print(f"### Incompatibilities - {caption}:") - print_caption = False - print(f'"{old_name}" is not registered for {old_idx}: "{def_by_name.name}",') - ok = False + all_ok = True + for mapping, data_vector, caption in values: + obsolete_found = [] + old_not_found = [] + old_idx_wrong = [] + old_type_changed = [] + + for old_name, (old_idx, old_type) in mapping.items(): + + # Try to get the definition of the "old name" + try: + def_by_name = data_vector.definitions.get(old_name) + except Exception as e: + def_by_name = None + old_found = def_by_name is not None + old_is_obsolete = old_name in data_vector._obsolete + + # Check names + # We do not use assert here, in order to catch all incompatibilities at once. + if old_found and old_is_obsolete: + obsolete_found.append(f"{old_name} marked as obsolete, but a definition was found!") + continue + + if not old_found and not old_is_obsolete: + old_not_found.append(f"No definition found for {old_name} with index {old_idx} and type {old_type.__name__}") + continue + + if old_found: + if old_idx != def_by_name.index: + old_idx_wrong.append(f"Index of {old_name} changed from {old_idx} to {def_by_name.index}") + continue + + #if old_name != def_by_name.name: + # new name available -> no error + + if old_type != def_by_name.field_type: + old_type_changed.append(f"Type of {old_name} changed from {old_type.__name__} to {def_by_name.field_type.__name__}") + + # Currently we allow type changes + ok = not obsolete_found and not old_not_found \ + and not old_idx_wrong # and not old_type_changed + do_print = not ok or len(old_type_changed) > 0 + + if do_print: + print(f"############################## Incompatibilities - {caption}:") + if obsolete_found: + print(f"############################## obsolete") + for err in obsolete_found: + print(err) + if old_not_found: + print(f"############################## not found") + for err in old_not_found: + print(err) + if old_idx_wrong: + print(f"############################## idx wrong") + for err in old_idx_wrong: + print(err) + if old_type_changed: + print(f"############################## type changed") + for err in old_type_changed: + print(err) + + all_ok &= ok assert ok, "Found incompatibilities. Please consider to add them to compatibilities.py" # Second, we check if all names are present in the above dicts. - ok = True - for mapping, obj, caption in values: - print_caption = True - for definition, field in obj.items(): - if field.name not in mapping: + all_ok = True + for mapping, data_vector, caption in values: + missing = [] + + for definition, field in data_vector.items(): + for name in definition.names: + + # Check existing entries # We do not use assert here, in order to catch all incompatibilities at once. # The output can be copied to the dicts above - if print_caption: - print(f"### Missing - {caption}:") - print_caption = False - print(f'"{field.name}": {definition.index},') - ok = False - assert ok, f"Found missing {obj.name}. Please consider to add them to the test suite." \ No newline at end of file + if name not in mapping: + missing.append(f'"{name}": ({definition.index}, {definition.field_type.__name__}),') + + ok = not missing + do_print = not ok + + if do_print: + print(f"############################## Missing - {caption}:") + if missing: + for err in missing: + print(err) + + all_ok &= ok + assert all_ok, f"Found missing entries. Please consider to add them to the test suite." \ No newline at end of file From 2ec5f563fe722ca939b155d7f66db995e14d2f81 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sat, 24 Jan 2026 21:04:17 +0100 Subject: [PATCH 27/61] wip --- luxtronik/datatypes.py | 26 -------------------------- tests/test_compatibility.py | 3 +++ 2 files changed, 3 insertions(+), 26 deletions(-) diff --git a/luxtronik/datatypes.py b/luxtronik/datatypes.py index a2862298..62150e6c 100755 --- a/luxtronik/datatypes.py +++ b/luxtronik/datatypes.py @@ -1,7 +1,6 @@ """datatype conversions.""" import datetime -import ipaddress import socket import struct @@ -328,31 +327,6 @@ class Seconds(Base): datatype_unit = "s" -class Pulses(Base): - """Pulses datatype, converts from and to Pulses.""" - - datatype_class = "pulses" - - -class IPAddress(Base): - """IP Address datatype, converts from and to an IP Address.""" - - datatype_class = "ipaddress" - - def from_heatpump(self, value): - if value < 0: - return str(ipaddress.IPv4Address(value + 2**32)) - if value > 2**32: - return str(ipaddress.IPv4Address(value - 2**32)) - return str(ipaddress.IPv4Address(value)) - - def to_heatpump(self, value): - result = int(ipaddress.IPv4Address(value)) - if result > 2**32: - return result - 2**32 - return result - - class IPv4Address(Base): """IPv4 address datatype, converts from and to an IPv4 address.""" diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index c4b42e4f..b23e9436 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -18,6 +18,9 @@ class Pulses(Base): pass +class IPAddress(Base): + pass + class TestCompatibility: """Test suite for compatibilities""" From 4fbb9594ba6ba312828823f57b673c6e7bbe3620 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sat, 24 Jan 2026 21:13:05 +0100 Subject: [PATCH 28/61] wip --- luxtronik/cfi/calculations.py | 2 ++ luxtronik/cfi/parameters.py | 2 ++ luxtronik/cfi/visibilities.py | 2 ++ luxtronik/definitions/calculations.py | 2 ++ luxtronik/definitions/holdings.py | 2 ++ luxtronik/definitions/inputs.py | 2 ++ luxtronik/definitions/parameters.py | 2 ++ luxtronik/definitions/visibilities.py | 2 ++ luxtronik/shi/holdings.py | 6 ++++-- luxtronik/shi/inputs.py | 4 +++- 10 files changed, 23 insertions(+), 3 deletions(-) diff --git a/luxtronik/cfi/calculations.py b/luxtronik/cfi/calculations.py index 5c2868af..986e7e8c 100644 --- a/luxtronik/cfi/calculations.py +++ b/luxtronik/cfi/calculations.py @@ -8,6 +8,7 @@ CALCULATIONS_DEFINITIONS_LIST, CALCULATIONS_OFFSET, CALCULATIONS_DEFAULT_DATA_TYPE, + CALCULATIONS_OUTDATED, ) from luxtronik.cfi.constants import CALCULATIONS_FIELD_NAME @@ -29,6 +30,7 @@ class Calculations(DataVectorConfig): name = CALCULATIONS_FIELD_NAME definitions = CALCULATIONS_DEFINITIONS + _outdated = CALCULATIONS_OUTDATED def get_firmware_version(self): """Get the firmware version as string.""" diff --git a/luxtronik/cfi/parameters.py b/luxtronik/cfi/parameters.py index 034c3fa8..f578b81c 100644 --- a/luxtronik/cfi/parameters.py +++ b/luxtronik/cfi/parameters.py @@ -8,6 +8,7 @@ PARAMETERS_DEFINITIONS_LIST, PARAMETERS_OFFSET, PARAMETERS_DEFAULT_DATA_TYPE, + PARAMETERS_OUTDATED, ) from luxtronik.cfi.constants import PARAMETERS_FIELD_NAME @@ -28,3 +29,4 @@ class Parameters(DataVectorConfig): name = PARAMETERS_FIELD_NAME definitions = PARAMETERS_DEFINITIONS + _outdated = PARAMETERS_OUTDATED diff --git a/luxtronik/cfi/visibilities.py b/luxtronik/cfi/visibilities.py index 0979001f..224f8f32 100644 --- a/luxtronik/cfi/visibilities.py +++ b/luxtronik/cfi/visibilities.py @@ -8,6 +8,7 @@ VISIBILITIES_DEFINITIONS_LIST, VISIBILITIES_OFFSET, VISIBILITIES_DEFAULT_DATA_TYPE, + VISIBILITIES_OUTDATED, ) from luxtronik.cfi.constants import VISIBILITIES_FIELD_NAME @@ -28,3 +29,4 @@ class Visibilities(DataVectorConfig): name = VISIBILITIES_FIELD_NAME definitions = VISIBILITIES_DEFINITIONS + _outdated = VISIBILITIES_OUTDATED diff --git a/luxtronik/definitions/calculations.py b/luxtronik/definitions/calculations.py index fcc7bd24..12e653ad 100644 --- a/luxtronik/definitions/calculations.py +++ b/luxtronik/definitions/calculations.py @@ -49,6 +49,8 @@ # to obtain the correct address of the data fields CALCULATIONS_OFFSET: Final = 0 CALCULATIONS_DEFAULT_DATA_TYPE: Final = 'INT32' +CALCULATIONS_OUTDATED = { +} CALCULATIONS_DEFINITIONS_LIST: Final = [ { diff --git a/luxtronik/definitions/holdings.py b/luxtronik/definitions/holdings.py index f7b7b67e..2f5b5110 100644 --- a/luxtronik/definitions/holdings.py +++ b/luxtronik/definitions/holdings.py @@ -30,6 +30,8 @@ # to obtain the correct address of the data fields HOLDINGS_OFFSET: Final = 10000 HOLDINGS_DEFAULT_DATA_TYPE: Final = 'INT16' +HOLDINGS_OUTDATED = { +} HOLDINGS_DEFINITIONS_LIST: Final = [ { diff --git a/luxtronik/definitions/inputs.py b/luxtronik/definitions/inputs.py index 7140e582..13ce5244 100644 --- a/luxtronik/definitions/inputs.py +++ b/luxtronik/definitions/inputs.py @@ -35,6 +35,8 @@ # to obtain the correct address of the data fields INPUTS_OFFSET: Final = 10000 INPUTS_DEFAULT_DATA_TYPE: Final = 'INT16' +INPUTS_OUTDATED = { +} INPUTS_DEFINITIONS_LIST: Final = [ { diff --git a/luxtronik/definitions/parameters.py b/luxtronik/definitions/parameters.py index 49a3f2fd..8271f05f 100644 --- a/luxtronik/definitions/parameters.py +++ b/luxtronik/definitions/parameters.py @@ -42,6 +42,8 @@ # to obtain the correct address of the data fields PARAMETERS_OFFSET: Final = 0 PARAMETERS_DEFAULT_DATA_TYPE: Final = 'INT32' +PARAMETERS_OUTDATED = { +} PARAMETERS_DEFINITIONS_LIST: Final = [ { diff --git a/luxtronik/definitions/visibilities.py b/luxtronik/definitions/visibilities.py index 83596572..a4a86451 100644 --- a/luxtronik/definitions/visibilities.py +++ b/luxtronik/definitions/visibilities.py @@ -19,6 +19,8 @@ # to obtain the correct address of the data fields VISIBILITIES_OFFSET: Final = 0 VISIBILITIES_DEFAULT_DATA_TYPE: Final = 'INT32' +VISIBILITIES_OUTDATED = { +} VISIBILITIES_DEFINITIONS_LIST: Final = [ { diff --git a/luxtronik/shi/holdings.py b/luxtronik/shi/holdings.py index cdc39054..1bd065eb 100644 --- a/luxtronik/shi/holdings.py +++ b/luxtronik/shi/holdings.py @@ -7,7 +7,8 @@ from luxtronik.definitions.holdings import ( HOLDINGS_DEFINITIONS_LIST, HOLDINGS_OFFSET, - HOLDINGS_DEFAULT_DATA_TYPE + HOLDINGS_DEFAULT_DATA_TYPE, + HOLDINGS_OUTDATED, ) from luxtronik.shi.constants import HOLDINGS_FIELD_NAME @@ -27,4 +28,5 @@ class Holdings(DataVectorSmartHome): """Class that holds holding fields.""" name = HOLDINGS_FIELD_NAME - definitions = HOLDINGS_DEFINITIONS \ No newline at end of file + definitions = HOLDINGS_DEFINITIONS + _outdated = HOLDINGS_OUTDATED \ No newline at end of file diff --git a/luxtronik/shi/inputs.py b/luxtronik/shi/inputs.py index d609e9c4..cf36a8ef 100644 --- a/luxtronik/shi/inputs.py +++ b/luxtronik/shi/inputs.py @@ -8,6 +8,7 @@ INPUTS_DEFINITIONS_LIST, INPUTS_OFFSET, INPUTS_DEFAULT_DATA_TYPE, + INPUTS_OUTDATED, ) from luxtronik.shi.constants import INPUTS_FIELD_NAME @@ -27,4 +28,5 @@ class Inputs(DataVectorSmartHome): """Class that holds input fields.""" name = INPUTS_FIELD_NAME - definitions = INPUTS_DEFINITIONS \ No newline at end of file + definitions = INPUTS_DEFINITIONS + _outdated = INPUTS_OUTDATED \ No newline at end of file From 8f58d435478a7457125eb759bc182efa6d2e4e5c Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sat, 24 Jan 2026 22:20:38 +0100 Subject: [PATCH 29/61] wip, multiple fields per register --- luxtronik/collections.py | 8 ++++++++ luxtronik/definitions/__init__.py | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/luxtronik/collections.py b/luxtronik/collections.py index 814411ad..8ead2a59 100644 --- a/luxtronik/collections.py +++ b/luxtronik/collections.py @@ -88,6 +88,10 @@ def get_data_arr(definition, field, num_bits): data = field.raw if data is None: return None + # Currently, no read-modify-write function is implemented. + # For this reason, we cannot write (and retrieve the data to write) + # from a field with a bit_offset. + # -> no additional code here like in `integrate_data` should_unpack = field.concatenate_multiple_data_chunks \ and definition.count > 1 if should_unpack and not isinstance(data, list): @@ -111,6 +115,7 @@ def integrate_data(definition, field, raw_data, num_bits, data_offset=-1): # Use data_offset if provided, otherwise the index data_offset = data_offset if data_offset >= 0 else definition.index # Use the information of the definition to extract the raw-value + use_bit_offset = definition.bit_offset and definition.num_bits if (data_offset + definition.count - 1) >= len(raw_data): raw = None elif definition.count == 1: @@ -124,6 +129,9 @@ def integrate_data(definition, field, raw_data, num_bits, data_offset=-1): raw = pack_values(raw, num_bits) raw = raw if definition.check_raw_not_none(raw) else None + # Perform bit shift operations + if use_bit_offset and isinstance(raw, int): + raw = (raw >> definition.bit_offset) & ((1 << definition.num_bits) - 1) field.raw = raw ############################################################################### diff --git a/luxtronik/definitions/__init__.py b/luxtronik/definitions/__init__.py index ed3b14c5..dddafa6a 100644 --- a/luxtronik/definitions/__init__.py +++ b/luxtronik/definitions/__init__.py @@ -39,6 +39,8 @@ class LuxtronikDefinition: "since": "", "until": "", "datatype": "", + "bit_offset": None, + "bit_count": None, "description": "", } @@ -91,8 +93,13 @@ def __init__(self, data_dict, type_name, offset): data_type_valid = self._data_type in self.VALID_DATA_TYPES self._valid &= data_type_valid data_type_valid &= self._data_type != "" - self._num_bits = int(self._data_type.replace('U', '').replace('INT', '')) \ - if data_type_valid else 0 + self._bit_offset = data_dict["bit_offset"] + bit_count = data_dict["bit_count"] + if bit_count: + self._num_bits = bit_count + else: + self._num_bits = int(self._data_type.replace('U', '').replace('INT', '')) \ + if data_type_valid else 0 except Exception as e: self._valid = False self._index = 0 @@ -160,6 +167,10 @@ def field_type(self): def writeable(self): return self._writeable + @property + def bit_offset(self): + return self._bit_offset + @property def num_bits(self): return self._num_bits From 767dbed62d53edf5fd29206b347ddfcb877191a7 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sat, 24 Jan 2026 22:33:14 +0100 Subject: [PATCH 30/61] wip --- tests/cfi/test_cfi_parameters.py | 1 - tests/test_compatibility.py | 68 +++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/tests/cfi/test_cfi_parameters.py b/tests/cfi/test_cfi_parameters.py index 3764af8c..6438ae15 100644 --- a/tests/cfi/test_cfi_parameters.py +++ b/tests/cfi/test_cfi_parameters.py @@ -5,7 +5,6 @@ from luxtronik import Parameters from luxtronik.datatypes import Base from luxtronik.definitions import LuxtronikDefinition -from luxtronik.cfi.constants import LUXTRONIK_CFI_REGISTER_BIT_SIZE class TestParameters: diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index b23e9436..fab20b21 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -12,7 +12,68 @@ Inputs, Holdings, ) -from luxtronik.datatypes import * + +from luxtronik.datatypes import ( + AccessLevel, + Base, + BivalenceLevel, + Bool, + BufferType, + Celsius, + CelsiusInt16, + CelsiusUInt16, + Character, + ControlMode, + CoolingMode, + Count, + Energy, + Errorcode, + Flow, + Frequency, + FullVersion, + HeatPumpStatus, + HeatingMode, + HeatpumpCode, + HotWaterMode, + Hours, + Hours2, + IPv4Address, + Icon, + Kelvin, + KelvinInt16, + Level, + LevelMode, + LockMode, + LpcMode, + MainMenuStatusLine1, + MainMenuStatusLine2, + MainMenuStatusLine3, + MajorMinorVersion, + Minutes, + MixedCircuitMode, + ModeStatus, + OnOffMode, + OperationMode, + Percent2, + PoolMode, + Power, + PowerKW, + Pressure, + SecOperationMode, + Seconds, + SolarMode, + Speed, + SwitchoffFile, + TimeOfDay, + TimeOfDay2, + TimerProgram, + Timestamp, + Unknown, + VentilationMode, + Version, + Voltage, +) + class Pulses(Base): @@ -1915,6 +1976,11 @@ def test_compatibilities(self): } inputs = { + "heatpump_vd1_status": (0, Bool), + "heatpump_vd2_status": (0, Bool), + "heatpump_zwe1_status": (0, Bool), + "heatpump_zwe2_status": (0, Bool), + "heatpump_zwe3_status": (0, Bool), "heatpump_status": (0, HeatPumpStatus), "operation_mode": (2, OperationMode), "heating_status": (3, ModeStatus), From 3b8b1d12626fd040581d130d6d73b1de7a8cc279 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sat, 24 Jan 2026 22:41:11 +0100 Subject: [PATCH 31/61] wip --- tests/test_compatibility.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index fab20b21..357dc23b 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -3,7 +3,6 @@ # pylint: disable=too-few-public-methods,invalid-name,too-many-lines import logging -logging.disable(logging.CRITICAL) from luxtronik import ( Calculations, @@ -14,7 +13,6 @@ ) from luxtronik.datatypes import ( - AccessLevel, Base, BivalenceLevel, Bool, @@ -36,8 +34,6 @@ HeatpumpCode, HotWaterMode, Hours, - Hours2, - IPv4Address, Icon, Kelvin, KelvinInt16, @@ -64,7 +60,6 @@ SolarMode, Speed, SwitchoffFile, - TimeOfDay, TimeOfDay2, TimerProgram, Timestamp, @@ -74,6 +69,7 @@ Voltage, ) +logging.disable(logging.CRITICAL) class Pulses(Base): @@ -2115,8 +2111,6 @@ def test_compatibilities(self): "unknown_holding_60": (60, Unknown), "heat_overall_mode": (65, ControlMode), "heat_overall_offset": (66, KelvinInt16), - "heat_overall_mode": (65, ControlMode), - "heat_overall_offset": (66, KelvinInt16), "heat_overall_level": (67, LevelMode), "circulation": (70, OnOffMode), "hot_water_extra": (71, OnOffMode), @@ -2143,7 +2137,7 @@ def test_compatibilities(self): # Try to get the definition of the "old name" try: def_by_name = data_vector.definitions.get(old_name) - except Exception as e: + except Exception: def_by_name = None old_found = def_by_name is not None old_is_obsolete = old_name in data_vector._obsolete @@ -2177,24 +2171,24 @@ def test_compatibilities(self): if do_print: print(f"############################## Incompatibilities - {caption}:") if obsolete_found: - print(f"############################## obsolete") + print("############################## obsolete") for err in obsolete_found: print(err) if old_not_found: - print(f"############################## not found") + print("############################## not found") for err in old_not_found: print(err) if old_idx_wrong: - print(f"############################## idx wrong") + print("############################## idx wrong") for err in old_idx_wrong: print(err) if old_type_changed: - print(f"############################## type changed") + print("############################## type changed") for err in old_type_changed: print(err) all_ok &= ok - assert ok, "Found incompatibilities. Please consider to add them to compatibilities.py" + assert all_ok, "Found incompatibilities. Please consider to add them to compatibilities.py" # Second, we check if all names are present in the above dicts. all_ok = True @@ -2220,4 +2214,4 @@ def test_compatibilities(self): print(err) all_ok &= ok - assert all_ok, f"Found missing entries. Please consider to add them to the test suite." \ No newline at end of file + assert all_ok, "Found missing entries. Please consider to add them to the test suite." \ No newline at end of file From 3c8fd2aca70136d480f80ad0e5328758148b42d9 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sat, 24 Jan 2026 22:52:21 +0100 Subject: [PATCH 32/61] wip --- luxtronik/datatypes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/luxtronik/datatypes.py b/luxtronik/datatypes.py index 62150e6c..2a7889ee 100755 --- a/luxtronik/datatypes.py +++ b/luxtronik/datatypes.py @@ -598,6 +598,8 @@ class Version(Base): concatenate_multiple_data_chunks = False def from_heatpump(self, value): + if not isinstance(value, list): + return None return "".join([chr(c) for c in value]).strip("\x00") From 447516e50dd71d411463ad13142bac08db705ec9 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sat, 24 Jan 2026 22:58:21 +0100 Subject: [PATCH 33/61] wip --- tests/shi/test_shi_vector.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/tests/shi/test_shi_vector.py b/tests/shi/test_shi_vector.py index 795870df..309e5ad8 100644 --- a/tests/shi/test_shi_vector.py +++ b/tests/shi/test_shi_vector.py @@ -319,33 +319,6 @@ def test_set(self): assert field_9.value == 6 assert field_9.write_pending - def test_parse(self): - data_vector = DataVectorTest(parse_version("1.1.2")) - field_5 = data_vector[5] - field_9 = data_vector[9] - field_9a = data_vector['field_9a'] - - # not enough data - data = [1] - data_vector.parse(data, LUXTRONIK_SHI_REGISTER_BIT_SIZE) - assert field_5.value is None - assert field_9.value is None - assert field_9a.value is None - - # data only for field 5 - data = [1, 2, 3, 4, 5, 6, 7] - data_vector.parse(data, LUXTRONIK_SHI_REGISTER_BIT_SIZE) - assert field_5.value == 6 - assert field_9.value is None - assert field_9a.value is None - - # data for all fields - data = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0, -1, -2] - data_vector.parse(data, LUXTRONIK_SHI_REGISTER_BIT_SIZE) - assert field_5.value == 4 - assert field_9.value == [0, -1] - assert field_9a.value == 0 - def test_alias(self): TEST_DEFINITIONS.register_alias('field_9a', 10) data_vector = DataVectorTest(parse_version("1.1.2")) From 720355c2d177a1a6630928ae23f5f2699b2995ce Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sat, 24 Jan 2026 23:15:44 +0100 Subject: [PATCH 34/61] wip --- luxtronik/definitions/calculations.py | 10 ++++++++++ luxtronik/definitions/parameters.py | 24 ++++++++++++++++++++++++ tests/test_LuxtronikData.py | 6 +++--- tests/test_socket_interaction.py | 7 ++++++- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/luxtronik/definitions/calculations.py b/luxtronik/definitions/calculations.py index 12e653ad..8d1267ff 100644 --- a/luxtronik/definitions/calculations.py +++ b/luxtronik/definitions/calculations.py @@ -2386,6 +2386,7 @@ { "index": 232, "names": ['Unknown_Calculation_232'], + "type": Unknown, }, { "index": 232, @@ -2400,6 +2401,7 @@ { "index": 233, "names": ['Unknown_Calculation_233'], + "type": Unknown, }, { "index": 233, @@ -2434,6 +2436,7 @@ { "index": 236, "names": ['Unknown_Calculation_236'], + "type": Unknown, }, { "index": 236, @@ -2448,6 +2451,7 @@ { "index": 237, "names": ['Unknown_Calculation_237'], + "type": Unknown, }, { "index": 237, @@ -2462,6 +2466,7 @@ { "index": 238, "names": ['Unknown_Calculation_238'], + "type": Unknown, }, { "index": 238, @@ -2476,6 +2481,7 @@ { "index": 239, "names": ['Unknown_Calculation_239'], + "type": Unknown, }, { "index": 239, @@ -2490,6 +2496,7 @@ { "index": 240, "names": ['Unknown_Calculation_240'], + "type": Unknown, }, { "index": 240, @@ -2514,6 +2521,7 @@ { "index": 242, "names": ['Unknown_Calculation_242'], + "type": Unknown, }, { "index": 242, @@ -2528,6 +2536,7 @@ { "index": 243, "names": ['Unknown_Calculation_243'], + "type": Unknown, }, { "index": 243, @@ -2682,6 +2691,7 @@ { "index": 258, "names": ['Unknown_Calculation_258'], + "type": Unknown, }, { "index": 258, diff --git a/luxtronik/definitions/parameters.py b/luxtronik/definitions/parameters.py index 8271f05f..ba1aa7d4 100644 --- a/luxtronik/definitions/parameters.py +++ b/luxtronik/definitions/parameters.py @@ -10919,6 +10919,7 @@ { "index": 1087, "names": ['Unknown_Parameter_1087'], + "type": Unknown, }, { "index": 1087, @@ -10973,6 +10974,7 @@ { "index": 1092, "names": ['Unknown_Parameter_1092'], + "type": Unknown, }, { "index": 1092, @@ -10987,6 +10989,7 @@ { "index": 1093, "names": ['Unknown_Parameter_1093'], + "type": Unknown, }, { "index": 1093, @@ -11001,6 +11004,7 @@ { "index": 1094, "names": ['Unknown_Parameter_1094'], + "type": Unknown, }, { "index": 1094, @@ -11015,6 +11019,7 @@ { "index": 1095, "names": ['Unknown_Parameter_1095'], + "type": Unknown, }, { "index": 1095, @@ -11029,6 +11034,7 @@ { "index": 1096, "names": ['Unknown_Parameter_1096'], + "type": Unknown, }, { "index": 1096, @@ -11043,6 +11049,7 @@ { "index": 1097, "names": ['Unknown_Parameter_1097'], + "type": Unknown, }, { "index": 1097, @@ -11057,6 +11064,7 @@ { "index": 1098, "names": ['Unknown_Parameter_1098'], + "type": Unknown, }, { "index": 1098, @@ -11071,6 +11079,7 @@ { "index": 1099, "names": ['Unknown_Parameter_1099'], + "type": Unknown, }, { "index": 1099, @@ -11085,6 +11094,7 @@ { "index": 1100, "names": ['Unknown_Parameter_1100'], + "type": Unknown, }, { "index": 1100, @@ -11099,6 +11109,7 @@ { "index": 1101, "names": ['Unknown_Parameter_1101'], + "type": Unknown, }, { "index": 1101, @@ -11113,6 +11124,7 @@ { "index": 1102, "names": ['Unknown_Parameter_1102'], + "type": Unknown, }, { "index": 1102, @@ -11127,6 +11139,7 @@ { "index": 1103, "names": ['Unknown_Parameter_1103'], + "type": Unknown, }, { "index": 1103, @@ -11141,6 +11154,7 @@ { "index": 1104, "names": ['Unknown_Parameter_1104'], + "type": Unknown, }, { "index": 1104, @@ -11155,6 +11169,7 @@ { "index": 1105, "names": ['Unknown_Parameter_1105'], + "type": Unknown, }, { "index": 1105, @@ -11169,6 +11184,7 @@ { "index": 1106, "names": ['Unknown_Parameter_1106'], + "type": Unknown, }, { "index": 1106, @@ -11183,6 +11199,7 @@ { "index": 1107, "names": ['Unknown_Parameter_1107'], + "type": Unknown, }, { "index": 1107, @@ -11197,6 +11214,7 @@ { "index": 1108, "names": ['Unknown_Parameter_1108'], + "type": Unknown, }, { "index": 1108, @@ -11211,6 +11229,7 @@ { "index": 1109, "names": ['Unknown_Parameter_1109'], + "type": Unknown, }, { "index": 1109, @@ -11225,6 +11244,7 @@ { "index": 1110, "names": ['Unknown_Parameter_1110'], + "type": Unknown, }, { "index": 1110, @@ -11239,6 +11259,7 @@ { "index": 1111, "names": ['Unknown_Parameter_1111'], + "type": Unknown, }, { "index": 1111, @@ -11253,6 +11274,7 @@ { "index": 1112, "names": ['Unknown_Parameter_1112'], + "type": Unknown, }, { "index": 1112, @@ -11267,6 +11289,7 @@ { "index": 1113, "names": ['Unknown_Parameter_1113'], + "type": Unknown, }, { "index": 1113, @@ -11331,6 +11354,7 @@ { "index": 1119, "names": ['Unknown_Parameter_1119'], + "type": Unknown, }, { "index": 1119, diff --git a/tests/test_LuxtronikData.py b/tests/test_LuxtronikData.py index 39ca2e52..f1e2bb62 100644 --- a/tests/test_LuxtronikData.py +++ b/tests/test_LuxtronikData.py @@ -65,9 +65,9 @@ def test_get_firmware_version(self): @pytest.mark.parametrize("vector, index, names", [ - ("para", 1106, ["ID_Einst_SilenceTimer_13", "Unknown_Parameter_1106"]), - ("para", 1109, ["ID_Einst_SilenceTimer_16", "Unknown_Parameter_1109"]), - ("calc", 232, ["Vapourisation_Temperature", "Unknown_Calculation_232"]), + ("para", 1106, ["ID_Einst_SilenceTimer_13"]), + ("para", 1109, ["ID_Einst_SilenceTimer_16"]), + ("calc", 232, ["Vapourisation_Temperature"]), ("calc", 241, ["HUP_PWM", "Circulation_Pump"]), ("visi", 182, ["ID_Visi_Heizung_Zeitschaltprogramm", "ID_Visi_Heizung_Zeitschlaltprogramm"]), ("visi", 326, ["Unknown_Visibility_326"]), diff --git a/tests/test_socket_interaction.py b/tests/test_socket_interaction.py index 96948d5f..196b7c36 100644 --- a/tests/test_socket_interaction.py +++ b/tests/test_socket_interaction.py @@ -3,6 +3,7 @@ import unittest.mock as mock from luxtronik import Luxtronik, LuxtronikSocketInterface, Parameters, Calculations, Visibilities +from luxtronik.collections import integrate_data from tests.fake import ( fake_create_connection, fake_parameter_value, @@ -34,7 +35,11 @@ def check_data_vector(self, data_vector): elif type(data_vector) is Visibilities: fct = fake_visibility_value for d, f in data_vector.items(): - if f.raw != fct(d.index): + # get raw data + raw = [fct(idx) for idx in range(d.index, d.index + d.count)] + temp_field = d.create_field() + integrate_data(d, temp_field, raw, 32, 0) + if f.raw != temp_field.raw: return False return True From 270c4cefdd439011bb33fd5afa2505c355a5b3ad Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sat, 24 Jan 2026 23:17:31 +0100 Subject: [PATCH 35/61] wip --- tests/shi/test_shi_vector.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/shi/test_shi_vector.py b/tests/shi/test_shi_vector.py index 309e5ad8..a18f9e03 100644 --- a/tests/shi/test_shi_vector.py +++ b/tests/shi/test_shi_vector.py @@ -1,7 +1,6 @@ from luxtronik.common import parse_version from luxtronik.datatypes import Base, Unknown from luxtronik.definitions import LuxtronikDefinitionsList -from luxtronik.shi.constants import LUXTRONIK_SHI_REGISTER_BIT_SIZE from luxtronik.shi.vector import DataVectorSmartHome from luxtronik.shi.holdings import Holdings from luxtronik.shi.inputs import Inputs From 93991fe6b335ee797a21d150d17ba584e3ab39a1 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sun, 25 Jan 2026 15:59:42 +0100 Subject: [PATCH 36/61] wip --- luxtronik/cfi/vector.py | 31 ++++++++- tests/cfi/test_cfi_vector.py | 112 +++++++++++++++++++++++++++++++ tests/shi/test_shi_contiguous.py | 10 +++ 3 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 tests/cfi/test_cfi_vector.py diff --git a/luxtronik/cfi/vector.py b/luxtronik/cfi/vector.py index 4f8b5206..840a2de3 100644 --- a/luxtronik/cfi/vector.py +++ b/luxtronik/cfi/vector.py @@ -3,6 +3,7 @@ from luxtronik.data_vector import DataVector + LOGGER = logging.getLogger(__name__) ############################################################################### @@ -12,17 +13,43 @@ class DataVectorConfig(DataVector): """Specialized DataVector for Luxtronik configuration fields.""" - def __init__(self, safe=True): - """Initialize config interface data-vector class.""" + def _init_instance(self, version, 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(version, 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(version, 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): diff --git a/tests/cfi/test_cfi_vector.py b/tests/cfi/test_cfi_vector.py new file mode 100644 index 00000000..2931fa0a --- /dev/null +++ b/tests/cfi/test_cfi_vector.py @@ -0,0 +1,112 @@ +from luxtronik.datatypes import Base, Unknown +from luxtronik.definitions import LuxtronikDefinition, LuxtronikDefinitionsList +from luxtronik.cfi.vector import DataVectorConfig +from luxtronik.cfi.parameters import Parameters +from luxtronik.cfi.calculations import Calculations +from luxtronik.cfi.visibilities import Visibilities + +############################################################################### +# Tests +############################################################################### + +def_list = [ + { + "index": 5, + "count": 1, + "names": ["field_5_bit1"], + "bit_offset": 0, + "bit_count": 1, + "type": Base, + "writeable": False, + }, + { + "index": 5, + "count": 1, + "names": ["field_5_bit2"], + "bit_offset": 1, + "bit_count": 3, + "type": Base, + "writeable": False, + }, + { + "index": 5, + "count": 1, + "names": ["field_5_all"], + "type": Base, + "writeable": False, + }, + { + "index": 7, + "count": 2, + "names": ["field_7"], + "type": Base, + "writeable": True, + }, + { + "index": 9, + "count": 2, + "names": ["field_9"], + "type": Base, + "writeable": True, + } +] +TEST_DEFINITIONS = LuxtronikDefinitionsList(def_list, 'foo', 100, 'INT32') + +FIELD_11_DICT = { + "index": 11, + "count": 1, + "names": ["field_11"], + "type": Base, + "writeable": True, +} +FIELD_12_DICT = { + "index": 12, + "count": 1, + "names": ["field_12"], + "type": Base, + "writeable": True, +} + +class DataVectorTest(DataVectorConfig): + name = 'foo' + definitions = TEST_DEFINITIONS + +class TestDataVector: + + def test_add(self): + data_vector = DataVectorTest() + assert len(data_vector) == 5 + + # In case the definitions are added after the creation + data_vector.definitions.add(FIELD_11_DICT) + data_vector.definitions.add(FIELD_12_DICT) + + # Add available index + field = data_vector.add(11) + assert len(data_vector) == 6 + assert 11 in data_vector + assert field.name == 'field_11' + + # Add not available index (not existing) + field = data_vector.add(13) + assert 'field_6' not in data_vector + assert field is None + assert len(data_vector) == 6 + + # Re-add available index + field = data_vector.add(5) + assert len(data_vector) == 6 + assert field.name == 'field_5_all' + + # Add available field + field_12 = Base('field_12', False) + field = data_vector.add(field_12) + assert 12 in data_vector + assert len(data_vector) == 7 + assert field == field_12 + + # Re-add available field + field = data_vector.add(field_12) + assert field_12 in data_vector + assert len(data_vector) == 7 + assert field == field_12 diff --git a/tests/shi/test_shi_contiguous.py b/tests/shi/test_shi_contiguous.py index 62a27d50..9dc7a72d 100644 --- a/tests/shi/test_shi_contiguous.py +++ b/tests/shi/test_shi_contiguous.py @@ -278,6 +278,16 @@ def test_get_data(self): data_arr = block.get_data_arr() assert data_arr is None + # Missing data (via gaps) + block = ContiguousDataBlock() + field_a.raw = [56, 57] + field_c.raw = [21, 22, 23] + block.add(def_a, field_a) + block.add(def_c, field_c) + + data_arr = block.get_data_arr() + assert data_arr is None + def test_repr(self): block = ContiguousDataBlock() text_empty = repr(block) From b4bd60c0b0ff7d023a6a8a8c1bca3dd31be415f9 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sun, 25 Jan 2026 16:16:38 +0100 Subject: [PATCH 37/61] wip --- tests/test_datatypes.py | 54 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index 29a6bf02..0117c337 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -77,6 +77,7 @@ def test_init(self): a = Base("base") assert a.name == "base" assert a.writeable is False + assert a.unit is None b = Base("base", writeable=True) assert b.name == "base" @@ -198,6 +199,7 @@ def test_init(self): assert a.name == "selection_base" assert not a.codes assert len(a.codes) == 0 + assert a.unit is None def test_options(self): """Test cases for options property""" @@ -217,6 +219,7 @@ def test_to_heatpump(self): """Test cases for to_heatpump function""" a = SelectionBase("") + assert a.to_heatpump("") is None assert a.to_heatpump("a") is None assert a.to_heatpump("Unknown_214") == 214 assert a.to_heatpump("unknown_215") == 215 @@ -246,6 +249,7 @@ def test_init(self): assert a.name == "selection_base_child" assert a.codes assert len(a.codes) == 3 + assert a.unit is None def test_options(self): """Test cases for options property""" @@ -287,6 +291,7 @@ def test_init(self): assert a.name == "bitmask_base" assert not a.bit_values assert len(a.bit_values) == 0 + assert a.unit is None def test_bits(self): """Test cases for bits property""" @@ -309,6 +314,7 @@ def test_to_heatpump(self): """Test cases for to_heatpump function""" a = BitMaskBase("") + assert a.to_heatpump("") is None assert a.to_heatpump("a") is None assert a.to_heatpump(1) is None assert a.to_heatpump(None) is None @@ -404,6 +410,7 @@ def test_to_heatpump(self): a = ScalingBaseTest("") assert a.to_heatpump(1) == 1 assert a.to_heatpump(42) == 42 + assert a.to_heatpump(12.0) == 12 class ScalingBaseChild(ScalingBase): @@ -430,6 +437,7 @@ def test_from_heatpump(self): assert a.from_heatpump(1) == 13 assert a.from_heatpump(2) == 26 assert a.from_heatpump(-4) == -52 + assert a.from_heatpump(None) is None def test_to_heatpump(self): """Test cases for to_heatpump function""" @@ -483,6 +491,7 @@ def test_init(self): assert a.name == "celsius" assert a.datatype_class == "temperature" assert a.datatype_unit == "°C" + assert a.unit == "°C" def test_from_heatpump(self): """Test cases for from_heatpump function""" @@ -504,6 +513,8 @@ def test_to_heatpump(self): assert Celsius.to_heatpump(-1) == -10 assert Celsius.to_heatpump(-1.1) == -11 + assert Celsius.to_heatpump(None) is None + class TestBool: """Test suite for Bool datatype""" @@ -515,18 +526,22 @@ def test_init(self): assert a.name == "bool" assert a.datatype_class == "boolean" assert a.datatype_unit is None + assert a.unit is None def test_from_heatpump(self): """Test cases for from_heatpump function""" assert Bool.from_heatpump(0) is False assert Bool.from_heatpump(1) is True + assert Bool.from_heatpump(None) is None def test_to_heatpump(self): """Test cases for to_heatpump function""" assert Bool.to_heatpump(False) == 0 assert Bool.to_heatpump(True) == 1 + assert Bool.to_heatpump(None) is None + assert Bool.to_heatpump("1") == 1 class TestFrequency: @@ -539,6 +554,7 @@ def test_init(self): assert a.name == "frequency" assert a.datatype_class == "frequency" assert a.datatype_unit == "Hz" + assert a.unit == "Hz" class TestSeconds: @@ -551,6 +567,7 @@ def test_init(self): assert a.name == "seconds" assert a.datatype_class == "timespan" assert a.datatype_unit == "s" + assert a.unit == "s" class TestIPv4Address: @@ -563,6 +580,7 @@ def test_init(self): assert a.name == "ipv4_address" assert a.datatype_class == "ipv4_address" assert a.datatype_unit is None + assert a.unit is None def test_from_heatpump(self): """Test cases for from_heatpump function""" @@ -572,6 +590,7 @@ def test_from_heatpump(self): assert IPv4Address.from_heatpump(-1062731775) == "192.168.0.1" assert IPv4Address.from_heatpump(-256) == "255.255.255.0" assert IPv4Address.from_heatpump(-1) == "255.255.255.255" + assert IPv4Address.from_heatpump(None) is None def test_to_heatpump(self): """Test cases for to_heatpump function""" @@ -581,6 +600,8 @@ def test_to_heatpump(self): assert IPv4Address.to_heatpump("192.168.0.1") == -1062731775 assert IPv4Address.to_heatpump("255.255.255.0") == -256 assert IPv4Address.to_heatpump("255.255.255.255") == -1 + assert IPv4Address.to_heatpump(1) is None + assert IPv4Address.to_heatpump(None) is None class TestTimestamp: @@ -593,6 +614,7 @@ def test_init(self): assert a.name == "timestamp" assert a.datatype_class == "timestamp" assert a.datatype_unit is None + assert a.unit is None def test_from_heatpump(self): """Test cases for from_heatpump function""" @@ -612,6 +634,8 @@ def test_to_heatpump(self): """Test cases for to_heatpump function""" a = Timestamp("") + assert a.to_heatpump(None) is None + assert a.to_heatpump("a") is None assert a.to_heatpump(datetime.datetime.fromtimestamp(0)) == 0 assert a.to_heatpump(datetime.datetime.fromtimestamp(1)) == 1 # pylint: disable=fixme @@ -656,6 +680,8 @@ def test_to_heatpump(self): assert Kelvin.to_heatpump(1) == 10 assert Kelvin.to_heatpump(1.1) == 11 + assert Kelvin.to_heatpump(None) is None + assert Kelvin.to_heatpump("b") is None class TestPressure: @@ -682,6 +708,8 @@ def test_to_heatpump(self): assert Pressure.to_heatpump(1) == 100 assert Pressure.to_heatpump(1.01) == 101 + assert Pressure.to_heatpump(None) is None + assert Pressure.to_heatpump("1") is None class TestPercent: @@ -709,6 +737,9 @@ def test_percent_to_heatpump(self): assert Percent.to_heatpump(1) == 10 assert Percent.to_heatpump(1.1) == 11 + assert Percent.to_heatpump(None) is None + assert Percent.to_heatpump("2") is None + class TestPercent2: """Test suite for Percent2 datatype""" @@ -733,6 +764,9 @@ def test_to_heatpump(self): assert Percent2.to_heatpump(10) == 10 assert Percent2.to_heatpump(11) == 11 + assert Percent2.to_heatpump(None) is None + assert Percent2.to_heatpump("3") is None + class TestSpeed: """Test suite for Speed datatype""" @@ -783,6 +817,9 @@ def test_to_heatpump(self): assert a.to_heatpump(1.5) == 15 assert a.to_heatpump(5.6) == 56 + assert a.to_heatpump(None) is None + assert a.to_heatpump("4") is None + class TestEnergy: """Test suite for Energy datatype""" @@ -809,6 +846,9 @@ def test_energy_to_heatpump(self): assert Energy.to_heatpump(1) == 10 assert Energy.to_heatpump(1.1) == 11 + assert Energy.to_heatpump(None) is None + assert Energy.to_heatpump("5") is None + class TestVoltage: """Test suite for Voltage datatype""" @@ -835,6 +875,9 @@ def test_voltage_to_heatpump(self): assert Voltage.to_heatpump(1) == 10 assert Voltage.to_heatpump(1.1) == 11 + assert Voltage.to_heatpump(None) is None + assert Voltage.to_heatpump("6") is None + class TestHours: """Test suite for Hours datatype""" @@ -861,6 +904,9 @@ def test_hours_to_heatpump(self): assert Hours.to_heatpump(1) == 10 assert Hours.to_heatpump(1.1) == 11 + assert Hours.to_heatpump(None) is None + assert Hours.to_heatpump("7") is None + class TestHours2: """Test suite for Hours2 datatype""" @@ -887,6 +933,9 @@ def test_hours2_to_heatpump(self): assert Hours2.to_heatpump(2) == 2 assert Hours2.to_heatpump(5) == 8 + assert Hours2.to_heatpump(None) is None + assert Hours2.to_heatpump("8") is None + class TestMinutes: """Test suite for Minutes datatype""" @@ -956,6 +1005,8 @@ def test_from_heatpump(self): assert Character.from_heatpump(56) == "8" assert Character.from_heatpump(48) == "0" + assert Character.from_heatpump(None) is None + class TestMajorMinorVersion: """Test suite for MajorMinorVersion datatype""" @@ -981,6 +1032,8 @@ def test_from_heatpump(self): assert MajorMinorVersion.from_heatpump(12) == "0.12" assert MajorMinorVersion.from_heatpump(-1) == "0" + assert MajorMinorVersion.from_heatpump(None) is None + class TestFullVersion: """Test suite for FullVersion datatype""" @@ -995,6 +1048,7 @@ def test_init(self): def test_from_heatpump(self): """Test cases for from_heatpump function""" + assert FullVersion.from_heatpump(None) is None assert FullVersion.from_heatpump(112) is None assert FullVersion.from_heatpump(0) is None assert FullVersion.from_heatpump([0, 12]) is None From 3f84acc4ef64b3f5a72328859858050a0ef189ab Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sun, 25 Jan 2026 16:19:33 +0100 Subject: [PATCH 38/61] wip --- luxtronik/cfi/vector.py | 4 ++-- tests/cfi/test_cfi_vector.py | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/luxtronik/cfi/vector.py b/luxtronik/cfi/vector.py index 840a2de3..a2d0154a 100644 --- a/luxtronik/cfi/vector.py +++ b/luxtronik/cfi/vector.py @@ -26,7 +26,7 @@ def __init__(self, safe=True): safe (bool): If true, prevent fields marked as not secure from being written to. """ - self._init_instance(version, safe) + self._init_instance(safe) # Add all available fields for d in self.definitions: @@ -42,7 +42,7 @@ def empty(cls, safe=True): not secure from being written to. """ obj = cls.__new__(cls) # this don't call __init__() - obj._init_instance(version, safe) + obj._init_instance(safe) return obj def add(self, def_field_name_or_idx, alias=None): diff --git a/tests/cfi/test_cfi_vector.py b/tests/cfi/test_cfi_vector.py index 2931fa0a..87e2dbe1 100644 --- a/tests/cfi/test_cfi_vector.py +++ b/tests/cfi/test_cfi_vector.py @@ -1,9 +1,7 @@ from luxtronik.datatypes import Base, Unknown -from luxtronik.definitions import LuxtronikDefinition, LuxtronikDefinitionsList +from luxtronik.definitions import LuxtronikDefinitionsList from luxtronik.cfi.vector import DataVectorConfig -from luxtronik.cfi.parameters import Parameters -from luxtronik.cfi.calculations import Calculations -from luxtronik.cfi.visibilities import Visibilities + ############################################################################### # Tests From 1d63174f24d947f10673aef66ba3e3a7c28ad2cf Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sun, 25 Jan 2026 16:22:00 +0100 Subject: [PATCH 39/61] wip --- luxtronik/cfi/vector.py | 2 +- tests/cfi/test_cfi_vector.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/luxtronik/cfi/vector.py b/luxtronik/cfi/vector.py index a2d0154a..8b6c9892 100644 --- a/luxtronik/cfi/vector.py +++ b/luxtronik/cfi/vector.py @@ -13,7 +13,7 @@ class DataVectorConfig(DataVector): """Specialized DataVector for Luxtronik configuration fields.""" - def _init_instance(self, version, safe): + def _init_instance(self, safe): """Re-usable method to initialize all instance variables.""" super()._init_instance(safe) diff --git a/tests/cfi/test_cfi_vector.py b/tests/cfi/test_cfi_vector.py index 87e2dbe1..49e9dd37 100644 --- a/tests/cfi/test_cfi_vector.py +++ b/tests/cfi/test_cfi_vector.py @@ -1,4 +1,4 @@ -from luxtronik.datatypes import Base, Unknown +from luxtronik.datatypes import Base from luxtronik.definitions import LuxtronikDefinitionsList from luxtronik.cfi.vector import DataVectorConfig From d7184af4329b6c17f3bda82d0a2efe2c7215f0f6 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sun, 25 Jan 2026 18:43:36 +0100 Subject: [PATCH 40/61] wip --- luxtronik/datatypes.py | 2 ++ tests/test_datatypes.py | 23 ++++++++++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/luxtronik/datatypes.py b/luxtronik/datatypes.py index 2a7889ee..24730482 100755 --- a/luxtronik/datatypes.py +++ b/luxtronik/datatypes.py @@ -39,6 +39,8 @@ def __init__(self, names, writeable=False): @classmethod def to_heatpump(cls, value): """Converts value into heatpump units.""" + if not isinstance(value, int): + return None return value @classmethod diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index 0117c337..83f1b953 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -540,7 +540,7 @@ def test_to_heatpump(self): assert Bool.to_heatpump(False) == 0 assert Bool.to_heatpump(True) == 1 - assert Bool.to_heatpump(None) is None + assert Bool.to_heatpump(None) == 0 assert Bool.to_heatpump("1") == 1 @@ -681,6 +681,7 @@ def test_to_heatpump(self): assert Kelvin.to_heatpump(1) == 10 assert Kelvin.to_heatpump(1.1) == 11 assert Kelvin.to_heatpump(None) is None + assert Kelvin.to_heatpump("0") == 0 assert Kelvin.to_heatpump("b") is None @@ -709,7 +710,8 @@ def test_to_heatpump(self): assert Pressure.to_heatpump(1) == 100 assert Pressure.to_heatpump(1.01) == 101 assert Pressure.to_heatpump(None) is None - assert Pressure.to_heatpump("1") is None + assert Pressure.to_heatpump("1") == 100 + assert Pressure.to_heatpump("c") is None class TestPercent: @@ -738,7 +740,8 @@ def test_percent_to_heatpump(self): assert Percent.to_heatpump(1.1) == 11 assert Percent.to_heatpump(None) is None - assert Percent.to_heatpump("2") is None + assert Percent.to_heatpump("2") == 20 + assert Percent.to_heatpump("d") is None class TestPercent2: @@ -766,6 +769,7 @@ def test_to_heatpump(self): assert Percent2.to_heatpump(None) is None assert Percent2.to_heatpump("3") is None + assert Percent2.to_heatpump("e") is None class TestSpeed: @@ -818,7 +822,8 @@ def test_to_heatpump(self): assert a.to_heatpump(5.6) == 56 assert a.to_heatpump(None) is None - assert a.to_heatpump("4") is None + assert a.to_heatpump("4") == 40 + assert a.to_heatpump("f") is None class TestEnergy: @@ -847,7 +852,8 @@ def test_energy_to_heatpump(self): assert Energy.to_heatpump(1.1) == 11 assert Energy.to_heatpump(None) is None - assert Energy.to_heatpump("5") is None + assert Energy.to_heatpump("5") == 50 + assert Energy.to_heatpump("g") is None class TestVoltage: @@ -876,7 +882,8 @@ def test_voltage_to_heatpump(self): assert Voltage.to_heatpump(1.1) == 11 assert Voltage.to_heatpump(None) is None - assert Voltage.to_heatpump("6") is None + assert Voltage.to_heatpump("6") == 60 + assert Voltage.to_heatpump("h") is None class TestHours: @@ -905,7 +912,8 @@ def test_hours_to_heatpump(self): assert Hours.to_heatpump(1.1) == 11 assert Hours.to_heatpump(None) is None - assert Hours.to_heatpump("7") is None + assert Hours.to_heatpump("7") == 70 + assert Hours.to_heatpump("i") is None class TestHours2: @@ -935,6 +943,7 @@ def test_hours2_to_heatpump(self): assert Hours2.to_heatpump(None) is None assert Hours2.to_heatpump("8") is None + assert Hours2.to_heatpump("i") is None class TestMinutes: From 0036df9115c0f1c2fffe6954e5f4128a5b022eb0 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sun, 25 Jan 2026 19:02:05 +0100 Subject: [PATCH 41/61] wip --- tests/shi/test_shi_vector.py | 50 ++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/tests/shi/test_shi_vector.py b/tests/shi/test_shi_vector.py index a18f9e03..4cf149ea 100644 --- a/tests/shi/test_shi_vector.py +++ b/tests/shi/test_shi_vector.py @@ -5,23 +5,35 @@ from luxtronik.shi.holdings import Holdings from luxtronik.shi.inputs import Inputs -""" -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 ############################################################################### + +class TestType(Base): + """ + 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 + + @classmethod + def to_heatpump(cls, value): + return value + + @classmethod + def from_heatpump(cls, value): + return value + + def_list = [ { "index": 5, "count": 1, "names": ["field_5"], - "type": Base, + "type": TestType, "writeable": False, "since": "1.1", "until": "1.2", @@ -30,7 +42,7 @@ "index": 7, "count": 2, "names": ["field_7"], - "type": Base, + "type": TestType, "writeable": True, "since": "3.1", }, @@ -38,7 +50,7 @@ "index": 9, "count": 1, "names": ["field_9a"], - "type": Base, + "type": TestType, "writeable": True, "until": "1.3", }, @@ -46,7 +58,7 @@ "index": 9, "count": 2, "names": ["field_9"], - "type": Base, + "type": TestType, "writeable": True, "until": "3.3", }, @@ -54,7 +66,7 @@ "index": -1, "count": 1, "names": ["field_invalid"], - "type": Base, + "type": TestType, "writeable": True, "until": "3.3", }, @@ -82,7 +94,7 @@ def test_create(self): field = DataVectorTest.create_any_field(7) assert field.name == 'field_7' assert field.writeable - assert type(field) is Base + assert type(field) is TestType # create not available field field = DataVectorTest.create_any_field('BAR') @@ -92,7 +104,7 @@ def test_create(self): field = DataVectorTest.create_any_field(TEST_DEFINITIONS._definitions[2]) assert field.name == 'field_9a' assert field.writeable - assert type(field) is Base + assert type(field) is TestType # create versioned data vector data_vector = DataVectorTest(parse_version("1.2")) @@ -106,7 +118,7 @@ def test_create(self): field = data_vector.create_field(5) assert field.name == 'field_5' assert not field.writeable - assert type(field) is Base + assert type(field) is TestType # create not available field (not available) field = data_vector.create_field(6) @@ -120,13 +132,13 @@ def test_create(self): field = data_vector.create_field(9) assert field.name == 'field_9' assert field.writeable - assert type(field) is Base + assert type(field) is TestType # create index-overloaded version-dependent field field = data_vector.create_field('field_9a') assert field.name == 'field_9a' assert field.writeable - assert type(field) is Base + assert type(field) is TestType # create versioned data vector data_vector = DataVectorTest(parse_version("3.0")) @@ -151,7 +163,7 @@ def test_create(self): field = data_vector.create_field(9) assert field.name == 'field_9' assert field.writeable - assert type(field) is Base + assert type(field) is TestType # create invalid field (not available) field = data_vector.create_field('field_invalid') @@ -198,7 +210,7 @@ def test_add(self): assert field.name == 'field_5' # Add available field - field_9 = Base('field_9', False) + field_9 = TestType('field_9', False) field = data_vector.add(field_9) assert 9 in data_vector assert len(data_vector) == 2 @@ -211,7 +223,7 @@ def test_add(self): assert field == field_9 # Add available field with same name - field_9_2 = Base('field_9', False) + field_9_2 = TestType('field_9', False) field = data_vector.add(field_9_2) assert field_9_2 not in data_vector assert len(data_vector) == 2 From ae0e609b17b0c57f6cc29db9f6dfd11899033e61 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sun, 25 Jan 2026 19:13:16 +0100 Subject: [PATCH 42/61] wip --- tests/cfi/test_cfi_vector.py | 14 +++++++------- tests/test_datatypes.py | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/tests/cfi/test_cfi_vector.py b/tests/cfi/test_cfi_vector.py index 49e9dd37..80480131 100644 --- a/tests/cfi/test_cfi_vector.py +++ b/tests/cfi/test_cfi_vector.py @@ -72,8 +72,8 @@ class DataVectorTest(DataVectorConfig): class TestDataVector: def test_add(self): - data_vector = DataVectorTest() - assert len(data_vector) == 5 + data_vector = DataVectorTest.empty() + assert len(data_vector) == 0 # In case the definitions are added after the creation data_vector.definitions.add(FIELD_11_DICT) @@ -81,7 +81,7 @@ def test_add(self): # Add available index field = data_vector.add(11) - assert len(data_vector) == 6 + assert len(data_vector) == 1 assert 11 in data_vector assert field.name == 'field_11' @@ -89,22 +89,22 @@ def test_add(self): field = data_vector.add(13) assert 'field_6' not in data_vector assert field is None - assert len(data_vector) == 6 + assert len(data_vector) == 1 # Re-add available index field = data_vector.add(5) - assert len(data_vector) == 6 + assert len(data_vector) == 1 assert field.name == 'field_5_all' # Add available field field_12 = Base('field_12', False) field = data_vector.add(field_12) assert 12 in data_vector - assert len(data_vector) == 7 + assert len(data_vector) == 2 assert field == field_12 # Re-add available field field = data_vector.add(field_12) assert field_12 in data_vector - assert len(data_vector) == 7 + assert len(data_vector) == 2 assert field == field_12 diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index 83f1b953..d19407ee 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -54,6 +54,7 @@ TimerProgram, TimeOfDay, TimeOfDay2, + Version, ) @@ -175,6 +176,8 @@ def test_eq(self): d = Bool("bool") assert c != d + assert a != "b" + def test_lt(self): """Test cases for __lt__ function""" @@ -542,6 +545,7 @@ def test_to_heatpump(self): assert Bool.to_heatpump(True) == 1 assert Bool.to_heatpump(None) == 0 assert Bool.to_heatpump("1") == 1 + assert Bool.to_heatpump("abc") is None class TestFrequency: @@ -994,6 +998,24 @@ def test_init(self): assert a.datatype_unit is None +class TestVersion: + """Test suite for Version datatype""" + + def test_init(self): + """Test cases for initialization""" + + a = Version("ver") + assert a.name == "ver" + assert a.datatype_class == "ver" + assert a.datatype_unit is None + + def test_from_heatpump(self): + + assert Version.from_heatpump([3, 1, 4]) == "3.1.4" + assert Version.from_heatpump(None) is None + assert Version.from_heatpump("a") is None + + class TestCharacter: """Test suite for Character datatype""" @@ -1421,6 +1443,7 @@ def test_timeofday_conversion(self): """Test cases for from_heatpump function""" assert TimeOfDay.from_heatpump(None) is None + assert TimeOfDay.from_heatpump(1) is None check_pair(TimeOfDay, 7 * 3600 + 30 * 60, "7:30") check_pair(TimeOfDay, 7 * 3600 + 30 * 60 + 50, "7:30:50") @@ -1445,6 +1468,7 @@ def test_timeofday_conversion(self): """Test cases for from_heatpump function""" assert TimeOfDay2.from_heatpump(None) is None + assert TimeOfDay2.from_heatpump(1) is None check_pair(TimeOfDay2, ((19 * 60) << 16) + 7 * 60 + 30, "7:30-19:00") check_pair(TimeOfDay2, ((19 * 60 + 30) << 16) + 5 * 60 + 23, "5:23-19:30") From 3d3315fbfa79029050906b9ca6307b9e7fa656c5 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sun, 25 Jan 2026 19:25:48 +0100 Subject: [PATCH 43/61] wip --- luxtronik/datatypes.py | 1 + tests/cfi/test_cfi_vector.py | 2 +- tests/test_datatypes.py | 17 ++++++++++++----- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/luxtronik/datatypes.py b/luxtronik/datatypes.py index 24730482..ccf79224 100755 --- a/luxtronik/datatypes.py +++ b/luxtronik/datatypes.py @@ -599,6 +599,7 @@ class Version(Base): concatenate_multiple_data_chunks = False + @classmethod def from_heatpump(self, value): if not isinstance(value, list): return None diff --git a/tests/cfi/test_cfi_vector.py b/tests/cfi/test_cfi_vector.py index 80480131..2ca6d552 100644 --- a/tests/cfi/test_cfi_vector.py +++ b/tests/cfi/test_cfi_vector.py @@ -92,7 +92,7 @@ def test_add(self): assert len(data_vector) == 1 # Re-add available index - field = data_vector.add(5) + field = data_vector.add(11) assert len(data_vector) == 1 assert field.name == 'field_5_all' diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index d19407ee..8139d201 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -541,11 +541,16 @@ def test_from_heatpump(self): def test_to_heatpump(self): """Test cases for to_heatpump function""" + class BoolExcept: + def __bool__(self): + return "x" + assert Bool.to_heatpump(False) == 0 assert Bool.to_heatpump(True) == 1 assert Bool.to_heatpump(None) == 0 assert Bool.to_heatpump("1") == 1 - assert Bool.to_heatpump("abc") is None + assert Bool.to_heatpump("abc") == 1 + assert Bool.to_heatpump(BoolExcept()) is None class TestFrequency: @@ -1006,12 +1011,12 @@ def test_init(self): a = Version("ver") assert a.name == "ver" - assert a.datatype_class == "ver" + assert a.datatype_class == "version" assert a.datatype_unit is None def test_from_heatpump(self): - assert Version.from_heatpump([3, 1, 4]) == "3.1.4" + assert Version.from_heatpump([3, 1, 4]) == '\x03\x01\x04' assert Version.from_heatpump(None) is None assert Version.from_heatpump("a") is None @@ -1443,7 +1448,8 @@ def test_timeofday_conversion(self): """Test cases for from_heatpump function""" assert TimeOfDay.from_heatpump(None) is None - assert TimeOfDay.from_heatpump(1) is None + assert TimeOfDay.to_heatpump(None) is None + assert TimeOfDay.to_heatpump(1) is None check_pair(TimeOfDay, 7 * 3600 + 30 * 60, "7:30") check_pair(TimeOfDay, 7 * 3600 + 30 * 60 + 50, "7:30:50") @@ -1468,7 +1474,8 @@ def test_timeofday_conversion(self): """Test cases for from_heatpump function""" assert TimeOfDay2.from_heatpump(None) is None - assert TimeOfDay2.from_heatpump(1) is None + assert TimeOfDay2.to_heatpump(None) is None + assert TimeOfDay2.to_heatpump(1) is None check_pair(TimeOfDay2, ((19 * 60) << 16) + 7 * 60 + 30, "7:30-19:00") check_pair(TimeOfDay2, ((19 * 60 + 30) << 16) + 5 * 60 + 23, "5:23-19:30") From c2f4447b1a44da0b32ed5bd4330663b2934425c4 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Sun, 25 Jan 2026 19:27:32 +0100 Subject: [PATCH 44/61] wip --- tests/cfi/test_cfi_vector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cfi/test_cfi_vector.py b/tests/cfi/test_cfi_vector.py index 2ca6d552..a97fa18b 100644 --- a/tests/cfi/test_cfi_vector.py +++ b/tests/cfi/test_cfi_vector.py @@ -94,7 +94,7 @@ def test_add(self): # Re-add available index field = data_vector.add(11) assert len(data_vector) == 1 - assert field.name == 'field_5_all' + assert field.name == 'field_11' # Add available field field_12 = Base('field_12', False) From 9ac333c06c6506d0644037197918d392dcbae3f9 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Tue, 27 Jan 2026 21:54:55 +0100 Subject: [PATCH 45/61] wip --- luxtronik/constants.py | 2 +- luxtronik/definitions/__init__.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/luxtronik/constants.py b/luxtronik/constants.py index bb88c487..65185d75 100644 --- a/luxtronik/constants.py +++ b/luxtronik/constants.py @@ -14,6 +14,6 @@ # Content of response that is contained in responses to discovery broadcast LUXTRONIK_DISCOVERY_RESPONSE_PREFIX: Final = "2500;111;" -# Since version 3.92.0, all unavailable 16 bit data fields +# 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 diff --git a/luxtronik/definitions/__init__.py b/luxtronik/definitions/__init__.py index dddafa6a..6f5d908a 100644 --- a/luxtronik/definitions/__init__.py +++ b/luxtronik/definitions/__init__.py @@ -167,6 +167,10 @@ def field_type(self): def writeable(self): return self._writeable + @property + def data_type(self): + return self._data_type + @property def bit_offset(self): return self._bit_offset From 61d884979484154bf24f823b161a6f89ff7b8063 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Thu, 29 Jan 2026 22:17:03 +0100 Subject: [PATCH 46/61] wip --- luxtronik/cfi/interface.py | 4 +++- tests/cfi/test_cfi_interface.py | 21 +++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/luxtronik/cfi/interface.py b/luxtronik/cfi/interface.py index 87ad8c99..0e643d13 100644 --- a/luxtronik/cfi/interface.py +++ b/luxtronik/cfi/interface.py @@ -266,9 +266,10 @@ def _parse(self, data_vector, raw_data): Parse raw data into the corresponding fields. Args: + data_vector (DataVector): Data vector in which + the raw data is to be integrated. raw_data (list[int]): List of raw register values. The raw data must start at register index 0. - num_bits (int): Number of bits per register. """ raw_len = len(raw_data) # Prepare a list of undefined indices @@ -281,6 +282,7 @@ def _parse(self, data_vector, raw_data): next_idx = definition.index + definition.count if next_idx > raw_len: # not enough registers + field.raw = None continue # remove all used indices from the list of undefined indices for index in range(definition.index, next_idx): diff --git a/tests/cfi/test_cfi_interface.py b/tests/cfi/test_cfi_interface.py index a46a2ad4..755b9e59 100644 --- a/tests/cfi/test_cfi_interface.py +++ b/tests/cfi/test_cfi_interface.py @@ -9,7 +9,6 @@ class TestLuxtronikSocketInterface: - def test_parse(self): lux = LuxtronikSocketInterface('host') parameters = Parameters() @@ -32,4 +31,22 @@ def test_parse(self): lux._parse(visibilities, t) v = visibilities.get(n) assert v.name == f"unknown_visibility_{n}" - assert v.raw == n \ No newline at end of file + assert v.raw == n + + n = 10 + t = list(range(0, n + 1)) + + lux._parse(parameters, t) + for definition, field in parameters.data.pairs(): + if definition.index > n: + assert field.raw is None + + lux._parse(calculations, t) + for definition, field in calculations.data.pairs(): + if definition.index > n: + assert field.raw is None + + lux._parse(visibilities, t) + for definition, field in visibilities.data.pairs(): + if definition.index > n: + assert field.raw is None \ No newline at end of file From bcfec0be64e4be8d9d7769a2c0b2ef8de8813b7f Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Tue, 3 Feb 2026 19:37:49 +0100 Subject: [PATCH 47/61] wip --- luxtronik/cfi/interface.py | 5 +++-- luxtronik/shi/interface.py | 2 ++ tests/cfi/test_cfi_parameters.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/luxtronik/cfi/interface.py b/luxtronik/cfi/interface.py index 0e643d13..a14df1ba 100644 --- a/luxtronik/cfi/interface.py +++ b/luxtronik/cfi/interface.py @@ -164,6 +164,7 @@ def _write_and_read(self, parameters, data): def _write(self, parameters): 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 isinstance(value, int): LOGGER.warning( @@ -172,7 +173,6 @@ def _write(self, parameters): definition.index, value, ) - field.write_pending = False continue LOGGER.info("%s: Parameter '%d' set to '%s'", self._host, definition.index, value) self._send_ints(LUXTRONIK_PARAMETERS_WRITE, definition.index, value) @@ -180,7 +180,6 @@ def _write(self, parameters): LOGGER.debug("%s: Command %s", self._host, cmd) val = self._read_int() LOGGER.debug("%s: Value %s", self._host, val) - field.write_pending = False # Give the heatpump a short time to handle the value changes/calculations: time.sleep(WAIT_TIME_AFTER_PARAMETER_WRITE) @@ -287,6 +286,8 @@ def _parse(self, data_vector, raw_data): # 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 diff --git a/luxtronik/shi/interface.py b/luxtronik/shi/interface.py index 2ff751e8..ab82c848 100644 --- a/luxtronik/shi/interface.py +++ b/luxtronik/shi/interface.py @@ -321,6 +321,8 @@ def _integrate_data(self, telegrams_data): success = True for block, telegram, read_not_write in telegrams_data: if (read_not_write == READ): + # integrate_data() also resets the write_pending flag, + # intentionally only for read fields valid = block.integrate_data(telegram.data) if not valid: LOGGER.debug(f"Failed to integrate read data into {block}") diff --git a/tests/cfi/test_cfi_parameters.py b/tests/cfi/test_cfi_parameters.py index 6438ae15..8acb6f8b 100644 --- a/tests/cfi/test_cfi_parameters.py +++ b/tests/cfi/test_cfi_parameters.py @@ -80,7 +80,7 @@ def test_set(self): parameters.set("BarFoo", 0) assert parameters["BarFoo"] is None - # Set something which was previously not allowed to be set + # Set something which was previously (v0.3.14) not allowed to be set parameters.set("ID_Transfert_LuxNet", 1) assert parameters["ID_Transfert_LuxNet"].raw == 1 assert parameters["ID_Transfert_LuxNet"].write_pending @@ -92,7 +92,7 @@ def test_set(self): parameters = Parameters(safe=False) - # Set something which was previously not allowed to be set, but we are brave. + # Set something which was previously (v0.3.14) not allowed to be set, but we are brave. parameters.set("ID_Transfert_LuxNet", 4) assert parameters["ID_Transfert_LuxNet"].raw == 4 assert parameters["ID_Transfert_LuxNet"].write_pending From def850517a4e08ce45775e3dd0525a9231c23198 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Tue, 3 Feb 2026 22:12:06 +0100 Subject: [PATCH 48/61] wip --- tests/shi/test_shi_interface.py | 152 +++++++++++++++++++++----------- 1 file changed, 100 insertions(+), 52 deletions(-) diff --git a/tests/shi/test_shi_interface.py b/tests/shi/test_shi_interface.py index 76fc209d..6f58b4ae 100644 --- a/tests/shi/test_shi_interface.py +++ b/tests/shi/test_shi_interface.py @@ -28,6 +28,10 @@ ) from tests.fake import FakeModbus +IDX_BLK = 0 +IDX_TLG = 1 +IDX_RNW = 2 + class TestLuxtronikSmartHomeData: @@ -244,8 +248,8 @@ def test_create_telegram(self): telegram = self.interface._create_telegram(block, "input", False) assert telegram is None - def test_create_telegrams(self): - blocks_list = [] + def create_contiguous_block_list(self): + block_list = [] blocks = ContiguousDataBlockList("holding", True) # block 1 @@ -260,86 +264,130 @@ def test_create_telegrams(self): # block 3 blocks.append_single(HOLDINGS_DEFINITIONS[10], HOLDINGS_DEFINITIONS[10].create_field()) - blocks_list.append(blocks) + block_list.append(blocks) blocks = ContiguousDataBlockList("holding", False) - # invalid block + # invalid block because of invalid data blocks.append_single(HOLDINGS_DEFINITIONS[12], HOLDINGS_DEFINITIONS[12].create_field()) + # block 4 field3 = HOLDINGS_DEFINITIONS[17].create_field() blocks.append_single(HOLDINGS_DEFINITIONS[17], field3) - blocks_list.append(blocks) + block_list.append(blocks) field3.raw = 17 - telegram_data = self.interface._create_telegrams(blocks_list) + assert len(block_list) == 2 + assert len(block_list[0]) == 3 + assert len(block_list[0][0]) == 2 + assert len(block_list[0][1]) == 1 + assert len(block_list[0][2]) == 1 + assert len(block_list[1]) == 2 + assert len(block_list[1][0]) == 1 + assert len(block_list[1][1]) == 1 + + return block_list + + def test_create_telegrams(self): + block_list = self.create_contiguous_block_list() + + telegram_data = self.interface._create_telegrams(block_list) assert len(telegram_data) == 4 + + # Note: telegram_data[block index][tuple index] + # Note: telegram_data[block index][IDX_BLK][part index] + # blocks - assert len(telegram_data[0][0]) == 2 - assert telegram_data[0][0].first_index == 10 - assert telegram_data[0][0].overall_count == 2 - assert len(telegram_data[1][0]) == 1 - assert telegram_data[1][0].first_index == 17 - assert telegram_data[1][0].overall_count == 1 - assert len(telegram_data[2][0]) == 1 - assert telegram_data[2][0].first_index == 10 - assert telegram_data[2][0].overall_count == 1 - assert len(telegram_data[3][0]) == 1 - assert telegram_data[3][0].first_index == 17 - assert telegram_data[3][0].overall_count == 1 + assert len(telegram_data[0][IDX_BLK]) == 2 + assert telegram_data[0][IDX_BLK].first_index == 10 + assert telegram_data[0][IDX_BLK].overall_count == 2 + assert len(telegram_data[1][IDX_BLK]) == 1 + assert telegram_data[1][IDX_BLK].first_index == 17 + assert telegram_data[1][IDX_BLK].overall_count == 1 + assert len(telegram_data[2][IDX_BLK]) == 1 + assert telegram_data[2][IDX_BLK].first_index == 10 + assert telegram_data[2][IDX_BLK].overall_count == 1 + assert len(telegram_data[3][IDX_BLK]) == 1 + assert telegram_data[3][IDX_BLK].first_index == 17 + assert telegram_data[3][IDX_BLK].overall_count == 1 # telegrams - assert telegram_data[0][1].count == 2 - assert telegram_data[1][1].count == 1 - assert telegram_data[2][1].count == 1 - assert telegram_data[3][1].count == 1 + assert telegram_data[0][IDX_TLG].count == 2 + assert telegram_data[1][IDX_TLG].count == 1 + assert telegram_data[2][IDX_TLG].count == 1 + assert telegram_data[3][IDX_TLG].count == 1 # read not write - assert telegram_data[0][2] - assert telegram_data[1][2] - assert telegram_data[2][2] - assert not telegram_data[3][2] + assert telegram_data[0][IDX_RNW] + assert telegram_data[1][IDX_RNW] + assert telegram_data[2][IDX_RNW] + assert not telegram_data[3][IDX_RNW] + + def test_integrate_data(self): + block_list = self.create_contiguous_block_list() + field3 = block_list[1][1][0].field + + telegram_data = self.interface._create_telegrams(block_list) # integrate - telegram_data[0][1].data = [18, 4] - telegram_data[1][1].data = [9] - telegram_data[2][1].data = [27] - telegram_data[3][0][0].field.write_pending = True + telegram_data[0][IDX_TLG].data = [18, 4] + telegram_data[0][IDX_BLK][0].field.write_pending = True + telegram_data[0][IDX_BLK][1].field.write_pending = False + telegram_data[1][IDX_TLG].data = [9] + telegram_data[2][IDX_TLG].data = [27] + telegram_data[3][IDX_BLK][0].field.write_pending = True valid = self.interface._integrate_data(telegram_data) assert valid # [index data, index for blocks, index for part] - assert telegram_data[0][0][0].field.raw == 18 - assert telegram_data[0][0][1].field.raw == 4 - assert telegram_data[1][0][0].field.raw == 9 - assert telegram_data[2][0][0].field.raw == 27 - assert not telegram_data[3][0][0].field.write_pending - assert telegram_data[3][0][0].field.raw == 17 # no update + assert telegram_data[0][IDX_BLK][0].field.raw == 18 + assert not telegram_data[0][IDX_BLK][0].field.write_pending + assert telegram_data[0][IDX_BLK][1].field.raw == 4 + assert not telegram_data[0][IDX_BLK][1].field.write_pending + assert telegram_data[1][IDX_BLK][0].field.raw == 9 + assert telegram_data[2][IDX_BLK][0].field.raw == 27 + assert telegram_data[3][IDX_BLK][0].field.raw == 17 # no update + assert not telegram_data[3][IDX_BLK][0].field.write_pending # integrate not available / None -> no error - telegram_data[0][1].data = [18, 4] - telegram_data[1][1].data = [LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] - telegram_data[2][1].data = [None] + telegram_data[0][IDX_TLG].data = [19, 5] + telegram_data[0][IDX_BLK][0].field.write_pending = True + telegram_data[1][IDX_TLG].data = [LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE] + telegram_data[1][IDX_BLK][0].field.write_pending = True + telegram_data[2][IDX_TLG].data = [None] + telegram_data[2][IDX_BLK][0].field.write_pending = True + telegram_data[3][IDX_BLK][0].field.write_pending = True valid = self.interface._integrate_data(telegram_data) assert valid # [index data, index for blocks, index for part] - assert telegram_data[0][0][0].field.raw == 18 - assert telegram_data[0][0][1].field.raw == 4 - assert telegram_data[1][0][0].field.raw is None - assert telegram_data[2][0][0].field.raw is None - assert telegram_data[3][0][0].field.raw == 17 # no update + assert telegram_data[0][IDX_BLK][0].field.raw == 19 + assert not telegram_data[0][IDX_BLK][0].field.write_pending + assert telegram_data[0][IDX_BLK][1].field.raw == 5 + assert not telegram_data[0][IDX_BLK][1].field.write_pending + assert telegram_data[1][IDX_BLK][0].field.raw is None + assert not telegram_data[1][IDX_BLK][0].field.write_pending # update with none -> reset flag + assert telegram_data[2][IDX_BLK][0].field.raw is None + assert not telegram_data[2][IDX_BLK][0].field.write_pending # update with none -> reset flag + assert telegram_data[3][IDX_BLK][0].field.raw == 17 # no update + assert not telegram_data[3][IDX_BLK][0].field.write_pending # integrate too less -> error - telegram_data[0][1].data = [18] - telegram_data[1][1].data = [1] - telegram_data[2][1].data = [None] + telegram_data[0][IDX_TLG].data = [18] + telegram_data[0][IDX_BLK][0].field.write_pending = True + telegram_data[0][IDX_BLK][1].field.write_pending = True + telegram_data[1][IDX_TLG].data = [2] + telegram_data[1][IDX_BLK][0].field.write_pending = True + telegram_data[2][IDX_TLG].data = [None] valid = self.interface._integrate_data(telegram_data) assert not valid # [index data, index for blocks, index for part] - assert telegram_data[0][0][0].field.raw == 18 - assert telegram_data[0][0][1].field.raw == 4 # no update - assert telegram_data[1][0][0].field.raw == 1 - assert telegram_data[2][0][0].field.raw is None - assert telegram_data[3][0][0].field.raw == 17 # no update + assert telegram_data[0][IDX_BLK][0].field.raw == 19 # no update + assert telegram_data[0][IDX_BLK][0].field.write_pending # no update + assert telegram_data[0][IDX_BLK][1].field.raw == 5 # no update + assert telegram_data[0][IDX_BLK][1].field.write_pending # no update + assert telegram_data[1][IDX_BLK][0].field.raw == 2 + assert not telegram_data[1][IDX_BLK][0].field.write_pending + assert telegram_data[2][IDX_BLK][0].field.raw is None + assert telegram_data[3][IDX_BLK][0].field.raw == 17 # no update def test_prepare(self): definition = HOLDINGS_DEFINITIONS[2] From 82dcc81f174ec60226a54854e19dd7fe21453aba Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Tue, 3 Feb 2026 22:22:35 +0100 Subject: [PATCH 49/61] wip --- tests/cfi/test_cfi_interface.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/tests/cfi/test_cfi_interface.py b/tests/cfi/test_cfi_interface.py index 755b9e59..e842d0f2 100644 --- a/tests/cfi/test_cfi_interface.py +++ b/tests/cfi/test_cfi_interface.py @@ -18,35 +18,53 @@ def test_parse(self): n = 2000 t = list(range(0, n + 1)) + parameters[0].write_pending = True lux._parse(parameters, t) p = parameters.get(n) assert p.name == f"unknown_parameter_{n}" assert p.raw == n + assert not p.write_pending + calculations[0].write_pending = True lux._parse(calculations, t) c = calculations.get(n) assert c.name == f"unknown_calculation_{n}" assert c.raw == n + assert not c.write_pending + visibilities[0].write_pending = True lux._parse(visibilities, t) v = visibilities.get(n) assert v.name == f"unknown_visibility_{n}" assert v.raw == n + assert not v.write_pending n = 10 t = list(range(0, n + 1)) + parameters[0].write_pending = True + parameters[20].write_pending = True + parameters[40].write_pending = True lux._parse(parameters, t) - for definition, field in parameters.data.pairs(): + for definition, field in parameters.data.items(): if definition.index > n: assert field.raw is None + assert not field.write_pending + calculations[0].write_pending = True + calculations[20].write_pending = True + calculations[40].write_pending = True lux._parse(calculations, t) - for definition, field in calculations.data.pairs(): + for definition, field in calculations.data.items(): if definition.index > n: assert field.raw is None + assert not field.write_pending + visibilities[0].write_pending = True + visibilities[20].write_pending = True + visibilities[40].write_pending = True lux._parse(visibilities, t) - for definition, field in visibilities.data.pairs(): + for definition, field in visibilities.data.items(): if definition.index > n: - assert field.raw is None \ No newline at end of file + assert field.raw is None + assert not field.write_pending \ No newline at end of file From e94f17fc7526ed23b43b05b2fbd2dcb0f11be966 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Tue, 3 Feb 2026 23:31:35 +0100 Subject: [PATCH 50/61] preserve --- luxtronik/__init__.py | 2 +- luxtronik/cfi/interface.py | 5 ++-- luxtronik/common.py | 10 +++++++ luxtronik/shi/contiguous.py | 8 ++++++ tests/cfi/test_cfi_interface.py | 49 ++++++++++++++++++++------------- tests/shi/test_shi_interface.py | 35 ++++++++++++++++++++--- 6 files changed, 83 insertions(+), 26 deletions(-) diff --git a/luxtronik/__init__.py b/luxtronik/__init__.py index 9f5fa168..52165676 100755 --- a/luxtronik/__init__.py +++ b/luxtronik/__init__.py @@ -6,7 +6,7 @@ import logging -from luxtronik.common import get_host_lock +from luxtronik.common import LuxtronikSettings, get_host_lock from luxtronik.discover import discover # noqa: F401 from luxtronik.cfi import ( diff --git a/luxtronik/cfi/interface.py b/luxtronik/cfi/interface.py index a14df1ba..62a86162 100644 --- a/luxtronik/cfi/interface.py +++ b/luxtronik/cfi/interface.py @@ -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, @@ -281,7 +281,8 @@ def _parse(self, data_vector, raw_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): diff --git a/luxtronik/common.py b/luxtronik/common.py index dbef88a7..013a84a7 100644 --- a/luxtronik/common.py +++ b/luxtronik/common.py @@ -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 ############################################################################### diff --git a/luxtronik/shi/contiguous.py b/luxtronik/shi/contiguous.py index fcb6c500..80653487 100644 --- a/luxtronik/shi/contiguous.py +++ b/luxtronik/shi/contiguous.py @@ -6,6 +6,7 @@ import logging +from luxtronik.common import LuxtronikSettings from luxtronik.collections import LuxtronikDefFieldPair from luxtronik.shi.constants import LUXTRONIK_SHI_REGISTER_BIT_SIZE @@ -161,11 +162,18 @@ def integrate_data(self, data_arr): data_len = len(data_arr) if valid else 0 valid &= data_len == self.overall_count + print('test1') + if not valid: + print('test2') LOGGER.debug( f"Data to integrate not valid! Expected length {self.overall_count} " \ + f"but got {data_len}: data = {data_arr}, block = {self}" ) + if not LuxtronikSettings.preserve_last_read_value_on_fail: + print('test3!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!1') + for definition, field in self._parts: + field.raw = None return False first = self.first_index diff --git a/tests/cfi/test_cfi_interface.py b/tests/cfi/test_cfi_interface.py index e842d0f2..aced441a 100644 --- a/tests/cfi/test_cfi_interface.py +++ b/tests/cfi/test_cfi_interface.py @@ -1,9 +1,10 @@ from luxtronik import ( - Parameters, - Calculations, - Visibilities, - LuxtronikSocketInterface, + LuxtronikSettings, + Parameters, + Calculations, + Visibilities, + LuxtronikSocketInterface, ) @@ -42,29 +43,39 @@ def test_parse(self): n = 10 t = list(range(0, n + 1)) - parameters[0].write_pending = True - parameters[20].write_pending = True - parameters[40].write_pending = True + orig_preserve = LuxtronikSettings.preserve_last_read_value_on_fail + + LuxtronikSettings.preserve_last_read_value_on_fail = True + preserve = LuxtronikSettings.preserve_last_read_value_on_fail + for definition, field in parameters.data.items(): + field.write_pending = True lux._parse(parameters, t) for definition, field in parameters.data.items(): - if definition.index > n: + value_available = definition.index > n + if value_available and not preserve: assert field.raw is None - assert not field.write_pending + assert field.write_pending == (value_available and preserve) - calculations[0].write_pending = True - calculations[20].write_pending = True - calculations[40].write_pending = True + LuxtronikSettings.preserve_last_read_value_on_fail = False + preserve = LuxtronikSettings.preserve_last_read_value_on_fail + for definition, field in calculations.data.items(): + field.write_pending = True lux._parse(calculations, t) for definition, field in calculations.data.items(): - if definition.index > n: + value_available = definition.index > n + if value_available and not preserve: assert field.raw is None - assert not field.write_pending + assert field.write_pending == (value_available and preserve) - visibilities[0].write_pending = True - visibilities[20].write_pending = True - visibilities[40].write_pending = True + LuxtronikSettings.preserve_last_read_value_on_fail = False + preserve = LuxtronikSettings.preserve_last_read_value_on_fail + for definition, field in visibilities.data.items(): + field.write_pending = True lux._parse(visibilities, t) for definition, field in visibilities.data.items(): - if definition.index > n: + value_available = definition.index > n + if value_available and not preserve: assert field.raw is None - assert not field.write_pending \ No newline at end of file + assert field.write_pending == (value_available and preserve) + + LuxtronikSettings.preserve_last_read_value_on_fail = orig_preserve \ No newline at end of file diff --git a/tests/shi/test_shi_interface.py b/tests/shi/test_shi_interface.py index 6f58b4ae..ed7c5d18 100644 --- a/tests/shi/test_shi_interface.py +++ b/tests/shi/test_shi_interface.py @@ -1,6 +1,7 @@ import pytest from unittest.mock import patch +from luxtronik.common import LuxtronikSettings from luxtronik.constants import LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE from luxtronik.datatypes import Base, Unknown from luxtronik.definitions import LuxtronikDefinition @@ -370,6 +371,30 @@ def test_integrate_data(self): assert telegram_data[3][IDX_BLK][0].field.raw == 17 # no update assert not telegram_data[3][IDX_BLK][0].field.write_pending + orig_preserve = LuxtronikSettings.preserve_last_read_value_on_fail + LuxtronikSettings.preserve_last_read_value_on_fail = True + + # integrate too less -> error + telegram_data[0][IDX_TLG].data = [18] + telegram_data[0][IDX_BLK][0].field.write_pending = True + telegram_data[0][IDX_BLK][1].field.write_pending = True + telegram_data[1][IDX_TLG].data = [2] + telegram_data[1][IDX_BLK][0].field.write_pending = True + telegram_data[2][IDX_TLG].data = [None] + valid = self.interface._integrate_data(telegram_data) + assert not valid + # [index data, index for blocks, index for part] + assert telegram_data[0][IDX_BLK][0].field.raw == 19 # preserve -> no update + assert telegram_data[0][IDX_BLK][0].field.write_pending # preserve -> no update + assert telegram_data[0][IDX_BLK][1].field.raw == 5 # preserve -> no update + assert telegram_data[0][IDX_BLK][1].field.write_pending # preserve -> no update + assert telegram_data[1][IDX_BLK][0].field.raw == 2 + assert not telegram_data[1][IDX_BLK][0].field.write_pending + assert telegram_data[2][IDX_BLK][0].field.raw is None + assert telegram_data[3][IDX_BLK][0].field.raw == 17 # no update + + LuxtronikSettings.preserve_last_read_value_on_fail = False + # integrate too less -> error telegram_data[0][IDX_TLG].data = [18] telegram_data[0][IDX_BLK][0].field.write_pending = True @@ -380,15 +405,17 @@ def test_integrate_data(self): valid = self.interface._integrate_data(telegram_data) assert not valid # [index data, index for blocks, index for part] - assert telegram_data[0][IDX_BLK][0].field.raw == 19 # no update - assert telegram_data[0][IDX_BLK][0].field.write_pending # no update - assert telegram_data[0][IDX_BLK][1].field.raw == 5 # no update - assert telegram_data[0][IDX_BLK][1].field.write_pending # no update + assert telegram_data[0][IDX_BLK][0].field.raw is None # no preserve -> update + assert not telegram_data[0][IDX_BLK][0].field.write_pending # no preserve -> update + assert telegram_data[0][IDX_BLK][1].field.raw is None # no preserve -> update + assert not telegram_data[0][IDX_BLK][1].field.write_pending # no preserve -> update assert telegram_data[1][IDX_BLK][0].field.raw == 2 assert not telegram_data[1][IDX_BLK][0].field.write_pending assert telegram_data[2][IDX_BLK][0].field.raw is None assert telegram_data[3][IDX_BLK][0].field.raw == 17 # no update + LuxtronikSettings.preserve_last_read_value_on_fail = orig_preserve + def test_prepare(self): definition = HOLDINGS_DEFINITIONS[2] field = definition.create_field() From 83e62971988db12c16505af952b3f64ef37add0b Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Tue, 3 Feb 2026 23:32:40 +0100 Subject: [PATCH 51/61] wip --- luxtronik/shi/contiguous.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/luxtronik/shi/contiguous.py b/luxtronik/shi/contiguous.py index 80653487..d28d8fbd 100644 --- a/luxtronik/shi/contiguous.py +++ b/luxtronik/shi/contiguous.py @@ -162,16 +162,12 @@ def integrate_data(self, data_arr): data_len = len(data_arr) if valid else 0 valid &= data_len == self.overall_count - print('test1') - if not valid: - print('test2') LOGGER.debug( f"Data to integrate not valid! Expected length {self.overall_count} " \ + f"but got {data_len}: data = {data_arr}, block = {self}" ) if not LuxtronikSettings.preserve_last_read_value_on_fail: - print('test3!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!1') for definition, field in self._parts: field.raw = None return False From 06e0729526d9e548d5d56061ac58a77b521db269 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Tue, 3 Feb 2026 23:36:51 +0100 Subject: [PATCH 52/61] wip --- luxtronik/__init__.py | 2 +- tests/shi/test_shi_interface.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/luxtronik/__init__.py b/luxtronik/__init__.py index 52165676..278a5516 100755 --- a/luxtronik/__init__.py +++ b/luxtronik/__init__.py @@ -6,7 +6,7 @@ import logging -from luxtronik.common import LuxtronikSettings, get_host_lock +from luxtronik.common import LuxtronikSettings, get_host_lock # noqa: F401 from luxtronik.discover import discover # noqa: F401 from luxtronik.cfi import ( diff --git a/tests/shi/test_shi_interface.py b/tests/shi/test_shi_interface.py index ed7c5d18..c2c96cc9 100644 --- a/tests/shi/test_shi_interface.py +++ b/tests/shi/test_shi_interface.py @@ -326,7 +326,6 @@ def test_create_telegrams(self): def test_integrate_data(self): block_list = self.create_contiguous_block_list() - field3 = block_list[1][1][0].field telegram_data = self.interface._create_telegrams(block_list) From 3505fdd94409ba8e13b171d9c9d6a33d18beaa0b Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Thu, 5 Feb 2026 18:52:12 +0100 Subject: [PATCH 53/61] wip --- luxtronik/definitions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luxtronik/definitions/__init__.py b/luxtronik/definitions/__init__.py index 6f5d908a..9afa9318 100644 --- a/luxtronik/definitions/__init__.py +++ b/luxtronik/definitions/__init__.py @@ -249,7 +249,7 @@ def __getitem__(self, name_or_idx): def __contains__(self, def_name_or_idx): if isinstance(def_name_or_idx, LuxtronikDefinition): - return any(def_name_or_idx is d for d in self._index_dict.values()) + return any(def_name_or_idx is d for d in self._name_dict.values()) return self._get(def_name_or_idx) is not None def _add_alias(self, definition, alias): From a97eef650d8dc2e11e01dbb8b882d8b644a270a2 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Thu, 5 Feb 2026 21:52:24 +0100 Subject: [PATCH 54/61] wip --- luxtronik/cfi/interface.py | 3 ++- luxtronik/data_vector.py | 26 +++++++++++++++++++++++++- luxtronik/datatypes.py | 14 ++++++++++---- luxtronik/definitions/parameters.py | 14 +++++++------- luxtronik/definitions/visibilities.py | 2 +- luxtronik/shi/interface.py | 11 +++++------ tests/test_LuxtronikData.py | 6 +++--- 7 files changed, 53 insertions(+), 23 deletions(-) diff --git a/luxtronik/cfi/interface.py b/luxtronik/cfi/interface.py index 62a86162..01344b45 100644 --- a/luxtronik/cfi/interface.py +++ b/luxtronik/cfi/interface.py @@ -6,6 +6,7 @@ import time from luxtronik.common import LuxtronikSettings, get_host_lock +from luxtronik.data_vector import check_write_data from luxtronik.cfi.constants import ( LUXTRONIK_DEFAULT_PORT, LUXTRONIK_PARAMETERS_WRITE, @@ -166,7 +167,7 @@ def _write(self, parameters): if field.write_pending: field.write_pending = False value = field.raw - if not isinstance(definition.index, int) or not isinstance(value, int): + if not isinstance(definition.index, int) or not check_write_data(parameters, field): LOGGER.warning( "%s: Parameter id '%s' or value '%s' invalid!", self._host, diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index c440559e..dae53bca 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -9,6 +9,30 @@ LOGGER = logging.getLogger(__name__) +############################################################################### +# Common functions +############################################################################### + + def check_write_data(data_vector, field): + """ + Returns true if the field is writable and the field data is valid. + + Args: + data_vector (DataVector): Data vector to which the field belongs + field (Base): The field object with the data to be written + + Returns: + bool: True if the data is writable, otherwise False. + """ + if field.writeable or not data_vector.safe: + if isinstance(field.raw, int): + return True + else: + LOGGER.error(f"Value of {data_vector.name} '{field.name}' invalid!") + else: + LOGGER.warning(f"{data_vector.name} '{field.name}' not safe for writing!") + return False + ############################################################################### # Base class for all luxtronik data vectors @@ -256,7 +280,7 @@ def get(self, def_field_name_or_idx, default=None): def set(self, def_field_name_or_idx, value): """ - Set field to new value. + Set the data of a field to the given value. The value is set, even if the field marked as non-writeable. No data validation is performed either. diff --git a/luxtronik/datatypes.py b/luxtronik/datatypes.py index ccf79224..56719a18 100755 --- a/luxtronik/datatypes.py +++ b/luxtronik/datatypes.py @@ -1,6 +1,7 @@ """datatype conversions.""" import datetime +import logging import socket import struct @@ -9,6 +10,9 @@ from functools import total_ordering +LOGGER = logging.getLogger(__name__) + + @total_ordering class Base: """Base datatype, no conversions.""" @@ -67,6 +71,8 @@ def value(self): def value(self, value): """Converts the value into heatpump units and store it.""" self._raw = self.to_heatpump(value) + if self._raw is None: + LOGGER.warning(f"Value '{value}' not valid for field '{self.name}'") self.write_pending = True @property @@ -76,7 +82,7 @@ def raw(self): @raw.setter def raw(self, raw): - """Store the raw data.""" + """Store the raw data. For internal use only""" self._raw = raw self.write_pending = False @@ -89,7 +95,7 @@ def __repr__(self): f"name: {self.name}, " f"writeable: {self.writeable}, " f"value: {self.value}, " - f"raw: {self._raw}, " + f"raw: {self.raw}, " f"write_pending: {self.write_pending}, " f"class: {self.datatype_class}, " f"unit: {self.datatype_unit}" @@ -111,7 +117,7 @@ def __eq__(self, other): return False return ( - self.value == other.value + self._raw == other._raw and self.datatype_class == other.datatype_class and self.datatype_unit == other.datatype_unit ) @@ -120,7 +126,7 @@ def __lt__(self, other): """Compares two datatype objects and returns which one contains the lower value""" return ( - self.value < other.value + self._raw < other._raw and self.datatype_class == other.datatype_class and self.datatype_unit == other.datatype_unit ) diff --git a/luxtronik/definitions/parameters.py b/luxtronik/definitions/parameters.py index ba1aa7d4..c8b7f62f 100644 --- a/luxtronik/definitions/parameters.py +++ b/luxtronik/definitions/parameters.py @@ -11529,7 +11529,7 @@ { "index": 1136, "count": 1, - "names": ['HEAT_ENERGY_INPUT'], + "names": ['HEAT_ENERGY_INPUT', 'Unknown_Parameter_1136'], "type": Energy, "writeable": False, "datatype": 'UINT32', @@ -11539,7 +11539,7 @@ { "index": 1137, "count": 1, - "names": ['DHW_ENERGY_INPUT'], + "names": ['DHW_ENERGY_INPUT', 'Unknown_Parameter_1137'], "type": Energy, "writeable": False, "datatype": 'UINT32', @@ -11559,7 +11559,7 @@ { "index": 1139, "count": 1, - "names": ['COOLING_ENERGY_INPUT'], + "names": ['COOLING_ENERGY_INPUT', 'Unknown_Parameter_1139'], "type": Energy, "writeable": False, "datatype": 'UINT32', @@ -11569,7 +11569,7 @@ { "index": 1140, "count": 1, - "names": ['SECOND_HEAT_GENERATOR_AMOUNT_COUNTER'], + "names": ['SECOND_HEAT_GENERATOR_AMOUNT_COUNTER', 'Unknown_Parameter_1140'], "type": Unknown, "writeable": False, "datatype": 'UINT32', @@ -11649,7 +11649,7 @@ { "index": 1148, "count": 1, - "names": ['HEATING_TARGET_TEMP_ROOM_THERMOSTAT'], + "names": ['HEATING_TARGET_TEMP_ROOM_THERMOSTAT', 'Unknown_Parameter_1148'], "type": Celsius, "writeable": True, "datatype": 'INT32', @@ -11749,7 +11749,7 @@ { "index": 1158, "count": 1, - "names": ['POWER_LIMIT_SWITCH'], + "names": ['POWER_LIMIT_SWITCH', 'Unknown_Parameter_1158'], "type": Unknown, "writeable": False, "datatype": 'UINT32', @@ -11759,7 +11759,7 @@ { "index": 1159, "count": 1, - "names": ['POWER_LIMIT_VALUE'], + "names": ['POWER_LIMIT_VALUE', 'Unknown_Parameter_1159'], "type": Unknown, "writeable": False, "datatype": 'UINT32', diff --git a/luxtronik/definitions/visibilities.py b/luxtronik/definitions/visibilities.py index a4a86451..502d05b6 100644 --- a/luxtronik/definitions/visibilities.py +++ b/luxtronik/definitions/visibilities.py @@ -3596,7 +3596,7 @@ { "index": 357, "count": 1, - "names": ['ELECTRICAL_POWER_LIMITATION_SWITCH', 'Unknown_Parameter_357'], + "names": ['ELECTRICAL_POWER_LIMITATION_SWITCH', 'Unknown_Visibility_357', 'Unknown_Parameter_357'], "type": Unknown, "writeable": False, "datatype": 'UINT32', diff --git a/luxtronik/shi/interface.py b/luxtronik/shi/interface.py index ab82c848..4187386d 100644 --- a/luxtronik/shi/interface.py +++ b/luxtronik/shi/interface.py @@ -5,6 +5,7 @@ from luxtronik.common import classproperty, version_in_range from luxtronik.collections import get_data_arr from luxtronik.datatypes import Base +from luxtronik.data_vector import check_write_data from luxtronik.definitions import ( LuxtronikDefinition, LuxtronikDefinitionsList, @@ -381,16 +382,14 @@ def _prepare_write_field(self, definition, field, safe, data): if not field.write_pending and data is None: return False - # Abort if field is not writeable - if safe and not (definition.writeable and field.writeable): - LOGGER.warning("Field marked as non-writeable: " \ - + f"name={definition.name}, data={field.raw}") - return False - # Override the field's data with the provided data if data is not None: field.value = data + # Abort if field is not writeable or the value is invalid + if check_write_data(field): + return False + # Abort if insufficient data is provided if not get_data_arr(definition, field, LUXTRONIK_SHI_REGISTER_BIT_SIZE): LOGGER.warning("Data error / insufficient data provided: " \ diff --git a/tests/test_LuxtronikData.py b/tests/test_LuxtronikData.py index f1e2bb62..39ca2e52 100644 --- a/tests/test_LuxtronikData.py +++ b/tests/test_LuxtronikData.py @@ -65,9 +65,9 @@ def test_get_firmware_version(self): @pytest.mark.parametrize("vector, index, names", [ - ("para", 1106, ["ID_Einst_SilenceTimer_13"]), - ("para", 1109, ["ID_Einst_SilenceTimer_16"]), - ("calc", 232, ["Vapourisation_Temperature"]), + ("para", 1106, ["ID_Einst_SilenceTimer_13", "Unknown_Parameter_1106"]), + ("para", 1109, ["ID_Einst_SilenceTimer_16", "Unknown_Parameter_1109"]), + ("calc", 232, ["Vapourisation_Temperature", "Unknown_Calculation_232"]), ("calc", 241, ["HUP_PWM", "Circulation_Pump"]), ("visi", 182, ["ID_Visi_Heizung_Zeitschaltprogramm", "ID_Visi_Heizung_Zeitschlaltprogramm"]), ("visi", 326, ["Unknown_Visibility_326"]), From 02669ec029a6b334b2854d04f346d0f661a8ec0f Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Thu, 5 Feb 2026 21:55:09 +0100 Subject: [PATCH 55/61] wip --- luxtronik/data_vector.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index dae53bca..23a2bf56 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -13,25 +13,25 @@ # Common functions ############################################################################### - def check_write_data(data_vector, field): - """ - Returns true if the field is writable and the field data is valid. +def check_write_data(data_vector, field): + """ + Returns true if the field is writable and the field data is valid. - Args: - data_vector (DataVector): Data vector to which the field belongs - field (Base): The field object with the data to be written + Args: + data_vector (DataVector): Data vector to which the field belongs + field (Base): The field object with the data to be written - Returns: - bool: True if the data is writable, otherwise False. - """ - if field.writeable or not data_vector.safe: - if isinstance(field.raw, int): - return True - else: - LOGGER.error(f"Value of {data_vector.name} '{field.name}' invalid!") + Returns: + bool: True if the data is writable, otherwise False. + """ + if field.writeable or not data_vector.safe: + if isinstance(field.raw, int): + return True else: - LOGGER.warning(f"{data_vector.name} '{field.name}' not safe for writing!") - return False + LOGGER.error(f"Value of {data_vector.name} '{field.name}' invalid!") + else: + LOGGER.warning(f"{data_vector.name} '{field.name}' not safe for writing!") + return False ############################################################################### From 1dfbc64a7dfad7455941e1e8af11a0dca57fed6b Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Thu, 5 Feb 2026 22:07:32 +0100 Subject: [PATCH 56/61] wip --- luxtronik/cfi/interface.py | 3 +-- luxtronik/data_vector.py | 24 ------------------------ luxtronik/datatypes.py | 20 ++++++++++++++++++++ luxtronik/shi/interface.py | 3 +-- 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/luxtronik/cfi/interface.py b/luxtronik/cfi/interface.py index 01344b45..2907d810 100644 --- a/luxtronik/cfi/interface.py +++ b/luxtronik/cfi/interface.py @@ -6,7 +6,6 @@ import time from luxtronik.common import LuxtronikSettings, get_host_lock -from luxtronik.data_vector import check_write_data from luxtronik.cfi.constants import ( LUXTRONIK_DEFAULT_PORT, LUXTRONIK_PARAMETERS_WRITE, @@ -167,7 +166,7 @@ def _write(self, parameters): if field.write_pending: field.write_pending = False value = field.raw - if not isinstance(definition.index, int) or not check_write_data(parameters, field): + 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, diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index 23a2bf56..efa2c41d 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -9,30 +9,6 @@ LOGGER = logging.getLogger(__name__) -############################################################################### -# Common functions -############################################################################### - -def check_write_data(data_vector, field): - """ - Returns true if the field is writable and the field data is valid. - - Args: - data_vector (DataVector): Data vector to which the field belongs - field (Base): The field object with the data to be written - - Returns: - bool: True if the data is writable, otherwise False. - """ - if field.writeable or not data_vector.safe: - if isinstance(field.raw, int): - return True - else: - LOGGER.error(f"Value of {data_vector.name} '{field.name}' invalid!") - else: - LOGGER.warning(f"{data_vector.name} '{field.name}' not safe for writing!") - return False - ############################################################################### # Base class for all luxtronik data vectors diff --git a/luxtronik/datatypes.py b/luxtronik/datatypes.py index 56719a18..732bd159 100755 --- a/luxtronik/datatypes.py +++ b/luxtronik/datatypes.py @@ -131,6 +131,26 @@ def __lt__(self, other): and self.datatype_unit == other.datatype_unit ) + def check_for_write(self, safe=True): + """ + Returns true if the field is writable and the field data is valid. + + Args: + safe (bool, Default: True): Flag for blocking write operations + if the field is not marked as writable + + Returns: + bool: True if the data is writable, otherwise False. + """ + if self.writeable or not safe: + if isinstance(self._raw, int): + return True + else: + LOGGER.error(f"Value of '{self.name}' invalid!") + else: + LOGGER.warning(f"'{self.name}' not safe for writing!") + return False + class SelectionBase(Base): """Selection base datatype, converts from and to list of codes.""" diff --git a/luxtronik/shi/interface.py b/luxtronik/shi/interface.py index 4187386d..39bc1cfa 100644 --- a/luxtronik/shi/interface.py +++ b/luxtronik/shi/interface.py @@ -5,7 +5,6 @@ from luxtronik.common import classproperty, version_in_range from luxtronik.collections import get_data_arr from luxtronik.datatypes import Base -from luxtronik.data_vector import check_write_data from luxtronik.definitions import ( LuxtronikDefinition, LuxtronikDefinitionsList, @@ -387,7 +386,7 @@ def _prepare_write_field(self, definition, field, safe, data): field.value = data # Abort if field is not writeable or the value is invalid - if check_write_data(field): + if field.check_for_write(safe): return False # Abort if insufficient data is provided From 3d8aa1072fc0117a56f6f403ac53d00891de7952 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Thu, 5 Feb 2026 22:11:53 +0100 Subject: [PATCH 57/61] wip --- luxtronik/shi/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luxtronik/shi/interface.py b/luxtronik/shi/interface.py index 39bc1cfa..202d6461 100644 --- a/luxtronik/shi/interface.py +++ b/luxtronik/shi/interface.py @@ -386,7 +386,7 @@ def _prepare_write_field(self, definition, field, safe, data): field.value = data # Abort if field is not writeable or the value is invalid - if field.check_for_write(safe): + if not field.check_for_write(safe): return False # Abort if insufficient data is provided From 6c31b3387e0ed341de06ddcc911ed86b51a97c2e Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Thu, 5 Feb 2026 22:16:36 +0100 Subject: [PATCH 58/61] wip --- tests/test_LuxtronikData.py | 6 +++--- tests/test_compatibility.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_LuxtronikData.py b/tests/test_LuxtronikData.py index 39ca2e52..f1e2bb62 100644 --- a/tests/test_LuxtronikData.py +++ b/tests/test_LuxtronikData.py @@ -65,9 +65,9 @@ def test_get_firmware_version(self): @pytest.mark.parametrize("vector, index, names", [ - ("para", 1106, ["ID_Einst_SilenceTimer_13", "Unknown_Parameter_1106"]), - ("para", 1109, ["ID_Einst_SilenceTimer_16", "Unknown_Parameter_1109"]), - ("calc", 232, ["Vapourisation_Temperature", "Unknown_Calculation_232"]), + ("para", 1106, ["ID_Einst_SilenceTimer_13"]), + ("para", 1109, ["ID_Einst_SilenceTimer_16"]), + ("calc", 232, ["Vapourisation_Temperature"]), ("calc", 241, ["HUP_PWM", "Circulation_Pump"]), ("visi", 182, ["ID_Visi_Heizung_Zeitschaltprogramm", "ID_Visi_Heizung_Zeitschlaltprogramm"]), ("visi", 326, ["Unknown_Visibility_326"]), diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index 357dc23b..97ae8679 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -1946,6 +1946,7 @@ def test_compatibilities(self): "ID_Visi_Heizung_Zeitschaltprogramm": (182, Unknown), "Unknown_Visibility_355": (355, Unknown), "Unknown_Visibility_356": (356, Unknown), + "Unknown_Visibility_357": (357, Unknown), "ELECTRICAL_POWER_LIMITATION_SWITCH": (357, Unknown), "Unknown_Visibility_358": (358, Unknown), "Unknown_Visibility_359": (359, Unknown), From d28f6a2401e3946f72867e19ccabeb58e61d5429 Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Thu, 5 Feb 2026 22:59:26 +0100 Subject: [PATCH 59/61] wip --- luxtronik/definitions/parameters.py | 28 ++++++++++++--- tests/test_compatibility.py | 56 +++++++++++++++++++++-------- 2 files changed, 65 insertions(+), 19 deletions(-) diff --git a/luxtronik/definitions/parameters.py b/luxtronik/definitions/parameters.py index c8b7f62f..dbdf8d56 100644 --- a/luxtronik/definitions/parameters.py +++ b/luxtronik/definitions/parameters.py @@ -11526,20 +11526,30 @@ "unit": '', "description": '', }, + { + "index": 1136, + "names": ['Unknown_Parameter_1136'], + "type": Unknown, + }, { "index": 1136, "count": 1, - "names": ['HEAT_ENERGY_INPUT', 'Unknown_Parameter_1136'], + "names": ['HEAT_ENERGY_INPUT'], "type": Energy, "writeable": False, "datatype": 'UINT32', "unit": 'kWh/10', "description": '', }, + { + "index": 1137, + "names": ['Unknown_Parameter_1137'], + "type": Unknown, + }, { "index": 1137, "count": 1, - "names": ['DHW_ENERGY_INPUT', 'Unknown_Parameter_1137'], + "names": ['DHW_ENERGY_INPUT'], "type": Energy, "writeable": False, "datatype": 'UINT32', @@ -11556,10 +11566,15 @@ "unit": '', "description": '', }, + { + "index": 1139, + "names": ['Unknown_Parameter_1139'], + "type": Unknown, + }, { "index": 1139, "count": 1, - "names": ['COOLING_ENERGY_INPUT', 'Unknown_Parameter_1139'], + "names": ['COOLING_ENERGY_INPUT'], "type": Energy, "writeable": False, "datatype": 'UINT32', @@ -11646,10 +11661,15 @@ "unit": '', "description": '', }, + { + "index": 1148, + "names": ['Unknown_Parameter_1148'], + "type": Unknown, + }, { "index": 1148, "count": 1, - "names": ['HEATING_TARGET_TEMP_ROOM_THERMOSTAT', 'Unknown_Parameter_1148'], + "names": ['HEATING_TARGET_TEMP_ROOM_THERMOSTAT'], "type": Celsius, "writeable": True, "datatype": 'INT32', diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index 97ae8679..30a922b8 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -84,6 +84,14 @@ class TestCompatibility: def test_compatibilities(self): """Test cases for compatibilities""" + # Structure of entries: + # old_name: (old_index, old_type[, type_intentionally_changed]) + # + # old_name: Previously used but still supported name + # old_index: Index assigned to this name + # type_intentionally_changed: We also check type changes. + # If this optional flag is set to True, you can skip the type check + paras = { # Status of 0.3.14: "ID_Transfert_LuxNet": (0, Unknown), @@ -1247,10 +1255,14 @@ def test_compatibilities(self): "Unknown_Parameter_1133": (1133, Unknown), "Unknown_Parameter_1134": (1134, Unknown), "Unknown_Parameter_1135": (1135, Unknown), + "Unknown_Parameter_1136": (1136, Unknown), "HEAT_ENERGY_INPUT": (1136, Energy), + "Unknown_Parameter_1137": (1137, Unknown), "DHW_ENERGY_INPUT": (1137, Energy), "Unknown_Parameter_1138": (1138, Unknown), + "Unknown_Parameter_1139": (1139, Unknown), "COOLING_ENERGY_INPUT": (1139, Energy), + "Unknown_Parameter_1140": (1140, Unknown), "SECOND_HEAT_GENERATOR_AMOUNT_COUNTER": (1140, Unknown), "Unknown_Parameter_1141": (1141, Unknown), "Unknown_Parameter_1142": (1142, Unknown), @@ -1259,6 +1271,7 @@ def test_compatibilities(self): "Unknown_Parameter_1145": (1145, Unknown), "Unknown_Parameter_1146": (1146, Unknown), "Unknown_Parameter_1147": (1147, Unknown), + "Unknown_Parameter_1148": (1148, Unknown), "HEATING_TARGET_TEMP_ROOM_THERMOSTAT": (1148, Celsius), "Unknown_Parameter_1149": (1149, Unknown), "Unknown_Parameter_1150": (1150, Unknown), @@ -1269,7 +1282,9 @@ def test_compatibilities(self): "Unknown_Parameter_1155": (1155, Unknown), "Unknown_Parameter_1156": (1156, Unknown), "Unknown_Parameter_1157": (1157, Unknown), + "Unknown_Parameter_1158": (1158, Unknown), "POWER_LIMIT_SWITCH": (1158, Unknown), + "Unknown_Parameter_1159": (1159, Unknown), "POWER_LIMIT_VALUE": (1159, Unknown), } @@ -1332,9 +1347,9 @@ def test_compatibilities(self): "ID_WEB_MZ2out": (54, Bool), "ID_WEB_MA2out": (55, Bool), "ID_WEB_Zaehler_BetrZeitVD1": (56, Seconds), - "ID_WEB_Zaehler_BetrZeitImpVD1": (57, Pulses), + "ID_WEB_Zaehler_BetrZeitImpVD1": (57, Pulses, True), # obsolete type -> type change allowed "ID_WEB_Zaehler_BetrZeitVD2": (58, Seconds), - "ID_WEB_Zaehler_BetrZeitImpVD2": (59, Pulses), + "ID_WEB_Zaehler_BetrZeitImpVD2": (59, Pulses, True), # obsolete type -> type change allowed "ID_WEB_Zaehler_BetrZeitZWE1": (60, Seconds), "ID_WEB_Zaehler_BetrZeitZWE2": (61, Seconds), "ID_WEB_Zaehler_BetrZeitZWE3": (62, Seconds), @@ -1357,10 +1372,10 @@ def test_compatibilities(self): "ID_WEB_BIV_Stufe_akt": (79, BivalenceLevel), "ID_WEB_WP_BZ_akt": (80, OperationMode), "ID_WEB_SoftStand": (81, Version), - "ID_WEB_AdresseIP_akt": (91, IPAddress), - "ID_WEB_SubNetMask_akt": (92, IPAddress), - "ID_WEB_Add_Broadcast": (93, IPAddress), - "ID_WEB_Add_StdGateway": (94, IPAddress), + "ID_WEB_AdresseIP_akt": (91, IPAddress, True), # obsolete type -> type change allowed + "ID_WEB_SubNetMask_akt": (92, IPAddress, True), # obsolete type -> type change allowed + "ID_WEB_Add_Broadcast": (93, IPAddress, True), # obsolete type -> type change allowed + "ID_WEB_Add_StdGateway": (94, IPAddress, True), # obsolete type -> type change allowed "ID_WEB_ERROR_Time0": (95, Timestamp), "ID_WEB_ERROR_Time1": (96, Timestamp), "ID_WEB_ERROR_Time2": (97, Timestamp), @@ -2131,9 +2146,12 @@ def test_compatibilities(self): obsolete_found = [] old_not_found = [] old_idx_wrong = [] - old_type_changed = [] + old_type_changed_info = [] + old_type_changed_err = [] - for old_name, (old_idx, old_type) in mapping.items(): + for old_name, (old_idx, old_type, *old_type_changed) in mapping.items(): + # using * old_type_changed is a list with all residual elements + old_type_changed = True if len(old_type_changed) > 0 and old_type_changed[0] else False # Try to get the definition of the "old name" try: @@ -2161,13 +2179,17 @@ def test_compatibilities(self): #if old_name != def_by_name.name: # new name available -> no error + # we allow type changes if we already use a name, but the type is still unknown + type_change_allowed = old_type_changed or (not old_name.lower().startswith("unknown_") and old_type == Unknown) if old_type != def_by_name.field_type: - old_type_changed.append(f"Type of {old_name} changed from {old_type.__name__} to {def_by_name.field_type.__name__}") + if type_change_allowed: + old_type_changed_info.append(f"Type of {old_name} changed from {old_type.__name__} to {def_by_name.field_type.__name__}") + else: + old_type_changed_err.append(f"Type of {old_name} changed from {old_type.__name__} to {def_by_name.field_type.__name__}") - # Currently we allow type changes ok = not obsolete_found and not old_not_found \ - and not old_idx_wrong # and not old_type_changed - do_print = not ok or len(old_type_changed) > 0 + and not old_idx_wrong and not old_type_changed_err + do_print = not ok or len(old_type_changed_info) > 0 if do_print: print(f"############################## Incompatibilities - {caption}:") @@ -2183,9 +2205,13 @@ def test_compatibilities(self): print("############################## idx wrong") for err in old_idx_wrong: print(err) - if old_type_changed: - print("############################## type changed") - for err in old_type_changed: + if old_type_changed_err: + print("############################## type change error") + for err in old_type_changed_err: + print(err) + if old_type_changed_info: + print("############################## type change info") + for err in old_type_changed_info: print(err) all_ok &= ok From 62a93087a47d2c48b52ff6f40c4de6d4d331c8ab Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Thu, 5 Feb 2026 23:03:57 +0100 Subject: [PATCH 60/61] wip --- tests/test_socket_interaction.py | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/test_socket_interaction.py b/tests/test_socket_interaction.py index 196b7c36..34e8ebdf 100644 --- a/tests/test_socket_interaction.py +++ b/tests/test_socket_interaction.py @@ -96,29 +96,29 @@ def test_luxtronik_socket_interface(self): # Finally, writing p = Parameters() - p[0].raw = 100 - p[0].write_pending = True - p[1].raw = 200 + p[1].raw = 100 p[1].write_pending = True + p[2].raw = 200 + p[2].write_pending = True lux.write(p) s = FakeSocket.last_instance - assert s.written_values[0] == 100 - assert s.written_values[1] == 200 - assert not p[0].write_pending + assert s.written_values[1] == 100 + assert s.written_values[2] == 200 assert not p[1].write_pending + assert not p[2].write_pending p = Parameters() - p[2].raw = 300 - p[2].write_pending = True - p[3].raw = "test" + p[3].raw = 300 p[3].write_pending = True + p[4].raw = "test" + p[4].write_pending = True d = lux.write_and_read(p) s = FakeSocket.last_instance - assert s.written_values[2] == 300 + assert s.written_values[3] == 300 # Make sure that the non-int value is not written: - assert 3 not in s.written_values - assert not p[2].write_pending + assert 4 not in s.written_values assert not p[3].write_pending + assert not p[4].write_pending assert self.check_luxtronik_data(d) def test_luxtronik(self): @@ -149,18 +149,18 @@ def test_luxtronik(self): ########################## # Test the write routine # ########################## - lux.parameters[0].raw = 500 - lux.parameters[0].write_pending = True + lux.parameters[1].raw = 500 + lux.parameters[1].write_pending = True lux.write() s = FakeSocket.last_instance - assert s.written_values[0] == 500 + assert s.written_values[1] == 500 p = Parameters() - p[1].raw = 501 - p[1].write_pending = True + p[2].raw = 501 + p[2].write_pending = True lux.write(p) s = FakeSocket.last_instance - assert s.written_values[1] == 501 + assert s.written_values[2] == 501 # lux.write() and lux.write(p) should not read: assert self.check_luxtronik_data(lux, False) @@ -168,24 +168,24 @@ def test_luxtronik(self): ################################### # Test the write_and_read routine # ################################### - lux.parameters[2].raw = 502 - lux.parameters[2].write_pending = True + lux.parameters[3].raw = 502 + lux.parameters[3].write_pending = True lux.write_and_read() # Currently write_and_read triggers two separate connections/operations s = FakeSocket.prev_instance - assert s.written_values[2] == 502 + assert s.written_values[3] == 502 # Now, the values should be read assert self.check_luxtronik_data(lux) self.clear_luxtronik_data(lux) - p[3].raw = 503 - p[3].write_pending = True + p[4].raw = 503 + p[4].write_pending = True lux.write_and_read(p) # Currently write_and_read triggers two separate connections/operations s = FakeSocket.prev_instance - assert s.written_values[3] == 503 + assert s.written_values[4] == 503 # Now, the values should be read assert self.check_luxtronik_data(lux) From 799fdf07b1d305e637ba5627574af7a781cb3ada Mon Sep 17 00:00:00 2001 From: Guzz-T Date: Mon, 16 Feb 2026 18:42:20 +0100 Subject: [PATCH 61/61] wip --- luxtronik/cfi/interface.py | 3 +++ luxtronik/datatypes.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/luxtronik/cfi/interface.py b/luxtronik/cfi/interface.py index 2907d810..f2b2bea8 100644 --- a/luxtronik/cfi/interface.py +++ b/luxtronik/cfi/interface.py @@ -162,6 +162,9 @@ def _write_and_read(self, parameters, data): return self._read(data) def _write(self, parameters): + 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 diff --git a/luxtronik/datatypes.py b/luxtronik/datatypes.py index 732bd159..b0e29312 100755 --- a/luxtronik/datatypes.py +++ b/luxtronik/datatypes.py @@ -143,8 +143,12 @@ def check_for_write(self, safe=True): bool: True if the data is writable, otherwise False. """ if self.writeable or not safe: + # We support integers if isinstance(self._raw, int): return True + # and list of integers + elif isinstance(self._raw, list) and all(isinstance(value, int) for value in self._raw): + return True else: LOGGER.error(f"Value of '{self.name}' invalid!") else: