diff --git a/luxtronik/datatypes.py b/luxtronik/datatypes.py index 90e992f..d883435 100755 --- a/luxtronik/datatypes.py +++ b/luxtronik/datatypes.py @@ -20,6 +20,12 @@ class Base: datatype_class = None datatype_unit = None + # If True, multiple data chucks should be combined externally into a single value. + # Otherwise, they are passed on "raw" as they are (list of chunks). + # Currently only necessary for the smart home interface, + # as 4-byte values are always transferred via the normal socket interface. + concatenate_multiple_data_chunks = True + def __init__(self, names, writeable=False): """Initialize the base data field class. Set the initial raw value to None""" # save the raw value only since the user value @@ -146,6 +152,8 @@ def options(cls): @classmethod def from_heatpump(cls, value): + if value is None: + return None if value in cls.codes: return cls.codes.get(value) return f"{cls.unknown_prefix}{cls.unknown_delimiter}{value}" @@ -155,29 +163,117 @@ def to_heatpump(cls, value): for index, code in cls.codes.items(): if code == value: return index - if value.startswith(cls.unknown_prefix): + if isinstance(value, str) and value.startswith(cls.unknown_prefix): return int(value.split(cls.unknown_delimiter)[1]) + if isinstance(value, (int, float)) or (isinstance(value, str) and value.isdigit()): + return int(value) return None +class BitMaskBase(Base): + + datatype_class = "bitmask" + unknown_prefix = "Unknown" + unknown_delimiter = "_" + + # Dictionary with the bit-index as key (2 means bit-index 2, 0b100 in binary notation) + bit_values = {} + value_zero = "None" + value_delim = ", " + values_postfix = "" + + @classmethod + def bits(cls): + """Return list of all available bits.""" + return [value for _, value in cls.bit_values.items()] + + @classmethod + def _get_unknown(cls, bit_index): + return f"{cls.unknown_prefix}{cls.unknown_delimiter}{bit_index}" + + @classmethod + def _get_bit_value(cls, bit_index): + if bit_index in cls.bit_values: + return f"{cls.bit_values[bit_index]}" + else: + return cls._get_unknown(bit_index) + + @classmethod + def from_heatpump(cls, value): + if not isinstance(value, int): + return None + # Check for zero + if value == 0: + return cls.value_zero + # We support up to 32 bits + result = [] + for bit_index in range(0, 32): + if value & (1 << bit_index): + bit_value = cls._get_bit_value(bit_index) + result.append(bit_value) + result = cls.value_delim.join(result) + # Add postfix + return result + cls.values_postfix + + @classmethod + def to_heatpump(cls, value): + if not isinstance(value, str) or not value: + return None + # Remove postfix and split + if cls.values_postfix and value.endswith(cls.values_postfix): + value = value[0:-len(cls.values_postfix)] + # Check for zero + if value == cls.value_zero: + return 0 + # We support up to 32 bits + values = value.split(cls.value_delim) + raw = 0 + count = 0 + for bit_index in range(0, 32): + bit_value = cls._get_bit_value(bit_index) + if bit_value in values: + raw += 1 << bit_index + count += 1 + if count != len(values): + return None + return raw + + class ScalingBase(Base): """Scaling base datatype, converts via a scaling factor.""" datatype_class = "scaling" + data_width = 32 # bits + data_type = "signed" + scaling_factor = 1 + def __init_subclass__(cls): + super().__init_subclass__() + cls.num_values = (1 << cls.data_width) + num_unsigned_bits = cls.data_width - 1 if cls.data_type == "signed" else cls.data_width + cls.max_value = (1 << num_unsigned_bits) - 1 + @classmethod def from_heatpump(cls, value): - if value is None: + if not isinstance(value, int): return None + while cls.data_type == "signed" and value > cls.max_value: + # correction for negative numbers + value -= cls.num_values value = value * cls.scaling_factor return value @classmethod def to_heatpump(cls, value): - raw = round(float(value) / cls.scaling_factor) - return raw + # Limitations due to the data_width are handled automatically. + # No need to add additional code here. + try: + raw = round(float(value) / cls.scaling_factor) + return raw + except Exception: + return None class Celsius(ScalingBase): @@ -188,6 +284,19 @@ class Celsius(ScalingBase): scaling_factor = 0.1 +class CelsiusInt16(Celsius): + """Celsius 16-bit signed, converts from and to Celsius.""" + + data_width = 16 + + +class CelsiusUInt16(Celsius): + """Celsius 16-bit unsigned, converts from and to Celsius.""" + + data_width = 16 + data_type = "unsigned" + + class Bool(Base): """Boolean datatype, converts from and to Boolean.""" @@ -254,6 +363,7 @@ class Errorcode(SelectionBase): datatype_class = "errorcode" codes = { + 0: "no error", 700: "sensor external return", 701: "error low pressure", 702: "low pressure blockade", @@ -360,6 +470,12 @@ class Kelvin(ScalingBase): scaling_factor = 0.1 +class KelvinInt16(Kelvin): + """Kelvin 16-bit signed, converts from and to Kelvin.""" + + data_width = 16 + + class Pressure(ScalingBase): """Pressure datatype, converts from and to Pressure.""" @@ -499,8 +615,6 @@ class Icon(Base): class HeatingMode(SelectionBase): """HeatingMode datatype, converts from and to list of HeatingMode codes.""" - datatype_class = "selection" - codes = { 0: "Automatic", 1: "Second heatsource", @@ -513,16 +627,12 @@ class HeatingMode(SelectionBase): class CoolingMode(SelectionBase): """CoolingMode datatype, converts from and to list of CoolingMode codes.""" - datatype_class = "selection" - codes = {0: "Off", 1: "Automatic"} class HotWaterMode(SelectionBase): """HotWaterMode datatype, converts from and to list of HotWaterMode codes.""" - datatype_class = "selection" - codes = { 0: "Automatic", 1: "Second heatsource", @@ -535,15 +645,11 @@ class HotWaterMode(SelectionBase): class PoolMode(SelectionBase): """PoolMode datatype, converts from and to list of PoolMode codes.""" - datatype_class = "selection" - codes = {0: "Automatic", 2: "Party", 3: "Holidays", 4: "Off"} class MixedCircuitMode(SelectionBase): - """MixCircuitMode datatype, converts from and to list of MixCircuitMode codes.""" - - datatype_class = "selection" + """MixedCircuitMode datatype, converts from and to list of MixedCircuitMode codes.""" codes = {0: "Automatic", 2: "Party", 3: "Holidays", 4: "Off"} @@ -551,8 +657,6 @@ class MixedCircuitMode(SelectionBase): class SolarMode(SelectionBase): """SolarMode datatype, converts from and to list of SolarMode codes.""" - datatype_class = "selection" - codes = { 0: "Automatic", 1: "Second heatsource", @@ -565,16 +669,12 @@ class SolarMode(SelectionBase): class VentilationMode(SelectionBase): """VentilationMode datatype, converts from and to list of VentilationMode codes.""" - datatype_class = "selection" - codes = {0: "Automatic", 1: "Party", 2: "Holidays", 3: "Off"} class HeatpumpCode(SelectionBase): """HeatpumpCode datatype, converts from and to list of Heatpump codes.""" - datatype_class = "selection" - codes = { 0: "ERC", 1: "SW1", @@ -671,8 +771,6 @@ class HeatpumpCode(SelectionBase): class BivalenceLevel(SelectionBase): """BivalanceLevel datatype, converts from and to list of BivalanceLevel codes.""" - datatype_class = "selection" - codes = { 1: "one compressor allowed to run", 2: "two compressors allowed to run", @@ -683,8 +781,6 @@ class BivalenceLevel(SelectionBase): class OperationMode(SelectionBase): """OperationMode datatype, converts from and to list of OperationMode codes.""" - datatype_class = "selection" - codes = { 0: "heating", 1: "hot water", @@ -700,8 +796,6 @@ class OperationMode(SelectionBase): class SwitchoffFile(SelectionBase): """SwitchOff datatype, converts from and to list of SwitchOff codes.""" - datatype_class = "selection" - codes = { 0: "heatpump error", 1: "system error", @@ -734,8 +828,6 @@ class SwitchoffFile(SelectionBase): class MainMenuStatusLine1(SelectionBase): """MenuStatusLine datatype, converts from and to list of MenuStatusLine codes.""" - datatype_class = "selection" - codes = { 0: "heatpump running", 1: "heatpump idle", @@ -751,16 +843,12 @@ class MainMenuStatusLine1(SelectionBase): class MainMenuStatusLine2(SelectionBase): """MenuStatusLine datatype, converts from and to list of MenuStatusLine codes.""" - datatype_class = "selection" - codes = {0: "since", 1: "in"} class MainMenuStatusLine3(SelectionBase): """MenuStatusLine datatype, converts from and to list of MenuStatusLine codes.""" - datatype_class = "selection" - codes = { 0: "heating", 1: "no request", @@ -784,8 +872,6 @@ class MainMenuStatusLine3(SelectionBase): class SecOperationMode(SelectionBase): """SecOperationMode datatype, converts from and to list of SecOperationMode codes.""" - datatype_class = "selection" - codes = { 0: "off", 1: "cooling", @@ -806,8 +892,6 @@ class SecOperationMode(SelectionBase): class AccessLevel(SelectionBase): """AccessLevel datatype, converts from and to list of AccessLevel codes""" - datatype_class = "selection" - codes = { 0: "user", 1: "after sales service", @@ -819,8 +903,6 @@ class AccessLevel(SelectionBase): class TimerProgram(SelectionBase): """TimerProgram datatype, converts from and to list of TimerProgram codes""" - datatype_class = "selection" - codes = { 0: "week", 1: "5+2", @@ -883,20 +965,23 @@ def to_heatpump(cls, value): return val -class HeatPumpState(SelectionBase): - """HeatPumpState datatype, converts from and to list of HeatPumpState codes.""" +class HeatPumpStatus(BitMaskBase): + """HeatPumpStatus datatype, converts from and to list of HeatPumpStatus codes.""" - datatype_class = "selection" - - codes = { - 0: "Idle", # Heatpump is idle - 1: "Running", # Heatpump is running + bit_values = { + 0: "VD1", + 1: "VD2", + 2: "ZWE1", + 3: "ZWE2", + 4: "ZWE3", } + value_zero = "Idle" + value_delim = ", " + values_postfix = " running" -class ModeState(SelectionBase): - """ModeState datatype, converts from and to list of ModeState codes.""" - datatype_class = "selection" +class ModeStatus(SelectionBase): + """ModeStatus datatype, converts from and to list of ModeStatus codes.""" codes = { 0: "Disabled", # Heating / Hot water is disabled @@ -908,8 +993,6 @@ class ModeState(SelectionBase): class ControlMode(SelectionBase): """ControlMode datatype, converts from and to list of ControlMode codes.""" - datatype_class = "selection" - codes = { 0: "Off", # System value is used 1: "Setpoint", # Setpoint register value is used @@ -922,8 +1005,6 @@ class ControlMode(SelectionBase): class LpcMode(SelectionBase): """LpcMode datatype, converts from and to list of LpcMode codes.""" - datatype_class = "selection" - codes = { 0: "No limit", 1: "Soft limit", @@ -934,8 +1015,6 @@ class LpcMode(SelectionBase): class LockMode(SelectionBase): """LockMode datatype, converts from and to list of LockMode codes.""" - datatype_class = "selection" - codes = { 0: "Off", # Function is not locked 1: "On", # Function is locked @@ -944,18 +1023,15 @@ class LockMode(SelectionBase): class OnOffMode(SelectionBase): """OnOffMode datatype, converts from and to list of OnOffMode codes.""" - datatype_class = "selection" - codes = { 0: "Off", # Function deactivated 1: "On", # Function activated } + class LevelMode(SelectionBase): """LevelMode datatype, converts from and to list of LevelMode codes.""" - datatype_class = "selection" - codes = { 0: "Normal", # No correction 1: "Increased", # Increase the temperature by the values @@ -969,7 +1045,18 @@ class LevelMode(SelectionBase): # TODO: Function unknown – requires further analysis } -class PowerLimit(ScalingBase): + +class BufferType(SelectionBase): + """BufferType datatype, converts from and to list of BufferType codes.""" + + codes = { + 0: "series buffer", + 1: "separation buffer", + 2: "multifunction buffer", + } + + +class PowerKW(ScalingBase): """PowerLimit datatype, converts from and to PowerLimit.""" datatype_class = "power" @@ -981,6 +1068,7 @@ class FullVersion(Base): """FullVersion datatype, converts from and to a RBEVersion""" datatype_class = "version" + concatenate_multiple_data_chunks = False @classmethod def from_heatpump(cls, value): diff --git a/luxtronik/definitions/holdings.py b/luxtronik/definitions/holdings.py index 2f10fb4..1d2b398 100644 --- a/luxtronik/definitions/holdings.py +++ b/luxtronik/definitions/holdings.py @@ -5,18 +5,20 @@ Unlike the setting registers, these SHI register are volatile and intended for communication with smart home systems. 'Holding' registers are readable and writable and are used to control the heat pump externally. + +NOTE: Data fields that span multiple registers are typically in big-endian/MSB-first order. """ from typing import Final from luxtronik.datatypes import ( - Celsius, + CelsiusUInt16, ControlMode, - Kelvin, + KelvinInt16, LevelMode, LockMode, LpcMode, OnOffMode, - PowerLimit, + PowerKW, Unknown, ) @@ -31,30 +33,45 @@ "names": ["heating_mode"], "type": ControlMode, "writeable": True, + "datatype": "UINT16", + "unit": "enum", + "default": 0, + "range": {"min": 0, "max": 3}, "since": "3.90.1", - "description": "Operating mode of the heating function", + "description": "Configuration for heating operation \ +0: no influence \ +1: Heating setpoint \ +2: Heating offset \ +3: Heating level" }, { "index": 1, "count": 1, "names": ["heating_setpoint"], - "type": Celsius, + "type": CelsiusUInt16, "writeable": True, - "range": {"min": 15, "max": 75}, + "datatype": "UINT16", + "unit": "°C/10", + "default": 350, + "range": {"min": 150, "max": 750}, "since": "3.90.1", - "description": "Desired target temperature in °C " \ - "for the heating function", + "description": "Overrides the current return temperature setpoint (tRL) for heating. \ +Value may be limited by heat pump controller settings. \ +Requires heating_mode = setpoint to apply." }, { "index": 2, "count": 1, "names": ["heating_offset"], - "type": Kelvin, + "type": KelvinInt16, "writeable": True, - "range": {"min": 0, "max": 20}, + "datatype": "INT16", + "unit": "K/10", + "default": 0, + "range": {"min": -200, "max": 200}, "since": "3.90.1", - "description": "Temperature correction in Kelvin " \ - "for the heating function", + "description": "Offset applied to the current return temperature setpoint (tRL) for heating. \ +Requires heating_mode = offset to apply." }, { "index": 3, @@ -69,38 +86,53 @@ { "index": 5, "count": 1, - "names": ["hot_water_mode"], + "names": ["hot_water_mode", "dhw_mode"], "type": ControlMode, "writeable": True, + "datatype": "UINT16", + "unit": "enum", + "default": 0, + "range": {"min": 0, "max": 3}, "since": "3.90.1", - "description": "Operating mode of the hot water system", + "description": "Configuration for domestic hot water operation \ +0: no influence \ +1: DHW setpoint \ +2: DHW offset \ +3: DHW level" }, { "index": 6, "count": 1, - "names": ["hot_water_setpoint"], - "type": Celsius, + "names": ["hot_water_setpoint", "dhw_setpoint"], + "type": CelsiusUInt16, "writeable": True, - "range": {"min": 30, "max": 75}, + "datatype": "UINT16", + "unit": "°C/10", + "default": 400, + "range": {"min": 300, "max": 750}, "since": "3.90.1", - "description": "Desired target temperature in °C " \ - "for hot water", + "description": "Overrides the current DHW setpoint. \ +Value may be limited by heat pump controller settings. \ +Requires dhw_mode = setpoint to apply." }, { "index": 7, "count": 1, - "names": ["hot_water_offset"], - "type": Kelvin, + "names": ["hot_water_offset", "dhw_offset"], + "type": KelvinInt16, "writeable": True, - "range": {"min": 0, "max": 20}, + "datatype": "INT16", + "unit": "K/10", + "default": 0, + "range": {"min": -200, "max": 200}, "since": "3.90.1", - "description": "Temperature correction in Kelvin " \ - "for hot water", + "description": "Offset applied to the current DHW setpoint. \ +Requires dhw_mode = offset to apply." }, { "index": 8, "count": 1, - "names": ["hot_water_level"], + "names": ["hot_water_level", "dhw_level"], "type": LevelMode, "writeable": True, "since": "3.92.0", @@ -113,30 +145,45 @@ "names": ["mc1_heat_mode"], "type": ControlMode, "writeable": True, + "datatype": "UINT16", + "unit": "enum", + "default": 0, + "range": {"min": 0, "max": 3}, "since": "3.90.1", - "description": "Operating mode for mixing circuit 1 in heating mode", + "description": "Configuration for mixing circuit 1 heating operation \ +0: no influence \ +1: Heating setpoint \ +2: Heating offset \ +3: Heating level" }, { "index": 11, "count": 1, "names": ["mc1_heat_setpoint"], - "type": Celsius, + "type": CelsiusUInt16, "writeable": True, - "range": {"min": 20, "max": 65}, + "datatype": "UINT16", + "unit": "°C/10", + "default": 350, + "range": {"min": 200, "max": 650}, "since": "3.90.1", - "description": "Desired target temperature in °C " \ - "for mixing circuit 1 in heating mode", + "description": "Overrides the current flow temperature for mixing circuit 1 heating. \ +Value may be limited by heat pump controller settings. \ +Requires mc1_heat_mode = setpoint to apply." }, { "index": 12, "count": 1, "names": ["mc1_heat_offset"], - "type": Kelvin, + "type": KelvinInt16, "writeable": True, - "range": {"min": 0, "max": 5}, + "datatype": "INT16", + "unit": "K/10", + "default": 0, + "range": {"min": -50, "max": 50}, "since": "3.90.1", - "description": "Temperature correction in Kelvin " \ - "for mixing circuit 1 in heating mode", + "description": "Offset applied to the current flow temperature for mixing circuit 1 heating. \ +Requires mc1_heat_mode = offset to apply." }, { "index": 13, @@ -154,30 +201,45 @@ "names": ["mc1_cool_mode"], "type": ControlMode, "writeable": True, + "datatype": "UINT16", + "unit": "enum", + "default": 0, + "range": {"min": 0, "max": 3}, "since": "3.90.1", - "description": "Operating mode for mixing circuit 1 in cooling mode", + "description": "Configuration for mixing circuit 1 cooling operation \ +0: no influence \ +1: Cooling setpoint \ +2: Cooling offset \ +3: Cooling level" }, { "index": 16, "count": 1, "names": ["mc1_cool_setpoint"], - "type": Celsius, + "type": CelsiusUInt16, "writeable": True, - "range": {"min": 5, "max": 25}, + "datatype": "UINT16", + "unit": "°C/10", + "default": 200, + "range": {"min": 50, "max": 250}, "since": "3.90.1", - "description": "Desired target temperature in °C " \ - "for mixing circuit 1 in cooling mode", + "description": "Overrides the current flow temperature for mixing circuit 1 cooling. \ +Value may be limited by heat pump controller settings. \ +Requires mc1_cool_mode = setpoint to apply." }, { "index": 17, "count": 1, "names": ["mc1_cool_offset"], - "type": Kelvin, + "type": KelvinInt16, "writeable": True, - "range": {"min": 0, "max": 5}, + "datatype": "INT16", + "unit": "K/10", + "default": 0, + "range": {"min": -50, "max": 50}, "since": "3.90.1", - "description": "Temperature correction in Kelvin " \ - "for mixing circuit 1 in cooling mode", + "description": "Offset applied to the current flow temperature for mixing circuit 1 cooling. \ +Requires mc1_cool_mode = offset to apply." }, { "index": 20, @@ -185,30 +247,45 @@ "names": ["mc2_heat_mode"], "type": ControlMode, "writeable": True, + "datatype": "UINT16", + "unit": "enum", + "default": 0, + "range": {"min": 0, "max": 3}, "since": "3.90.1", - "description": "Operating mode for mixing circuit 2 in heating mode", + "description": "Configuration for mixing circuit 2 heating operation \ +0: no influence \ +1: Heating setpoint \ +2: Heating offset \ +3: Heating level" }, { "index": 21, "count": 1, "names": ["mc2_heat_setpoint"], - "type": Celsius, + "type": CelsiusUInt16, "writeable": True, - "range": {"min": 20, "max": 65}, + "datatype": "UINT16", + "unit": "°C/10", + "default": 350, + "range": {"min": 200, "max": 650}, "since": "3.90.1", - "description": "Desired target temperature in °C " \ - "for mixing circuit 2 in heating mode", + "description": "Overrides the current flow temperature for mixing circuit 2 heating. \ +Value may be limited by heat pump controller settings. \ +Requires mc2_heat_mode = setpoint to apply." }, { "index": 22, "count": 1, "names": ["mc2_heat_offset"], - "type": Kelvin, + "type": KelvinInt16, "writeable": True, - "range": {"min": 0, "max": 5}, + "datatype": "INT16", + "unit": "K/10", + "default": 0, + "range": {"min": -50, "max": 50}, "since": "3.90.1", - "description": "Temperature correction in Kelvin " \ - "for mixing circuit 2 in heating mode", + "description": "Offset applied to the current flow temperature for mixing circuit 2 heating. \ +Requires mc2_heat_mode = offset to apply." }, { "index": 23, @@ -226,30 +303,45 @@ "names": ["mc2_cool_mode"], "type": ControlMode, "writeable": True, + "datatype": "UINT16", + "unit": "enum", + "default": 0, + "range": {"min": 0, "max": 3}, "since": "3.90.1", - "description": "Operating mode for mixing circuit 2 in cooling mode", + "description": "Configuration for mixing circuit 2 cooling operation \ +0: no influence \ +1: Cooling setpoint \ +2: Cooling offset \ +3: Cooling level" }, { "index": 26, "count": 1, "names": ["mc2_cool_setpoint"], - "type": Celsius, + "type": CelsiusUInt16, "writeable": True, - "range": {"min": 5, "max": 25}, + "datatype": "UINT16", + "unit": "°C/10", + "default": 200, + "range": {"min": 50, "max": 250}, "since": "3.90.1", - "description": "Desired target temperature in °C " \ - "for mixing circuit 2 in cooling mode", + "description": "Overrides the current flow temperature for mixing circuit 2 cooling. \ +Value may be limited by heat pump controller settings. \ +Requires mc2_cool_mode = setpoint to apply." }, { "index": 27, "count": 1, "names": ["mc2_cool_offset"], - "type": Kelvin, + "type": KelvinInt16, "writeable": True, - "range": {"min": 0, "max": 5}, + "datatype": "INT16", + "unit": "K/10", + "default": 0, + "range": {"min": -50, "max": 50}, "since": "3.90.1", - "description": "Temperature correction in Kelvin " \ - "for mixing circuit 2 in cooling mode", + "description": "Offset applied to the current flow temperature for mixing circuit 2 cooling. \ +Requires mc2_cool_mode = offset to apply." }, { "index": 30, @@ -257,30 +349,45 @@ "names": ["mc3_heat_mode"], "type": ControlMode, "writeable": True, + "datatype": "UINT16", + "unit": "enum", + "default": 0, + "range": {"min": 0, "max": 3}, "since": "3.90.1", - "description": "Operating mode for mixing circuit 3 in heating mode", + "description": "Configuration for mixing circuit 3 heating operation \ +0: no influence \ +1: Heating setpoint \ +2: Heating offset \ +3: Heating level" }, { "index": 31, "count": 1, "names": ["mc3_heat_setpoint"], - "type": Celsius, + "type": CelsiusUInt16, "writeable": True, - "range": {"min": 20, "max": 65}, + "datatype": "UINT16", + "unit": "°C/10", + "default": 350, + "range": {"min": 200, "max": 650}, "since": "3.90.1", - "description": "Desired target temperature in °C " \ - "for mixing circuit 3 in heating mode", + "description": "Overrides the current flow temperature for mixing circuit 3 heating. \ +Value may be limited by heat pump controller settings. \ +Requires mc3_heat_mode = setpoint to apply." }, { "index": 32, "count": 1, "names": ["mc3_heat_offset"], - "type": Kelvin, + "type": KelvinInt16, "writeable": True, - "range": {"min": 0, "max": 5}, + "datatype": "INT16", + "unit": "K/10", + "default": 0, + "range": {"min": -50, "max": 50}, "since": "3.90.1", - "description": "Temperature correction in Kelvin " \ - "for mixing circuit 3 in heating mode", + "description": "Offset applied to the current flow temperature for mixing circuit 3 heating. \ +Requires mc3_heat_mode = offset to apply." }, { "index": 33, @@ -298,30 +405,45 @@ "names": ["mc3_cool_mode"], "type": ControlMode, "writeable": True, + "datatype": "UINT16", + "unit": "enum", + "default": 0, + "range": {"min": 0, "max": 3}, "since": "3.90.1", - "description": "Operating mode for mixing circuit 3 in cooling mode", + "description": "Configuration for mixing circuit 3 cooling operation \ +0: no influence \ +1: Cooling setpoint \ +2: Cooling offset \ +3: Cooling level" }, { "index": 36, "count": 1, "names": ["mc3_cool_setpoint"], - "type": Celsius, + "type": CelsiusUInt16, "writeable": True, - "range": {"min": 5, "max": 25}, + "datatype": "UINT16", + "unit": "°C/10", + "default": 200, + "range": {"min": 50, "max": 250}, "since": "3.90.1", - "description": "Desired target temperature in °C " \ - "for mixing circuit 3 in cooling mode", + "description": "Overrides the current flow temperature for mixing circuit 3 cooling. \ +Value may be limited by heat pump controller settings. \ +Requires mc3_cool_mode = setpoint to apply." }, { "index": 37, "count": 1, "names": ["mc3_cool_offset"], - "type": Kelvin, + "type": KelvinInt16, "writeable": True, - "range": {"min": 0, "max": 5}, + "datatype": "INT16", + "unit": "K/10", + "default": 0, + "range": {"min": -50, "max": 50}, "since": "3.90.1", - "description": "Temperature correction in Kelvin " \ - "for mixing circuit 3 in cooling mode", + "description": "Offset applied to the current flow temperature for mixing circuit 3 cooling. \ +Requires mc3_cool_mode = offset to apply." }, { "index": 40, @@ -329,18 +451,36 @@ "names": ["lpc_mode"], "type": LpcMode, "writeable": True, + "datatype": "UINT16", + "unit": "enum", + "default": 0, + "range": {"min": 0, "max": 2}, "since": "3.90.1", - "description": "Operating mode of the load power control", + "description": "Configuration for limitation of power consumption: \ +0: no power limitation (normal operation) \ +Setpoint values are achieved with heat pump performance curve \ +1: Soft limitation (recommended for PV surplus) \ +Power recommendation for heat pump, i.e., heat pump attempts to \ +limit power demand according to data point pc_limit \ +If the actual value deviates too much from the setpoint (hysteresis), \ +the heat pump ignores the PC Limit power specification. \ +2: Hard limitation (recommended only for §14a EnWG). \ +The heat pump limits the power consumption according to pc_limit regardless of hysteresis. \ +Hard limitation may reduce comfort." }, { "index": 41, "count": 1, "names": ["pc_limit"], - "type": PowerLimit, + "type": PowerKW, "writeable": True, - "range": {"min": 0, "max": 30}, + "datatype": "UINT16", + "unit": "kW/10", + "default": 300, + "range": {"min": 0, "max": 300}, "since": "3.90.1", - "description": "Maximum power limit", + "description": "Maximum allowed power consumption of the heat pump. \ +Requires lpc_mode to be set accordingly." }, { "index": 50, @@ -366,8 +506,15 @@ "names": ["lock_cooling"], "type": LockMode, "writeable": True, + "datatype": "UINT16", + "unit": "bool", + "default": 0, + "range": {"min": 0, "max": 1}, "since": "3.90.1", - "description": "Lock state for the cooling function", + "description": "Cooling operation lock. \ +0: normal operation \ +1: lock passive and active cooling. \ +Frequent switching may cause wear on heat pump and hydraulic components." }, { "index": 53, @@ -375,8 +522,15 @@ "names": ["lock_swimming_pool"], "type": LockMode, "writeable": True, + "datatype": "UINT16", + "unit": "bool", + "default": 0, + "range": {"min": 0, "max": 1}, "since": "3.90.1", - "description": "Lock state for the swimming pool function", + "description": "Swimming pool heating lock. \ +0: normal operation \ +1: lock pool heating. \ +Frequent switching may cause wear on heat pump and hydraulic components." }, { "index": 60, @@ -400,9 +554,10 @@ "index": 66, "count": 1, "names": ["heat_overall_offset"], - "type": Kelvin, + "type": KelvinInt16, "writeable": True, - "range": {"min": 0, "max": 20}, + "datatype": "INT16", + "range": {"min": -200, "max": 200}, "since": "3.92.0", "description": "Temperature correction in Kelvin " \ "for all heating functions", diff --git a/luxtronik/definitions/inputs.py b/luxtronik/definitions/inputs.py index 57db395..cc5be83 100644 --- a/luxtronik/definitions/inputs.py +++ b/luxtronik/definitions/inputs.py @@ -5,16 +5,24 @@ Unlike the setting registers, these SHI register are volatile and intended for communication with smart home systems. 'Input' register are read-only and are used for display or to control other devices. + +NOTE: Data fields that span multiple registers are typically in big-endian/MSB-first order. """ from typing import Final from luxtronik.datatypes import ( - Celsius, + BufferType, + CelsiusInt16, + CelsiusUInt16, Energy, + Errorcode, FullVersion, - HeatPumpState, - ModeState, + HeatPumpStatus, + Minutes, + ModeStatus, + OnOffMode, OperationMode, + PowerKW, Unknown, ) @@ -26,11 +34,20 @@ { "index": 0, "count": 1, - "names": ["heatpump_state"], - "type": HeatPumpState, + "names": ["heatpump_status"], + "type": HeatPumpStatus, "writeable": False, + "datatype": "UINT16", + "unit": "bitmask", "since": "3.90.1", - "description": "Current operating state of the heat pump", + "description": "Heat pump status bitmask: \ +1: VD1 \ +2: VD2 \ +4: ZWE1 \ +8: ZWE2 \ +16: ZWE3 \ +0: Heat pump inactive \ +>0: Heat pump or auxiliary heater active" }, { "index": 2, @@ -38,51 +55,93 @@ "names": ["operation_mode"], "type": OperationMode, "writeable": False, + "datatype": "UINT16", + "unit": "enum", + "default": 5, + "range": {"min": 0, "max": 7}, "since": "3.90.1", - "description": "Overall operation mode of the system", + "description": "Operating mode status: \ +0: Heating \ +1: DHW heating \ +2: Pool heating / Solar\ +3: Utility lockout \ +4: Defrost \ +5: No demand \ +6: Not used \ +7: Cooling" }, { "index": 3, "count": 1, - "names": ["heating_state"], - "type": ModeState, + "names": ["heating_status"], + "type": ModeStatus, "writeable": False, + "datatype": "UINT16", + "unit": "enum", + "range": {"min": 0, "max": 3}, "since": "3.90.1", - "description": "Current state of the heating function", + "description": "Heating status: \ +0: Off \ +1: No demand \ +2: Demand \ +3: Active" }, { "index": 4, "count": 1, - "names": ["hot_water_state"], - "type": ModeState, + "names": ["hot_water_status", "dhw_status"], + "type": ModeStatus, "writeable": False, + "datatype": "UINT16", + "unit": "enum", + "range": {"min": 0, "max": 3}, "since": "3.90.1", - "description": "Current state of the hot water system", + "description": "DHW status: \ +0: Off \ +1: No demand \ +2: Demand \ +3: Active" }, { "index": 6, "count": 1, - "names": ["unknown_input_6"], - "type": Unknown, + "names": ["cooling_status"], + "type": ModeStatus, "writeable": False, + "datatype": "UINT16", + "unit": "enum", + "range": {"min": 0, "max": 3}, "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", + "description": "Cooling status: \ +0: Off \ +1: No demand \ +2: Demand \ +3: Active" }, { "index": 7, "count": 1, - "names": ["unknown_input_7"], - "type": Unknown, + "names": ["pool_heating_status"], + "type": ModeStatus, "writeable": False, + "datatype": "UINT16", + "unit": "enum", + "range": {"min": 0, "max": 3}, "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", + "description": "Pool heating / Solar status: \ +0: Off \ +1: No demand \ +2: Demand \ +3: Active" }, { "index": 100, "count": 1, "names": ["return_line_temp"], - "type": Celsius, + "type": CelsiusUInt16, "writeable": False, + "datatype": "UINT16", + "unit": "°C/10", "since": "3.90.1", "description": "Current return line temperature", }, @@ -90,8 +149,10 @@ "index": 101, "count": 1, "names": ["return_line_target"], - "type": Celsius, + "type": CelsiusUInt16, "writeable": False, + "datatype": "UINT16", + "unit": "°C/10", "since": "3.90.1", "description": "Target return line temperature", }, @@ -99,8 +160,10 @@ "index": 102, "count": 1, "names": ["return_line_ext"], - "type": Celsius, + "type": CelsiusUInt16, "writeable": False, + "datatype": "UINT16", + "unit": "°C/10", "since": "3.90.1", "description": "Current value of the external return temperature sensor", }, @@ -108,8 +171,10 @@ "index": 103, "count": 1, "names": ["return_line_limit"], - "type": Celsius, + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", "description": "Maximum allowed return line temperature", }, @@ -117,8 +182,10 @@ "index": 104, "count": 1, "names": ["return_line_min_target"], - "type": Celsius, + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", "description": "Minimum target return line temperature", }, @@ -126,73 +193,89 @@ "index": 105, "count": 1, "names": ["flow_line_temp"], - "type": Celsius, + "type": CelsiusUInt16, "writeable": False, + "datatype": "UINT16", + "unit": "°C/10", "since": "3.90.1", "description": "Current flow line temperature", }, { "index": 106, "count": 1, - "names": ["unknown_input_106"], - "type": Unknown, + "names": ["room_temperature"], + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", + "description": "Current room temperature. \ +Requires accessory RBE+ room control unit." }, { "index": 107, "count": 1, - "names": ["inside_temp"], - "type": Celsius, + "names": ["heating_limit"], + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", - "description": "Measured indoor temperature", + "description": "Heating limit temperature. \ +If undershot (heating curve setpoint - hysteresis), soft-limit power control is ignored." }, { "index": 108, "count": 1, "names": ["outside_temp"], - "type": Celsius, + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", "description": "Measured outdoor temperature", }, { "index": 109, "count": 1, - "names": ["unknown_input_109"], - "type": Unknown, + "names": ["outside_temp_average"], + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.92.0", - "description": "TODO: Function unknown – requires further analysis", + "description": "Average outdoor temperature", }, { "index": 110, "count": 1, - "names": ["unknown_input_110"], - "type": Unknown, + "names": ["heat_source_input"], + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.92.0", - "description": "TODO: Function unknown – requires further analysis", + "description": "Heat source input temperature", }, { "index": 111, "count": 1, - "names": ["unknown_input_111"], - "type": Unknown, + "names": ["heat_source_output"], + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.92.0", - "description": "TODO: Function unknown – requires further analysis", + "description": "Heat source output temperature", }, { "index": 112, "count": 1, - "names": ["unknown_input_112"], - "type": Unknown, + "names": ["max_flow_temp"], + "type": CelsiusUInt16, "writeable": False, "since": "3.92.0", - "description": "TODO: Function unknown – requires further analysis", + "description": "Maximum flow temperature", }, { "index": 113, @@ -206,63 +289,78 @@ { "index": 120, "count": 1, - "names": ["hot_water_temp"], - "type": Celsius, + "names": ["hot_water_temp", "dhw_temp"], + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", "description": "Current hot water temperature", }, { "index": 121, "count": 1, - "names": ["hot_water_target"], - "type": Celsius, + "names": ["hot_water_target", "dhw_target"], + "type": CelsiusUInt16, "writeable": False, + "datatype": "UINT16", + "unit": "°C/10", "since": "3.90.1", "description": "Target hot water temperature", }, { "index": 122, "count": 1, - "names": ["hot_water_min"], - "type": Celsius, + "names": ["hot_water_min", "dhw_min"], + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", - "description": "Minimum hot water temperature", + "description": "Minimum adjustable hot water temperature", }, { "index": 123, "count": 1, - "names": ["hot_water_max"], - "type": Celsius, + "names": ["hot_water_max", "dhw_max"], + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", - "description": "Maximum hot water temperature", + "description": "Maximum adjustable hot water temperature", }, { "index": 124, "count": 1, - "names": ["unknown_input_124"], - "type": Unknown, + "names": ["hot_water_limit", "dhw_limit"], + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", - "description": "TODO: Function unknown – possibly hot water idle/running indicator", + "description": "DHW limit temperature. \ +If undershot (desired regulation value), soft-limit power control is ignored." }, { "index": 140, "count": 1, "names": ["mc1_temp"], - "type": Celsius, + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", - "description": "Current temperature of mixing circuit 1", + "description": "Current flow temperature of mixing circuit 1", }, { "index": 141, "count": 1, "names": ["mc1_target"], - "type": Celsius, + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", "description": "Desired target temperature of mixing circuit 1", }, @@ -270,8 +368,10 @@ "index": 142, "count": 1, "names": ["mc1_min"], - "type": Celsius, + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", "description": "Minimum temperature of mixing circuit 1", }, @@ -279,8 +379,10 @@ "index": 143, "count": 1, "names": ["mc1_max"], - "type": Celsius, + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", "description": "Maximum temperature of mixing circuit 1", }, @@ -288,17 +390,21 @@ "index": 150, "count": 1, "names": ["mc2_temp"], - "type": Celsius, + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", - "description": "Current temperature of mixing circuit 2", + "description": "Current flow temperature of mixing circuit 2", }, { "index": 151, "count": 1, "names": ["mc2_target"], - "type": Celsius, + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", "description": "Desired target temperature of mixing circuit 2", }, @@ -306,8 +412,10 @@ "index": 152, "count": 1, "names": ["mc2_min"], - "type": Celsius, + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", "description": "Minimum temperature of mixing circuit 2", }, @@ -315,8 +423,10 @@ "index": 153, "count": 1, "names": ["mc2_max"], - "type": Celsius, + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", "description": "Maximum temperature of mixing circuit 2", }, @@ -324,17 +434,21 @@ "index": 160, "count": 1, "names": ["mc3_temp"], - "type": Celsius, + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", - "description": "Current temperature of mixing circuit 3", + "description": "Current flow temperature of mixing circuit 3", }, { "index": 161, "count": 1, "names": ["mc3_target"], - "type": Celsius, + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", "description": "Desired target temperature of mixing circuit 3", }, @@ -342,8 +456,10 @@ "index": 162, "count": 1, "names": ["mc3_min"], - "type": Celsius, + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", "description": "Minimum temperature of mixing circuit 3", }, @@ -351,280 +467,246 @@ "index": 163, "count": 1, "names": ["mc3_max"], - "type": Celsius, + "type": CelsiusInt16, "writeable": False, + "datatype": "INT16", + "unit": "°C/10", "since": "3.90.1", "description": "Maximum temperature of mixing circuit 3", }, { "index": 201, "count": 1, - "names": ["unknown_input_201"], - "type": Unknown, + "names": ["error_number"], + "type": Errorcode, "writeable": False, + "datatype": "UINT16", "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", + "description": "Current error number: \ +0: no error \ +X: error code." }, { "index": 202, "count": 1, - "names": ["unknown_input_202"], - "type": Unknown, + "names": ["buffer_type"], + "type": BufferType, "writeable": False, + "datatype": "UINT16", + "range": {"min": 0, "max": 2}, "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", + "description": "Buffer tank configuration: \ +0: series buffer \ +1: separation buffer \ +2: multifunction buffer." }, { "index": 203, "count": 1, - "names": ["unknown_input_203"], - "type": Unknown, + "names": ["min_off_time"], + "type": Minutes, "writeable": False, + "datatype": "UINT16", + "unit": "minute", "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", + "description": "Minimum off-time before heat pump may restart." }, { "index": 204, "count": 1, - "names": ["unknown_input_204"], - "type": Unknown, + "names": ["min_run_time"], + "type": Minutes, "writeable": False, + "datatype": "UINT16", + "unit": "minute", "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", + "description": "Minimum runtime of the heat pump." }, { "index": 205, "count": 1, - "names": ["unknown_input_205"], - "type": Unknown, + "names": ["cooling_configured"], + "type": OnOffMode, "writeable": False, + "datatype": "UINT16", + "unit": "bool", + "range": {"min": 0, "max": 1}, "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", + "description": "Indicates whether cooling mode is configured: \ +0: no \ +1: yes." }, { "index": 206, "count": 1, - "names": ["unknown_input_206"], - "type": Unknown, + "names": ["pool_heating_configured"], + "type": OnOffMode, "writeable": False, + "datatype": "UINT16", + "unit": "bool", + "range": {"min": 0, "max": 1}, "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", + "description": "Indicates whether pool heating is configured: \ +0: no \ +1: yes." }, { "index": 207, "count": 1, - "names": ["unknown_input_207"], - "type": Unknown, + "names": ["cooling_release"], + "type": OnOffMode, "writeable": False, + "datatype": "UINT16", + "unit": "bool", + "range": {"min": 0, "max": 1}, "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", + "description": "Cooling release condition fulfilled: \ +0: no \ +1: yes. \ +Cooling release only valid if cooling is enabled (see cooling_configured)." }, { "index": 300, "count": 1, - "names": ["unknown_input_300"], - "type": Unknown, + "names": ["heating_power_actual"], + "type": PowerKW, "writeable": False, + "datatype": "INT16", + "unit": "kW/10", "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", + "description": "Current heating power." }, { "index": 301, "count": 1, - "names": ["unknown_input_301"], - "type": Unknown, + "names": ["electric_power_actual"], + "type": PowerKW, "writeable": False, + "datatype": "UINT16", + "unit": "kW/10", "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", + "description": "Current electrical power consumption." }, { "index": 302, "count": 1, - "names": ["unknown_input_302"], - "type": Unknown, + "names": ["electric_power_min_predicted"], + "type": PowerKW, "writeable": False, + "datatype": "UINT16", + "unit": "kW/10", "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", + "description": "Minimum predicted electrical power consumption." }, { "index": 310, - "count": 1, - "names": ["unknown_input_310"], - "type": Unknown, - "writeable": False, - "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", - }, - { - "index": 311, - "count": 1, - "names": ["total_power_consumption"], + "count": 2, + "names": ["electric_energy_total"], "type": Energy, "writeable": False, + "datatype": "INT32", + "unit": "kWh/10", "since": "3.90.1", - "description": "Total electrical energy consumption", + "description": "Total electrical energy consumption (all modes)." }, { "index": 312, - "count": 1, - "names": ["unknown_input_312"], - "type": Unknown, - "writeable": False, - "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", - }, - { - "index": 313, - "count": 1, - "names": ["heat_power_consumption"], + "count": 2, + "names": ["electric_energy_heating"], "type": Energy, "writeable": False, + "datatype": "INT32", + "unit": "kWh/10", "since": "3.90.1", - "description": "Electrical energy consumption for heating", + "description": "Total electrical energy consumption for heating." }, { "index": 314, - "count": 1, - "names": ["unknown_input_314"], - "type": Unknown, - "writeable": False, - "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", - }, - { - "index": 315, - "count": 1, - "names": ["hot_water_power_consumption"], + "count": 2, + "names": ["electric_energy_dhw"], "type": Energy, "writeable": False, + "datatype": "INT32", + "unit": "kWh/10", "since": "3.90.1", - "description": "Electrical energy consumption for hot water", + "description": "Total electrical energy consumption for DHW." }, { "index": 316, - "count": 1, - "names": ["unknown_input_316"], - "type": Unknown, - "writeable": False, - "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", - }, - { - "index": 317, - "count": 1, - "names": ["unknown_input_317"], - "type": Unknown, + "count": 2, + "names": ["electric_energy_cooling"], + "type": Energy, "writeable": False, + "datatype": "INT32", + "unit": "kWh/10", "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", + "description": "Total electrical energy consumption for cooling." }, { "index": 318, - "count": 1, - "names": ["unknown_input_318"], - "type": Unknown, - "writeable": False, - "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", - }, - { - "index": 319, - "count": 1, - "names": ["unknown_input_319"], - "type": Unknown, + "count": 2, + "names": ["electric_energy_pool"], + "type": Energy, "writeable": False, + "datatype": "INT32", + "unit": "kWh/10", "since": "3.90.1", - "description": "TODO: Function unknown – requires further analysis", + "description": "Total electrical energy consumption for pool heating / solar." }, { "index": 320, - "count": 1, - "names": ["unknown_input_320"], - "type": Unknown, - "writeable": False, - "since": "3.92.0", - "description": "TODO: Function unknown – requires further analysis", - }, - { - "index": 321, - "count": 1, - "names": ["unknown_input_321"], - "type": Unknown, + "count": 2, + "names": ["thermal_energy_total"], + "type": Energy, "writeable": False, + "datatype": "INT32", + "unit": "kWh/10", "since": "3.92.0", - "description": "TODO: Function unknown – requires further analysis", + "description": "Total thermal energy production (all modes)." }, { "index": 322, - "count": 1, - "names": ["unknown_input_322"], - "type": Unknown, - "writeable": False, - "since": "3.92.0", - "description": "TODO: Function unknown – requires further analysis", - }, - { - "index": 323, - "count": 1, - "names": ["unknown_input_323"], - "type": Unknown, + "count": 2, + "names": ["thermal_energy_heating"], + "type": Energy, "writeable": False, + "datatype": "INT32", + "unit": "kWh/10", "since": "3.92.0", - "description": "TODO: Function unknown – requires further analysis", + "description": "Total thermal energy production for heating." }, { "index": 324, - "count": 1, - "names": ["unknown_input_324"], - "type": Unknown, - "writeable": False, - "since": "3.92.0", - "description": "TODO: Function unknown – requires further analysis", - }, - { - "index": 325, - "count": 1, - "names": ["unknown_input_325"], - "type": Unknown, + "count": 2, + "names": ["thermal_energy_dhw"], + "type": Energy, "writeable": False, + "datatype": "INT32", + "unit": "kWh/10", "since": "3.92.0", - "description": "TODO: Function unknown – requires further analysis", + "description": "Total thermal energy production for DHW." }, { "index": 326, - "count": 1, - "names": ["unknown_input_326"], - "type": Unknown, - "writeable": False, - "since": "3.92.0", - "description": "TODO: Function unknown – requires further analysis", - }, - { - "index": 327, - "count": 1, - "names": ["unknown_input_327"], - "type": Unknown, + "count": 2, + "names": ["thermal_energy_cooling"], + "type": Energy, "writeable": False, + "datatype": "INT32", + "unit": "kWh/10", "since": "3.92.0", - "description": "TODO: Function unknown – requires further analysis", + "description": "Total thermal energy production for cooling." }, { "index": 328, - "count": 1, - "names": ["unknown_input_328"], - "type": Unknown, - "writeable": False, - "since": "3.92.0", - "description": "TODO: Function unknown – requires further analysis", - }, - { - "index": 329, - "count": 1, - "names": ["unknown_input_329"], - "type": Unknown, + "count": 2, + "names": ["thermal_energy_pool"], + "type": Energy, "writeable": False, + "datatype": "INT32", + "unit": "kWh/10", "since": "3.92.0", - "description": "TODO: Function unknown – requires further analysis", + "description": "Total thermal energy production for pool heating / solar." }, { "index": 350, diff --git a/luxtronik/parameters.py b/luxtronik/parameters.py index 77b57ef..9f8a182 100755 --- a/luxtronik/parameters.py +++ b/luxtronik/parameters.py @@ -1216,7 +1216,11 @@ def set(self, target, value): index, parameter = self._lookup(target, with_index=True) if index is not None: if parameter.writeable or not self.safe: - self.queue[index] = int(parameter.to_heatpump(value)) + raw = parameter.to_heatpump(value) + if isinstance(raw, int): + self.queue[index] = raw + else: + self.logger.error("Value '%s' for Parameter '%s' not valid!", value, parameter.name) else: self.logger.warning("Parameter '%s' not safe for writing!", parameter.name) else: diff --git a/luxtronik/shi/constants.py b/luxtronik/shi/constants.py index 338539d..29ea1a7 100644 --- a/luxtronik/shi/constants.py +++ b/luxtronik/shi/constants.py @@ -21,6 +21,9 @@ # to give the controller time to recalculate values, etc. LUXTRONIK_WAIT_TIME_AFTER_HOLDING_WRITE: Final = 1 +# The data from the smart home interface are transmitted in 16-bit chunks. +LUXTRONIK_SHI_REGISTER_BIT_SIZE = 16 + # Since version 3.92.0, all unavailable data fields # have been returning this value (0x7FFF) LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE: Final = 32767 diff --git a/luxtronik/shi/definitions.py b/luxtronik/shi/definitions.py index 5164455..46f2224 100644 --- a/luxtronik/shi/definitions.py +++ b/luxtronik/shi/definitions.py @@ -10,6 +10,7 @@ from luxtronik.datatypes import Unknown from luxtronik.shi.constants import ( LUXTRONIK_DEFAULT_DEFINITION_OFFSET, + LUXTRONIK_SHI_REGISTER_BIT_SIZE, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE ) from luxtronik.shi.common import ( @@ -549,6 +550,64 @@ def add(self, data_dict): # Definition-Field-Pair methods ############################################################################### +VALUE_MASK = (1 << LUXTRONIK_SHI_REGISTER_BIT_SIZE) - 1 + +def pack_values(values, reverse=True): + """ + Packs a list of data chunks into one integer. + + Args: + values (list[int]): raw data; distributed across multiple registers. + reverse (bool): Use big-endian/MSB-first if true, + otherwise use little-endian/LSB-first order. + + Returns: + int: Packed raw data as a single integer value. + + Note: + The smart home interface uses a chunk size of 16 bits. + """ + count = len(values) + + result = 0 + for idx, value in enumerate(values): + # normal: idx = 0..n-1 + # reversed index: highest chunk first + bit_index = (count - 1 - idx) if reverse else idx + + result |= (value & VALUE_MASK) << (LUXTRONIK_SHI_REGISTER_BIT_SIZE * bit_index) + + return result + +def unpack_values(packed, count, reverse=True): + """ + Unpacks 'count' values from a packed integer. + + Args: + packed (int): Packed raw data as a single integer value. + count (int): Number of chunks to unpack. + reverse (bool): Use big-endian/MSB-first if true, + otherwise use little-endian/LSB-first order. + + Returns: + list[int]: List of unpacked raw data values. + + Note: + The smart home interface uses a chunk size of 16 bits. + """ + values = [] + + for idx in range(count): + # normal: idx = 0..n-1 + # reversed: highest chunk first + bit_index = (count - 1 - idx) if reverse else idx + + chunk = (packed >> (LUXTRONIK_SHI_REGISTER_BIT_SIZE * bit_index)) & VALUE_MASK + values.append(chunk) + + return values + + def get_data_arr(definition, field): """ Normalize the field's data to a list of the correct size. @@ -562,6 +621,12 @@ def get_data_arr(definition, field): or None if the data size does not match. """ data = field.raw + if data is None: + return None + if not isinstance(data, list) and definition.count > 1 \ + and field.concatenate_multiple_data_chunks: + # Usually big-endian (reverse=True) is used + data = unpack_values(data, definition.count) if not isinstance(data, list): data = [data] return data if len(data) == definition.count else None @@ -600,4 +665,7 @@ def integrate_data(definition, field, raw_data, data_offset=-1): raw = raw_data[data_offset : data_offset + definition.count] raw = raw if len(raw) == definition.count and \ not any(data == LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE for data in raw) else None + if field.concatenate_multiple_data_chunks and raw is not None: + # Usually big-endian (reverse=True) is used + raw = pack_values(raw) field.raw = raw \ No newline at end of file diff --git a/tests/shi/test_contiguous.py b/tests/shi/test_contiguous.py index 3ca4e2d..2153d25 100644 --- a/tests/shi/test_contiguous.py +++ b/tests/shi/test_contiguous.py @@ -8,6 +8,13 @@ ContiguousDataBlockList, ) +""" +The test was originally written for "False". +Since "True" is already checked in "test_definitions.py", +we continue to use "False" consistently here. +""" +Base.concatenate_multiple_data_chunks = False + def_a1 = LuxtronikDefinition({ 'index': 1, diff --git a/tests/shi/test_definitions.py b/tests/shi/test_definitions.py index 1077379..aaa1752 100644 --- a/tests/shi/test_definitions.py +++ b/tests/shi/test_definitions.py @@ -465,6 +465,7 @@ class TestDefinitionFieldPair: def test_data_arr(self): definition = LuxtronikDefinition.unknown(2, 'Foo', 30) field = definition.create_field() + field.concatenate_multiple_data_chunks = False # get from value definition._count = 1 @@ -494,9 +495,33 @@ def test_data_arr(self): assert arr is None assert not check_data(definition, field) + field.concatenate_multiple_data_chunks = True + + # get from array + definition._count = 2 + field.raw = 0x0007_0003 + arr = get_data_arr(definition, field) + assert arr == [7, 3] + assert check_data(definition, field) + + # too much data + definition._count = 2 + field.raw = 0x0004_0008_0001 + arr = get_data_arr(definition, field) + assert arr == [8, 1] + assert check_data(definition, field) + + # insufficient data + definition._count = 2 + field.raw = 0x0009 + arr = get_data_arr(definition, field) + assert arr == [0, 9] + assert check_data(definition, field) + def test_integrate(self): definition = LuxtronikDefinition.unknown(2, 'Foo', 30) field = definition.create_field() + field.concatenate_multiple_data_chunks = False data = [1, LUXTRONIK_VALUE_FUNCTION_NOT_AVAILABLE, 3, 4, 5, 6, 7] @@ -520,4 +545,28 @@ def test_integrate(self): integrate_data(definition, field, data, 9) assert field.raw is None integrate_data(definition, field, data, 1) + assert field.raw is None + + field.concatenate_multiple_data_chunks = True + + # set array + definition._count = 2 + integrate_data(definition, field, data) + assert field.raw == 0x0003_0004 + integrate_data(definition, field, data, 4) + assert field.raw == 0x0005_0006 + integrate_data(definition, field, data, 7) + assert field.raw is None + integrate_data(definition, field, data, 0) + assert field.raw is None + + # set value + definition._count = 1 + integrate_data(definition, field, data) + assert field.raw == 0x0003 + integrate_data(definition, field, data, 5) + assert field.raw == 0x0006 + integrate_data(definition, field, data, 9) + assert field.raw is None + integrate_data(definition, field, data, 1) assert field.raw is None \ No newline at end of file diff --git a/tests/shi/test_vector.py b/tests/shi/test_vector.py index 62a0ab3..74e429d 100644 --- a/tests/shi/test_vector.py +++ b/tests/shi/test_vector.py @@ -4,6 +4,12 @@ from luxtronik.shi.vector import DataVectorSmartHome from luxtronik.shi.definitions import LuxtronikDefinitionsList +""" +The test was originally written for "False". +Since "True" is already checked in "test_definitions.py", +we continue to use "False" consistently here. +""" +Base.concatenate_multiple_data_chunks = False ############################################################################### # Tests diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index e787a88..79ba895 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -12,6 +12,7 @@ from luxtronik.datatypes import ( Base, + BitMaskBase, SelectionBase, ScalingBase, Celsius, @@ -27,7 +28,7 @@ Percent2, Speed, Power, - PowerLimit, + PowerKW, Energy, Voltage, Hours, @@ -224,6 +225,7 @@ def test_from_heatpump(self): a = SelectionBase("") assert a.from_heatpump(0) == 'Unknown_0' + assert a.from_heatpump(None) is None def test_to_heatpump(self): """Test cases for to_heatpump function""" @@ -231,6 +233,10 @@ def test_to_heatpump(self): a = SelectionBase("") assert a.to_heatpump("a") is None assert a.to_heatpump("Unknown_214") == 214 + assert a.to_heatpump(0) == 0 + assert a.to_heatpump("1") == 1 + assert a.to_heatpump(2.3) == 2 + assert a.to_heatpump("3.1") is None class SelectionBaseChild(SelectionBase): @@ -269,6 +275,7 @@ def test_from_heatpump(self): assert a.from_heatpump(1) == "b" assert a.from_heatpump(2) == "c" assert a.from_heatpump(3) == "Unknown_3" + assert a.from_heatpump(None) is None def test_to_heatpump(self): """Test cases for to_heatpump function""" @@ -278,6 +285,112 @@ def test_to_heatpump(self): assert a.to_heatpump("b") == 1 assert a.to_heatpump("c") == 2 assert a.to_heatpump("d") is None + assert a.to_heatpump(None) is None + assert a.to_heatpump(2) == 2 + assert a.to_heatpump("3") == 3 + + +class TestBitMaskBase: + """Test suite for BitMaskBase datatype""" + + def test_init(self): + """Test cases for initialization""" + + a = BitMaskBase("bitmask_base") + assert a.name == "bitmask_base" + assert not a.bit_values + assert len(a.bit_values) == 0 + + def test_bits(self): + """Test cases for bits property""" + + a = BitMaskBase("") + assert len(a.bits()) == 0 + assert a.bits() == list(a.bit_values.values()) + + def test_from_heatpump(self): + """Test cases for from_heatpump function""" + + a = BitMaskBase("") + assert a.from_heatpump(0) == a.value_zero + assert a.from_heatpump(16) == "Unknown_4" + assert a.from_heatpump(5) == "Unknown_0, Unknown_2" + assert a.from_heatpump("Unknown_1") is None + assert a.from_heatpump(None) is None + + def test_to_heatpump(self): + """Test cases for to_heatpump function""" + + a = BitMaskBase("") + assert a.to_heatpump("a") is None + assert a.to_heatpump(1) is None + assert a.to_heatpump(None) is None + assert a.to_heatpump("Unknown_3") == 8 + assert a.to_heatpump("Unknown_0, Unknown_2") == 5 + assert a.to_heatpump("Unknown_0, 2") is None + + +class BitMaskBaseChild(BitMaskBase): + """Child class of BitMaskBase containing codes to test it in the context of TestBitMaskBaseChild""" + + bit_values = { + 0: "a", + 2: "b", + 4: "c", + } + value_zero = "empty" + value_delim = "-" + values_postfix = "-z" + + +class TestBitMaskBaseChild: + """Test suite for BitMaskBase datatype""" + + def test_init(self): + """Test cases for initialization""" + + a = BitMaskBaseChild("bitmask_base_child") + assert a.name == "bitmask_base_child" + assert a.bit_values + assert len(a.bit_values) == 3 + + def test_bits(self): + """Test cases for bits property""" + + a = BitMaskBaseChild("") + assert len(a.bits()) == 3 + assert a.bits() == list(a.bit_values.values()) + + def test_from_heatpump(self): + """Test cases for from_heatpump function""" + + a = BitMaskBaseChild("") + assert a.from_heatpump(0) == "empty" + assert a.from_heatpump(1) == "a-z" + assert a.from_heatpump(2) == "Unknown_1-z" + assert a.from_heatpump(3) == "a-Unknown_1-z" + assert a.from_heatpump(4) == "b-z" + assert a.from_heatpump(5) == "a-b-z" + assert a.from_heatpump("Unknown_1") is None + assert a.from_heatpump(None) is None + + def test_to_heatpump(self): + """Test cases for to_heatpump function""" + + a = BitMaskBaseChild("") + assert a.to_heatpump("empty") == 0 + assert a.to_heatpump("empty-z") == 0 + assert a.to_heatpump("a") == 1 + assert a.to_heatpump("b-z") == 4 + assert a.to_heatpump("a-c-z") == 17 + assert a.to_heatpump("Unknown_1-b-z") == 6 + assert a.to_heatpump(1) is None + assert a.to_heatpump(None) is None + + +class ScalingBaseTest(ScalingBase): + """Class to test ScalingBase. Required because of __init_subclass__""" + pass class TestScalingBase: @@ -286,14 +399,14 @@ class TestScalingBase: def test_init(self): """Test cases for initialization""" - a = ScalingBase("scaling_base") + a = ScalingBaseTest("scaling_base") assert a.name == "scaling_base" assert a.scaling_factor == 1 def test_from_heatpump(self): """Test cases for from_heatpump function""" - a = ScalingBase("") + a = ScalingBaseTest("") assert a.from_heatpump(1) == 1 assert a.from_heatpump(42) == 42 assert a.from_heatpump(None) is None @@ -301,7 +414,7 @@ def test_from_heatpump(self): def test_to_heatpump(self): """Test cases for to_heatpump function""" - a = ScalingBase("") + a = ScalingBaseTest("") assert a.to_heatpump(1) == 1 assert a.to_heatpump(42) == 42 @@ -341,6 +454,38 @@ def test_to_heatpump(self): assert a.to_heatpump(-100) == -8 +class ScalingBaseInt4Child(ScalingBase): + """Child class of ScalingBase containing a scaling_factor to test it in the context of TestScalingBaseChild""" + + data_width = 4 # bits + + +class TestScalingBaseInt4Child: + """Test suite for 16-bit ScalingBase datatype""" + + def test_from_heatpump(self): + """Test cases for from_heatpump function""" + + a = ScalingBaseInt4Child("") + assert a.from_heatpump(None) is None + assert a.from_heatpump(0) == 0 + assert a.from_heatpump(1) == 1 + assert a.from_heatpump(8) == -8 + assert a.from_heatpump(10) == -6 + assert a.from_heatpump(15) == -1 + assert a.from_heatpump(17) == 1 + + def test_to_heatpump(self): + """Test cases for to_heatpump function""" + + a = ScalingBaseInt4Child("") + assert a.to_heatpump(None) is None + assert a.to_heatpump(0) == 0 + assert a.to_heatpump(26) == 26 + assert a.to_heatpump(40) == 40 + assert a.to_heatpump(-100) == -100 + + class TestCelsius: """Test suite for Celsius datatype""" @@ -625,21 +770,21 @@ def test_init(self): assert a.datatype_class == "power" assert a.datatype_unit == "W" -class TestPowerLimit: - """Test suite for PowerLimit datatype""" +class TestPowerKW: + """Test suite for PowerKW datatype""" def test_init(self): """Test cases for initialization""" - a = PowerLimit("power_limit") - assert a.name == "power_limit" + a = PowerKW("power_kW") + assert a.name == "power_kW" assert a.datatype_class == "power" assert a.datatype_unit == "kW" def test_from_heatpump(self): """Test cases for from_heatpump function""" - a = PowerLimit("") + a = PowerKW("") assert a.from_heatpump(15) == 1.5 assert a.from_heatpump(525) == 52.5 assert a.from_heatpump(None) is None @@ -647,7 +792,7 @@ def test_from_heatpump(self): def test_to_heatpump(self): """Test cases for to_heatpump function""" - a = PowerLimit("") + a = PowerKW("") assert a.to_heatpump(1.5) == 15 assert a.to_heatpump(5.6) == 56