From 073576546b134e0e92c176276ccb633d26034c86 Mon Sep 17 00:00:00 2001 From: Gerd Wachsmuth Date: Sat, 22 Jul 2023 21:35:06 +0200 Subject: [PATCH 01/13] Update pytest.yml According to https://github.com/actions/first-interaction/issues/10#issuecomment-670968624, this should fix the issue with the failing of pytest on pull requests from forks. --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 9fa41de..70b4756 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -4,7 +4,7 @@ on: # yamllint disable-line rule:truthy push: branches: - main - pull_request: + pull_request_target: jobs: pytest: runs-on: ubuntu-latest From fefa963ddd6ba98615835138bb67ffd31540a3cc Mon Sep 17 00:00:00 2001 From: Gerd Wachsmuth Date: Wed, 19 Jul 2023 21:07:35 +0200 Subject: [PATCH 02/13] Add test for Parameters --- tests/test_parameters.py | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/test_parameters.py diff --git a/tests/test_parameters.py b/tests/test_parameters.py new file mode 100644 index 0000000..1b9c42a --- /dev/null +++ b/tests/test_parameters.py @@ -0,0 +1,55 @@ +"""Test suite for parameters module""" + +# pylint: disable=too-few-public-methods,invalid-name,protected-access + +from luxtronik.parameters import Parameters + + +class TestParameters: + """Test suite for Parameters""" + + def test_init(self): + """Test cases for initialization""" + parameters = Parameters() + assert parameters.safe + assert len(parameters.queue) == 0 + + parameters = Parameters(False) + assert not parameters.safe + assert len(parameters.queue) == 0 + + def test_get(self): + """Test cases for get""" + parameters = Parameters() + s = "ID_Transfert_LuxNet" + assert parameters.get(0).name == s + 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) + + s = "ID_BarFoo" + assert parameters._lookup(s, True)[0] is None + + def test_parse(self): + """Test cases for _parse""" + parameters = Parameters() + + n = 2000 + t = [0] * (n + 1) + parameters.parse(t) + + p = parameters.get(n) + + assert p.name == f"Unknown_Parameter_{n}" From 55fc126ba50ffe7633ce9d41847bbfefbb04f58a Mon Sep 17 00:00:00 2001 From: Gerd Wachsmuth Date: Wed, 19 Jul 2023 21:28:47 +0200 Subject: [PATCH 03/13] Simplify _lookup() of Parameters --- luxtronik/parameters.py | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/luxtronik/parameters.py b/luxtronik/parameters.py index 7f0ac0f..6e41629 100755 --- a/luxtronik/parameters.py +++ b/luxtronik/parameters.py @@ -1178,32 +1178,29 @@ def parse(self, raw_data): self._parameters[index] = parameter def _lookup(self, target, with_index=False): - # pylint: disable=too-many-return-statements,fixme - # TODO Evaluate whether logic can be re-arranged to get rid of the - # pylint error regarding too many return statements. """Lookup parameter by either id or name.""" - # Get parameter by id - if isinstance(target, int): - if with_index: - return target, self._parameters.get(target, None) - return self._parameters.get(target, None) - # Get parameter by name if isinstance(target, str): try: - target = int(target) - if with_index: - return target, self._parameters.get(target, None) - return self._parameters.get(target, None) + # Try to get parameter by id + target_index = int(target) except ValueError: + # Get parameter by name + target_index = None for index, parameter in self._parameters.items(): if parameter.name == target: - if with_index: - return index, parameter - return parameter - LOGGER.warning("Parameter '%s' not found", target) + target_index = index + elif isinstance(target, int): + # Get parameter by id + target_index = target + else: + target_index = None + + target_parameter = self._parameters.get(target_index, None) + if target_parameter is None: + LOGGER.warning("Parameter '%s' not found", target) if with_index: - return None, None - return None + return target_index, target_parameter + return target_parameter def get(self, target): """Get parameter by id or name.""" From 06868e1f21034fddfc47ad41af1d221dce96274b Mon Sep 17 00:00:00 2001 From: Gerd Wachsmuth Date: Fri, 21 Jul 2023 21:49:57 +0200 Subject: [PATCH 04/13] Common base class for para, calc, visi --- luxtronik/calculations.py | 49 +++++------------------------- luxtronik/data_vector.py | 60 +++++++++++++++++++++++++++++++++++++ luxtronik/parameters.py | 63 ++++++++------------------------------- luxtronik/visibilities.py | 49 +++++------------------------- 4 files changed, 88 insertions(+), 133 deletions(-) create mode 100644 luxtronik/data_vector.py diff --git a/luxtronik/calculations.py b/luxtronik/calculations.py index babf76b..5896f52 100755 --- a/luxtronik/calculations.py +++ b/luxtronik/calculations.py @@ -1,6 +1,8 @@ """Parse luxtronik calculations.""" import logging +from luxtronik.data_vector import DataVector + from luxtronik.datatypes import ( BivalenceLevel, Bool, @@ -33,14 +35,16 @@ Voltage, ) -LOGGER = logging.getLogger("Luxtronik.Calculations") - -class Calculations: +class Calculations(DataVector): """Class that holds all calculations.""" + logger = logging.getLogger("Luxtronik.Calculations") + name = "Calculation" + def __init__(self): - self._calculations = { + super().__init__() + self._data = { 0: Unknown("Unknown_Calculation_0"), 1: Unknown("Unknown_Calculation_1"), 2: Unknown("Unknown_Calculation_2"), @@ -302,40 +306,3 @@ def __init__(self): 258: MajorMinorVersion("RBE_Version"), 259: Unknown("Unknown_Calculation_259"), } - - def __iter__(self): - return iter(self._calculations.items()) - - def parse(self, raw_data): - """Parse raw calculations data.""" - for index, data in enumerate(raw_data): - calculation = self._calculations.get(index, False) - if calculation is not False: - calculation.raw = data - else: - # LOGGER.warning("Calculation '%d' not in list of calculations", index) - calculation = Unknown(f"Unknown_Calculation_{index}") - calculation.raw = data - self._calculations[index] = calculation - - def _lookup(self, target): - """Lookup calculation by either id or name.""" - # Get calculation by id - if isinstance(target, int): - return self._calculations.get(target, None) - # Get calculation by name - if isinstance(target, str): - try: - target = int(target) - return self._calculations.get(target, None) - except ValueError: - for _, calculation in self._calculations.items(): - if calculation.name == target: - return calculation - LOGGER.warning("Calculation '%s' not found", target) - return None - - def get(self, target): - """Get calculation by id or name.""" - calculation = self._lookup(target) - return calculation diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py new file mode 100644 index 0000000..984b66d --- /dev/null +++ b/luxtronik/data_vector.py @@ -0,0 +1,60 @@ +"""Provides a base class for parameters, calculations, visibilities.""" +import logging + +from luxtronik.datatypes import Unknown + + +class DataVector: + """Class that holds a vector of data entries.""" + + logger = logging.getLogger("Luxtronik.DataVector") + name = "DataVector" + + def __init__(self): + """Initialize DataVector class.""" + self._data = {} + + def __iter__(self): + return iter(self._data.items()) + + def parse(self, raw_data): + """Parse raw data.""" + for index, data in enumerate(raw_data): + entry = self._data.get(index, None) + if entry is not None: + entry.raw = data + else: + # self.logger.warning(f"Entry '%d' not in list of {self.name}", index) + entry = Unknown(f"Unknown_{self.name}_{index}") + entry.raw = data + self._data[index] = entry + + def _lookup(self, target, with_index=False): + """Lookup entry by either id or name.""" + if isinstance(target, str): + try: + # Try to get entry by id + target_index = int(target) + except ValueError: + # Get entry by name + target_index = None + for index, entry in self._data.items(): + if entry.name == target: + target_index = index + 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: + self.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 diff --git a/luxtronik/parameters.py b/luxtronik/parameters.py index 6e41629..7286794 100755 --- a/luxtronik/parameters.py +++ b/luxtronik/parameters.py @@ -3,6 +3,8 @@ """Parse luxtronik parameters.""" import logging +from luxtronik.data_vector import DataVector + from luxtronik.datatypes import ( AccessLevel, Bool, @@ -22,18 +24,20 @@ VentilationMode, ) -LOGGER = logging.getLogger("Luxtronik.Parameters") - -class Parameters: +class Parameters(DataVector): """Class that holds all parameters.""" + logger = logging.getLogger("Luxtronik.Parameters") + name = "Parameter" + def __init__(self, safe=True): """Initialize parameters class.""" + super().__init__() self.safe = safe self.queue = {} - self._parameters = { + self._data = { 0: Unknown("ID_Transfert_LuxNet"), 1: Celsius("ID_Einst_WK_akt", True), 2: Celsius("ID_Einst_BWS_akt", True), @@ -1162,51 +1166,6 @@ def __init__(self, safe=True): 1125: Unknown("Unknown_Parameter_1125"), } - def __iter__(self): - return iter(self._parameters.items()) - - def parse(self, raw_data): - """Parse raw parameter data.""" - for index, data in enumerate(raw_data): - parameter = self._parameters.get(index, False) - if parameter is not False: - parameter.raw = data - else: - # LOGGER.warning("Parameter '%d' not in list of parameters", index) - parameter = Unknown(f"Unknown_Parameter_{index}") - parameter.raw = data - self._parameters[index] = parameter - - def _lookup(self, target, with_index=False): - """Lookup parameter by either id or name.""" - if isinstance(target, str): - try: - # Try to get parameter by id - target_index = int(target) - except ValueError: - # Get parameter by name - target_index = None - for index, parameter in self._parameters.items(): - if parameter.name == target: - target_index = index - elif isinstance(target, int): - # Get parameter by id - target_index = target - else: - target_index = None - - target_parameter = self._parameters.get(target_index, None) - if target_parameter is None: - LOGGER.warning("Parameter '%s' not found", target) - if with_index: - return target_index, target_parameter - return target_parameter - - def get(self, target): - """Get parameter by id or name.""" - parameter = self._lookup(target) - return parameter - def set(self, target, value): """Set parameter to new value.""" index, parameter = self._lookup(target, with_index=True) @@ -1214,6 +1173,8 @@ def set(self, target, value): if parameter.writeable or not self.safe: self.queue[index] = parameter.to_heatpump(value) else: - LOGGER.warning("Parameter '%s' not safe for writing!", parameter.name) + self.logger.warning( + "Parameter '%s' not safe for writing!", parameter.name + ) else: - LOGGER.warning("Parameter '%s' not found", target) + self.logger.warning("Parameter '%s' not found", target) diff --git a/luxtronik/visibilities.py b/luxtronik/visibilities.py index cbd31ac..6585aed 100755 --- a/luxtronik/visibilities.py +++ b/luxtronik/visibilities.py @@ -1,16 +1,20 @@ """Parse luxtronik visibilities.""" import logging -from luxtronik.datatypes import Unknown +from luxtronik.data_vector import DataVector -LOGGER = logging.getLogger("Luxtronik.Visibilities") +from luxtronik.datatypes import Unknown -class Visibilities: +class Visibilities(DataVector): """Class that holds all visibilities.""" + logger = logging.getLogger("Luxtronik.Visibilities") + name = "Visibility" + def __init__(self): - self._visibilities = { + super().__init__() + self._data = { 0: Unknown("ID_Visi_NieAnzeigen"), 1: Unknown("ID_Visi_ImmerAnzeigen"), 2: Unknown("ID_Visi_Heizung"), @@ -367,40 +371,3 @@ def __init__(self): 353: Unknown("Unknown_Visibility_353"), 354: Unknown("Unknown_Visibility_354"), } - - def __iter__(self): - return iter(self._visibilities.items()) - - def parse(self, raw_data): - """Parse raw visibility data.""" - for index, data in enumerate(raw_data): - visibility = self._visibilities.get(index, False) - if visibility is not False: - visibility.raw = data - else: - # LOGGER.warning("Visibility '%d' not in list of visibilities", index) - visibility = Unknown(f"Unknown_Parameter_{index}") - visibility.raw = data - self._visibilities[index] = visibility - - def _lookup(self, target): - """Lookup visibility by either id or name.""" - # Get visibility by id - if isinstance(target, int): - return self._visibilities.get(target, None) - # Get visibility by name - if isinstance(target, str): - try: - target = int(target) - return self._visibilities.get(target, None) - except ValueError: - for _, visibility in self._visibilities.items(): - if visibility.name == target: - return visibility - LOGGER.warning("Visibility '%s' not found", target) - return None - - def get(self, target): - """Get visibility by id or name.""" - visibility = self._lookup(target) - return visibility From 82e16ef252f76c599c10f0669fbac8e1fc46db9b Mon Sep 17 00:00:00 2001 From: Gerd Wachsmuth Date: Fri, 21 Jul 2023 22:14:18 +0200 Subject: [PATCH 05/13] Improve code coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (and fix a bug in Parameters if ´index == 0´) --- luxtronik/parameters.py | 2 +- tests/test_calculations.py | 14 ++++++++++++++ tests/test_parameters.py | 39 ++++++++++++++++++++++++++++++++++++++ tests/test_visibilities.py | 14 ++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tests/test_calculations.py create mode 100644 tests/test_visibilities.py diff --git a/luxtronik/parameters.py b/luxtronik/parameters.py index 7286794..2826a52 100755 --- a/luxtronik/parameters.py +++ b/luxtronik/parameters.py @@ -1169,7 +1169,7 @@ def __init__(self, safe=True): def set(self, target, value): """Set parameter to new value.""" index, parameter = self._lookup(target, with_index=True) - if index: + if index is not None: if parameter.writeable or not self.safe: self.queue[index] = parameter.to_heatpump(value) else: diff --git a/tests/test_calculations.py b/tests/test_calculations.py new file mode 100644 index 0000000..a769e66 --- /dev/null +++ b/tests/test_calculations.py @@ -0,0 +1,14 @@ +"""Test suite for parameters module""" + +# pylint: disable=too-few-public-methods + +from luxtronik.calculations import Calculations + + +class TestCalculations: + """Test suite for Calculations""" + + def test_init(self): + """Test cases for initialization""" + calculations = Calculations() + assert calculations.name == "Calculation" diff --git a/tests/test_parameters.py b/tests/test_parameters.py index 1b9c42a..feccf3e 100644 --- a/tests/test_parameters.py +++ b/tests/test_parameters.py @@ -39,9 +39,14 @@ def test__lookup(self): assert parameters._lookup("0", True) == (0, p0) assert parameters._lookup(s, True) == (0, p0) + # Look for a name which does not exist s = "ID_BarFoo" assert parameters._lookup(s, True)[0] is None + # Look for something which is not an int and not a string + j = 0.0 + assert parameters._lookup(j) is None + def test_parse(self): """Test cases for _parse""" parameters = Parameters() @@ -53,3 +58,37 @@ def test_parse(self): p = parameters.get(n) assert p.name == f"Unknown_Parameter_{n}" + + def test___iter__(self): + """Test cases for __iter__""" + parameters = Parameters() + + for i, p in parameters: + if i == 0: + assert p.name == "ID_Transfert_LuxNet" + elif i == 1: + assert p.name == "ID_Einst_WK_akt" + else: + break + + def test_set(self): + """Test cases for set""" + parameters = Parameters() + + # Set something which does not exist + parameters.set("BarFoo", 0) + assert len(parameters.queue) == 0 + + # Set something which is not allowed to be set + parameters.set("ID_Transfert_LuxNet", 0) + assert len(parameters.queue) == 0 + + # Set something which is allowed to be set + parameters.set("ID_Einst_WK_akt", 0) + assert len(parameters.queue) == 1 + + 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 diff --git a/tests/test_visibilities.py b/tests/test_visibilities.py new file mode 100644 index 0000000..47bc22f --- /dev/null +++ b/tests/test_visibilities.py @@ -0,0 +1,14 @@ +"""Test suite for parameters module""" + +# pylint: disable=too-few-public-methods + +from luxtronik.visibilities import Visibilities + + +class TestVisibilities: + """Test suite for Visibilities""" + + def test_init(self): + """Test cases for initialization""" + visibilities = Visibilities() + assert visibilities.name == "Visibility" From 077543447dd8c3911634e46249cd85013b027017 Mon Sep 17 00:00:00 2001 From: Gerd Wachsmuth Date: Sat, 22 Jul 2023 23:19:26 +0200 Subject: [PATCH 06/13] Address issues by kbabioch --- luxtronik/data_vector.py | 9 ++++++++- tests/test_parameters.py | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/luxtronik/data_vector.py b/luxtronik/data_vector.py index 984b66d..96d2ee2 100644 --- a/luxtronik/data_vector.py +++ b/luxtronik/data_vector.py @@ -15,6 +15,7 @@ def __init__(self): self._data = {} def __iter__(self): + """Iterator for the data entries.""" return iter(self._data.items()) def parse(self, raw_data): @@ -30,7 +31,13 @@ def parse(self, raw_data): self._data[index] = entry def _lookup(self, target, with_index=False): - """Lookup entry by either id or name.""" + """ + 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 diff --git a/tests/test_parameters.py b/tests/test_parameters.py index feccf3e..c803852 100644 --- a/tests/test_parameters.py +++ b/tests/test_parameters.py @@ -11,6 +11,7 @@ class TestParameters: def test_init(self): """Test cases for initialization""" parameters = Parameters() + assert parameters.name == "Parameter" assert parameters.safe assert len(parameters.queue) == 0 From 9470de3267fad8c5f9aa3cb379b37f292044fcca Mon Sep 17 00:00:00 2001 From: 02curls <107803294+02curls@users.noreply.github.com> Date: Tue, 19 Sep 2023 20:17:07 +0200 Subject: [PATCH 07/13] Update parameters.py edited the 14,15,16 and 774,775,776 triplets, these are independent internal heating circuits parameters analogous with 11,12,13 triplet, 11 is analogous to 14 and 774 12 is analogous to 15 and 775 13 is analogous to 16 and 776 --- luxtronik/parameters.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/luxtronik/parameters.py b/luxtronik/parameters.py index 2826a52..28e3971 100755 --- a/luxtronik/parameters.py +++ b/luxtronik/parameters.py @@ -52,9 +52,9 @@ def __init__(self, safe=True): 11: Celsius("ID_Einst_HzHwHKE_akt", True), 12: Celsius("ID_Einst_HzHKRANH_akt", True), 13: Celsius("ID_Einst_HzHKRABS_akt", True), - 14: Unknown("ID_Einst_HzMK1E_akt"), - 15: Unknown("ID_Einst_HzMK1ANH_akt"), - 16: Unknown("ID_Einst_HzMK1ABS_akt"), + 14: Celsius("ID_Einst_HzMK1E_akt", True), + 15: Celsius("ID_Einst_HzMK1ANH_akt", True), + 16: Celsius("ID_Einst_HzMK1ABS_akt", True), 17: Unknown("ID_Einst_HzFtRl_akt"), 18: Unknown("ID_Einst_HzFtMK1Vl_akt"), 19: Unknown("ID_Einst_SUBW_akt"), @@ -812,9 +812,9 @@ def __init__(self, safe=True): 771: Unknown("ID_IP_PB_Slave_5"), 772: Unknown("ID_Einst_BwHup_akt_backup"), 773: Unknown("ID_Einst_SuMk3_akt"), - 774: Unknown("ID_Einst_HzMK3E_akt"), - 775: Unknown("ID_Einst_HzMK3ANH_akt"), - 776: Unknown("ID_Einst_HzMK3ABS_akt"), + 774: Celsius("ID_Einst_HzMK3E_akt", True), + 775: Celsius("ID_Einst_HzMK3ANH_akt", True), + 776: Celsius("ID_Einst_HzMK3ABS_akt", True), 777: Unknown("ID_Einst_HzMK3Hgr_akt"), 778: Unknown("ID_Einst_HzFtMK3Vl_akt"), 779: MixedCircuitMode("ID_Ba_Hz_MK3_akt", True), From c16e2ddc6ca7e77c6f4b4a68d3c92f90061bec36 Mon Sep 17 00:00:00 2001 From: Karol Babioch Date: Thu, 28 Sep 2023 23:39:04 +0200 Subject: [PATCH 08/13] Add new parameters related to operating time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This identifies and adds the according datatype to several parameters that have been identified to be related to the operating time of the machine. Essentially most of it is tracked in seconds, while some other parameters keep track of the counts. - `Zaehler` refers to the German word for counter. - `BetrZeit` refers to the German word for "operating time" - `WP` stands for "Wärmepumpe", the German word for heat pump. - `ZWE` refers to `Zusätzlicher Wärmeerzeuger`, essentially heating rods, etc. - `Imp` refers to `Impulse`, the German word for "impulses" and/or "cycles". - `Hz` refers to the German word "Heizung", i.e. "heating" - `BW` refers to the German word "Brauchwasser", i.e. hot water - `SW` refers to the German word `Schwimmwasser", i.e. swimming pool water - `Kue` refers to the Terman word "Kühlung", i.e. cooling. The necessary datatypes have already been available. --- luxtronik/parameters.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/luxtronik/parameters.py b/luxtronik/parameters.py index 2826a52..dd98d72 100755 --- a/luxtronik/parameters.py +++ b/luxtronik/parameters.py @@ -8,6 +8,7 @@ from luxtronik.datatypes import ( AccessLevel, Bool, + Count, Energy, Kelvin, Celsius, @@ -19,6 +20,7 @@ Minutes, MixedCircuitMode, PoolMode, + Seconds, SolarMode, Unknown, VentilationMode, @@ -706,14 +708,14 @@ def __init__(self, safe=True): 665: Unknown("ID_Einst_SuSwbTg_zeit_1_13"), 666: Unknown("ID_Einst_SuSwbTg_zeit_2_12"), 667: Unknown("ID_Einst_SuSwbTg_zeit_2_13"), - 668: Unknown("ID_Zaehler_BetrZeitWP"), - 669: Unknown("ID_Zaehler_BetrZeitVD1"), - 670: Unknown("ID_Zaehler_BetrZeitVD2"), - 671: Unknown("ID_Zaehler_BetrZeitZWE1"), - 672: Unknown("ID_Zaehler_BetrZeitZWE2"), - 673: Unknown("ID_Zaehler_BetrZeitZWE3"), - 674: Unknown("ID_Zaehler_BetrZeitImpVD1"), - 675: Unknown("ID_Zaehler_BetrZeitImpVD2"), + 668: Seconds("ID_Zaehler_BetrZeitWP"), + 669: Seconds("ID_Zaehler_BetrZeitVD1"), + 670: Seconds("ID_Zaehler_BetrZeitVD2"), + 671: Seconds("ID_Zaehler_BetrZeitZWE1"), + 672: Seconds("ID_Zaehler_BetrZeitZWE2"), + 673: Seconds("ID_Zaehler_BetrZeitZWE3"), + 674: Count("ID_Zaehler_BetrZeitImpVD1"), + 675: Count("ID_Zaehler_BetrZeitImpVD2"), 676: Unknown("ID_Zaehler_BetrZeitEZMVD1"), 677: Unknown("ID_Zaehler_BetrZeitEZMVD2"), 678: Unknown("ID_Einst_Entl_Typ_0"), @@ -766,9 +768,9 @@ def __init__(self, safe=True): 725: Unknown("ID_Switchoff_file_4_1"), 726: Unknown("ID_DauerDatenLoggerAktiv"), 727: Unknown("ID_Laufvar_Heizgrenze"), - 728: Unknown("ID_Zaehler_BetrZeitHz"), - 729: Unknown("ID_Zaehler_BetrZeitBW"), - 730: Unknown("ID_Zaehler_BetrZeitKue"), + 728: Seconds("ID_Zaehler_BetrZeitHz"), + 729: Seconds("ID_Zaehler_BetrZeitBW"), + 730: Seconds("ID_Zaehler_BetrZeitKue"), 731: Unknown("ID_SU_FstdHz"), 732: Unknown("ID_SU_FstdBw"), 733: Unknown("ID_SU_FstdSwb"), @@ -897,7 +899,7 @@ def __init__(self, safe=True): 856: Unknown("ID_Einst_Entl_Typ_13"), 857: Unknown("ID_Einst_Entl_Typ_14"), 858: Unknown("ID_Einst_Entl_Typ_15"), - 859: Unknown("ID_Zaehler_BetrZeitSW"), + 859: Seconds("ID_Zaehler_BetrZeitSW"), 860: Unknown("ID_Einst_Fernwartung_akt"), 861: Unknown("ID_AdresseIPServ_akt"), 862: Unknown("ID_Einst_TA_EG_akt"), From aa51bc38383f424089b4f5a75327cbe5deb58a57 Mon Sep 17 00:00:00 2001 From: Karol Babioch Date: Thu, 28 Sep 2023 23:39:14 +0200 Subject: [PATCH 09/13] Add support for parameters related to energy usage This adds newly identified parameters related to the energy usage. The appropriate parameters are currently named: ``` + 878: Energy("ID_Waermemenge_BW"), + 879: Energy("ID_Waermemenge_SW"), + 880: Timestamp("ID_Waermemenge_Datum"), ``` - `Waermemenge` refers to the German word for "amount of heat". - `BW` refers to `Brauchwasser`, the German word for hot water. - `SW` (probably) refers to `Schwimmwasser` the German word for pool water. This is also a mode that is supported. - `Datum` refers to "date", which is the date when this data has been reset the last time. Luckily the datatypes have already been available, so they are just assigned to those parameters. Since it doesn't make sense to write those parameters, safe mode is not turned on in these cases. --- luxtronik/parameters.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/luxtronik/parameters.py b/luxtronik/parameters.py index dd98d72..5a9c992 100755 --- a/luxtronik/parameters.py +++ b/luxtronik/parameters.py @@ -22,6 +22,7 @@ PoolMode, Seconds, SolarMode, + Timestamp, Unknown, VentilationMode, ) @@ -918,9 +919,9 @@ def __init__(self, safe=True): 875: Unknown("ID_WP_SerienNummer_HEX"), 876: Unknown("ID_WP_SerienNummer_INDEX"), 877: Unknown("ID_ProgWerteWebSrvBeobarten"), - 878: Unknown("ID_Waermemenge_BW"), - 879: Unknown("ID_Waermemenge_SW"), - 880: Unknown("ID_Waermemenge_Datum"), + 878: Energy("ID_Waermemenge_BW"), + 879: Energy("ID_Waermemenge_SW"), + 880: Timestamp("ID_Waermemenge_Datum"), 881: SolarMode("ID_Einst_Solar_akt", True), 882: Unknown("ID_BSTD_Solar"), 883: Celsius("ID_Einst_TDC_Koll_Max_akt"), From a0bb4c0a9e275da5946095b5dcb1097db71b882e Mon Sep 17 00:00:00 2001 From: Karol Babioch Date: Fri, 29 Sep 2023 13:56:21 +0200 Subject: [PATCH 10/13] parameters: Handle MK2 parameters analogoues to MK1 and MK2 This edits the heating curve parameters for `MK2` similar to `MK1` and `MK3` by defining the parameters as datatype `Celsius` and making it safe to write. --- luxtronik/parameters.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/luxtronik/parameters.py b/luxtronik/parameters.py index 28e3971..41ab4c7 100755 --- a/luxtronik/parameters.py +++ b/luxtronik/parameters.py @@ -179,9 +179,9 @@ def __init__(self, safe=True): 138: Unknown("ID_Einst_TV2VDSWB_akt"), 139: Unknown("ID_Einst_MinSwan_Time_akt"), 140: Unknown("ID_Einst_SuMk2_akt"), - 141: Unknown("ID_Einst_HzMK2E_akt"), - 142: Unknown("ID_Einst_HzMK2ANH_akt"), - 143: Unknown("ID_Einst_HzMK2ABS_akt"), + 141: Celsius("ID_Einst_HzMK2E_akt", True), + 142: Celsius("ID_Einst_HzMK2ANH_akt", True), + 143: Celsius("ID_Einst_HzMK2ABS_akt", True), 144: Unknown("ID_Einst_HzMK2Hgr_akt"), 145: Unknown("ID_Einst_HzFtMK2Vl_akt"), 146: Unknown("ID_Temp_THG_BwHD_saved"), From 5ab5e420c81ac24d7b6373255fcdf2181e0f1e6e Mon Sep 17 00:00:00 2001 From: Karol Babioch Date: Mon, 2 Oct 2023 10:31:50 +0200 Subject: [PATCH 11/13] Add placeholders for new datapoints This adds new parameters, calculations and visibilities as reported by the dump script after running it against a newly updated Luxtronik controller (3.89.1). For now the meaning of those datapoints is not known, hence they are added as unknown. --- luxtronik/calculations.py | 8 ++++++++ luxtronik/parameters.py | 27 +++++++++++++++++++++++++++ luxtronik/visibilities.py | 25 +++++++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/luxtronik/calculations.py b/luxtronik/calculations.py index 5896f52..e622e52 100755 --- a/luxtronik/calculations.py +++ b/luxtronik/calculations.py @@ -305,4 +305,12 @@ def __init__(self): 257: Power("Heat_Output"), 258: MajorMinorVersion("RBE_Version"), 259: Unknown("Unknown_Calculation_259"), + 260: Unknown("Unknown_Calculation_260"), + 261: Unknown("Unknown_Calculation_261"), + 262: Unknown("Unknown_Calculation_262"), + 263: Unknown("Unknown_Calculation_263"), + 264: Unknown("Unknown_Calculation_264"), + 265: Unknown("Unknown_Calculation_265"), + 266: Unknown("Unknown_Calculation_266"), + 267: Unknown("Unknown_Calculation_267"), } diff --git a/luxtronik/parameters.py b/luxtronik/parameters.py index 2826a52..80c5dbc 100755 --- a/luxtronik/parameters.py +++ b/luxtronik/parameters.py @@ -1164,6 +1164,33 @@ def __init__(self, safe=True): 1123: Unknown("Unknown_Parameter_1123"), 1124: Unknown("Unknown_Parameter_1124"), 1125: Unknown("Unknown_Parameter_1125"), + 1126: Unknown("Unknown_Parameter_1126"), + 1127: Unknown("Unknown_Parameter_1127"), + 1128: Unknown("Unknown_Parameter_1128"), + 1129: Unknown("Unknown_Parameter_1129"), + 1130: Unknown("Unknown_Parameter_1130"), + 1131: Unknown("Unknown_Parameter_1131"), + 1132: Unknown("Unknown_Parameter_1132"), + 1133: Unknown("Unknown_Parameter_1133"), + 1134: Unknown("Unknown_Parameter_1134"), + 1135: Unknown("Unknown_Parameter_1135"), + 1136: Unknown("Unknown_Parameter_1136"), + 1137: Unknown("Unknown_Parameter_1137"), + 1138: Unknown("Unknown_Parameter_1138"), + 1139: Unknown("Unknown_Parameter_1139"), + 1140: Unknown("Unknown_Parameter_1140"), + 1141: Unknown("Unknown_Parameter_1141"), + 1142: Unknown("Unknown_Parameter_1142"), + 1143: Unknown("Unknown_Parameter_1143"), + 1144: Unknown("Unknown_Parameter_1144"), + 1145: Unknown("Unknown_Parameter_1145"), + 1146: Unknown("Unknown_Parameter_1146"), + 1147: Unknown("Unknown_Parameter_1147"), + 1148: Unknown("Unknown_Parameter_1148"), + 1149: Unknown("Unknown_Parameter_1149"), + 1150: Unknown("Unknown_Parameter_1150"), + 1151: Unknown("Unknown_Parameter_1151"), + 1152: Unknown("Unknown_Parameter_1152"), } def set(self, target, value): diff --git a/luxtronik/visibilities.py b/luxtronik/visibilities.py index 6585aed..48b5708 100755 --- a/luxtronik/visibilities.py +++ b/luxtronik/visibilities.py @@ -370,4 +370,29 @@ def __init__(self): 352: Unknown("Unknown_Visibility_352"), 353: Unknown("Unknown_Visibility_353"), 354: Unknown("Unknown_Visibility_354"), + 355: Unknown("Unknown_Visibility_355"), + 356: Unknown("Unknown_Visibility_356"), + 357: Unknown("Unknown_Visibility_357"), + 358: Unknown("Unknown_Visibility_358"), + 359: Unknown("Unknown_Visibility_359"), + 360: Unknown("Unknown_Visibility_360"), + 361: Unknown("Unknown_Visibility_361"), + 362: Unknown("Unknown_Visibility_362"), + 363: Unknown("Unknown_Visibility_363"), + 364: Unknown("Unknown_Visibility_364"), + 365: Unknown("Unknown_Visibility_365"), + 366: Unknown("Unknown_Visibility_366"), + 367: Unknown("Unknown_Visibility_367"), + 368: Unknown("Unknown_Visibility_368"), + 369: Unknown("Unknown_Visibility_369"), + 370: Unknown("Unknown_Visibility_370"), + 371: Unknown("Unknown_Visibility_371"), + 372: Unknown("Unknown_Visibility_372"), + 373: Unknown("Unknown_Visibility_373"), + 374: Unknown("Unknown_Visibility_374"), + 375: Unknown("Unknown_Visibility_375"), + 376: Unknown("Unknown_Visibility_376"), + 377: Unknown("Unknown_Visibility_377"), + 378: Unknown("Unknown_Visibility_378"), + 379: Unknown("Unknown_Visibility_379"), } From f5423ba6991d04fa7366a49c1744285aef0f3264 Mon Sep 17 00:00:00 2001 From: Gerd Wachsmuth Date: Fri, 6 Oct 2023 22:17:05 +0200 Subject: [PATCH 12/13] More parameters identified as energies --- luxtronik/parameters.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/luxtronik/parameters.py b/luxtronik/parameters.py index cf116d8..354390c 100755 --- a/luxtronik/parameters.py +++ b/luxtronik/parameters.py @@ -893,9 +893,9 @@ def __init__(self, safe=True): 849: Unknown("ID_Ba_Hz_MK3_saved"), 850: Hours("ID_Einst_Kuhl_Zeit_Ein_akt", True), 851: Hours("ID_Einst_Kuhl_Zeit_Aus_akt", True), - 852: Unknown("ID_Waermemenge_Seit"), + 852: Energy("ID_Waermemenge_Seit"), 853: Unknown("ID_Waermemenge_WQ"), - 854: Unknown("ID_Waermemenge_Hz"), + 854: Energy("ID_Waermemenge_Hz"), 855: Unknown("ID_Waermemenge_WQ_ges"), 856: Unknown("ID_Einst_Entl_Typ_13"), 857: Unknown("ID_Einst_Entl_Typ_14"), @@ -1101,7 +1101,7 @@ def __init__(self, safe=True): 1057: Unknown("ID_Einst_Vorlauf_ZUP"), 1058: Unknown("ID_Einst_Abtauen_im_Warmwasser"), 1059: Energy("ID_Waermemenge_ZWE"), - 1060: Unknown("ID_Waermemenge_Reset"), + 1060: Energy("ID_Waermemenge_Reset"), 1061: Unknown("ID_Waermemenge_Reset_2"), 1062: Unknown("ID_Einst_Brunnenpumpe_min"), 1063: Unknown("ID_Einst_Brunnenpumpe_max"), From 2a9eaabeb9b44291e23328c229fdf0d7f01104ca Mon Sep 17 00:00:00 2001 From: Guzz-T <126437616+Guzz-T@users.noreply.github.com> Date: Fri, 21 Jul 2023 22:01:47 +0200 Subject: [PATCH 13/13] issue-130: Added a connect decorator for read/write functions --- luxtronik/__init__.py | 83 +++++++++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 31 deletions(-) diff --git a/luxtronik/__init__.py b/luxtronik/__init__.py index 9111635..c556f01 100755 --- a/luxtronik/__init__.py +++ b/luxtronik/__init__.py @@ -63,9 +63,23 @@ def __init__(self, host, port=LUXTRONIK_DEFAULT_PORT, safe=True): self._port = port self._safe = safe self._socket = None - self.read() + self._connect() def __del__(self): + self._disconnect() + + def _connect(self): + """Connect the socket if not already done.""" + is_none = self._socket is None + if is_none: + self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if is_none or is_socket_closed(self._socket): + self._socket.connect((self._host, self._port)) + LOGGER.info( + "Connected to Luxtronik heat pump %s:%s", self._host, self._port + ) + + def _disconnect(self): if self._socket is not None: if not is_socket_closed(self._socket): self._socket.close() @@ -74,43 +88,50 @@ def __del__(self): "Disconnected from Luxtronik heatpump %s:%s", self._host, self._port ) - def read(self): - """Read data from heatpump.""" - return self._read_after_write(parameters=None) - - def write(self, parameters): - """Write parameter to heatpump.""" - return self._read_after_write(parameters=parameters) - - def _read_after_write(self, parameters): + def _with_lock_and_connect(self, func, *args, **kwargs): """ - Read and/or write value from and/or to heatpump. - This method is essentially a wrapper for the _read() and _write() - methods. + Decorator around various read/write functions to connect first. + + This method is essentially a wrapper for the _read() and _write() methods. Locking is being used to ensure that only a single socket operation is performed at any point in time. This helps to avoid issues with the Luxtronik controller, which seems unstable otherwise. - If write is true, all parameters will be written to the heat pump - prior to reading back in all data from the heat pump. If write is - false, no data will be written, but all available data will be read - from the heat pump. + """ + with self._lock: + self._connect() + ret_val = func(*args, **kwargs) + # self._disconnect() + return ret_val + + def read(self): + """ + Read data from heat pump. + All available data will be read from the heat pump. + """ + return self._with_lock_and_connect(self._read) + + def read_parameters(self): + """Read parameters from heat pump.""" + return self._with_lock_and_connect(self._read_parameters) + + def read_calculations(self): + """Read calculations from heat pump.""" + return self._with_lock_and_connect(self._read_calculations) + + def read_visibilities(self): + """Read visibilities from heat pump.""" + return self._with_lock_and_connect(self._read_visibilities) + + def write(self, parameters): + """ + Write parameter to heat pump. + All parameters will be written to the heat pump + prior to reading back in all data from the heat pump. :param Parameters() parameters Parameter dictionary to be written to the heatpump before reading all available data - from the heatpump. At 'None' it is read only. + from the heat pump. """ - - with self._lock: - is_none = self._socket is None - if is_none: - self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - if is_none or is_socket_closed(self._socket): - self._socket.connect((self._host, self._port)) - LOGGER.info( - "Connected to Luxtronik heatpump %s:%s", self._host, self._port - ) - if parameters is not None: - return self._write(parameters) - return self._read() + return self._with_lock_and_connect(self._write, parameters) def _read(self): parameters = self._read_parameters()