Skip to content
69 changes: 66 additions & 3 deletions luxtronik/cfi/vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,71 @@
class DataVectorConfig(DataVector):
"""Specialized DataVector for Luxtronik configuration fields."""

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

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

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

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

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

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

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

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

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

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

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

# Add a (new) field
if field is None:
field = definition.create_field()
self._data.add_sorted(definition, field, alias)
return field
4 changes: 0 additions & 4 deletions luxtronik/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,3 @@
# Since version 3.92.0, all unavailable 16 bit signed data fields
# have been returning this value (0x7FFF)
LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE: Final = 32767

LUXTRONIK_NAME_CHECK_NONE: Final = "none"
LUXTRONIK_NAME_CHECK_PREFERRED: Final = "preferred"
LUXTRONIK_NAME_CHECK_OBSOLETE: Final = "obsolete"
244 changes: 183 additions & 61 deletions luxtronik/data_vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@

import logging

from luxtronik.constants import (
LUXTRONIK_NAME_CHECK_PREFERRED,
LUXTRONIK_NAME_CHECK_OBSOLETE,
)

from luxtronik.collections import LuxtronikFieldsDictionary
from luxtronik.datatypes import Base
from luxtronik.datatypes import Base, Unknown
from luxtronik.definitions import LuxtronikDefinition


LOGGER = logging.getLogger(__name__)
Expand All @@ -19,15 +15,104 @@
###############################################################################

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
for this vector) or directly in the `LuxtronikDefinitionsList`
(globally = valid for all newly created vector).
"""

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, 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:
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.
"""
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

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:
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.
"""
definition, _ = self._get_definition(def_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_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):
Expand Down Expand Up @@ -86,73 +171,110 @@ def items(self):
"""
return iter(self._data.items())

def _name_lookup(self, name):

# Alias methods ###############################################################

def register_alias(self, def_field_name_or_idx, alias):
"""
Forward the `LuxtronikFieldsDictionary.register_alias` method.
Please check its documentation.
"""
return self._data.register_alias(def_field_name_or_idx, alias)


# Get and set methods #########################################################

def _get_definition(self, def_field_name_or_idx, all_not_version_dependent):
"""
Try to find the index using the given field name.
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:
name (string): Field name.
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[int | None, str | None]:
0: Index found or None
1: New preferred name, if available, otherwise None
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
"""
obsolete_entry = self._obsolete.get(name, None)
if obsolete_entry:
return None, obsolete_entry
for definition, field in self._data.items():
check_result = field.check_name(name)
if check_result == LUXTRONIK_NAME_CHECK_PREFERRED:
return definition.index, None
elif check_result == LUXTRONIK_NAME_CHECK_OBSOLETE:
return definition.index, field.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
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_field_name_or_idx, default=None):
"""
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_field_name_or_idx (LuxtronikDefinition | Base | str | int):
Definition, name, or register index to be used to search for the field.

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
Returns:
Base | None: The field found or the provided default if not found.

def get(self, target):
"""Get entry by id or name."""
entry = self._lookup(target)
return entry
Note:
If multiple fields added for the same index/name,
the last added takes precedence.
"""
# check for obsolete
obsolete_entry = self._obsolete.get(def_field_name_or_idx, None)
if obsolete_entry:
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_field_name_or_idx}' not found")
return field

def set(self, target, value):
def set(self, def_field_name_or_idx, value):
"""
Set the value of a field to the given 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.

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.
It is also possible to pass the field itself.
value (int | List[int]): Value to set
"""
field = target
field = def_field_name_or_idx
if not isinstance(field, Base):
field = self.get(target)
field = self.get(def_field_name_or_idx)
if field is not None:
field.value = value
Loading