Bouni / python-luxtronik

python-luxtronik is a library that allow you to interact with a Luxtronik heatpump controller.
MIT License
38 stars 20 forks source link

Ensure backwards compatibility #187

Open Guzz-T opened 3 weeks ago

Guzz-T commented 3 weeks ago

With the changes from these pull requests, the corresponding entries can be found under the old names again. This is possible because you can now assign more than one name to the entries.

The names are passed directly to the constructor. No additional files / additional constants are required. It is therefore immediately clear which names can be used to find the entry.

The current version can now be exchanged with version 0.3.14. Tested with BenPru/luxtronik/2024.10.5-beta on HA 2024.10.4 (with own bugfix for https://github.com/BenPru/luxtronik/pull/283). A new release could then be created (0.4.0?)

Fixes #168. Replaces #171.

github-actions[bot] commented 3 weeks ago

Coverage

Coverage Report
FileStmtsMissCoverMissing
luxtronik
   __init__.py1841194%42–43, 46–51, 257–258, 263
   __main__.py21210%3–49
   calculations.py23196%322
   datatypes.py290199%107
   discover.py403415%21–69
   parameters.py21195%1204
   visibilities.py12192%403
luxtronik/scripts
   dump_changes.py43430%7–85
   dump_luxtronik.py26260%6–52
TOTAL71313981% 

Tests Skipped Failures Errors Time
131 0 :zzz: 0 :x: 0 :fire: 7.371s :stopwatch:
Bouni commented 3 weeks ago

I'll wait for @gerw s review on this before merging it

gerw commented 3 weeks ago

I am a little bit biased, because I wrote the previous PR #171. I have had a careful look at both PRs and I must admit that I like my PR more. In my opinion,

Guzz-T commented 3 weeks ago
  • it is cleaner to have the compatibilities in an extra file,

Everyone has their own coding style :) In my opinion, an additional file is more difficult to maintain. Especially since sometimes 3 names are used (see HUP). If you want to assign a new name here, all you have to do is enter the name at the top of the list. Additionally, in this version, the check for obsolete names can also be done outside of the data vector.

  • it is not so nice that Base._name has not a 'stable' type (str vs list of str)

To get around this, we could add the following:

if isinstance(name, list):
    self._name = name
else:
    self._name = [name]

Then at least self._name would be static. Or we could always hand over a list. This would then usually only consist of one element.

  • my test cases are more elaborate and make sure that no further incompatibilities arise in the future.

I think we could also use the test code for this.

BTW, your test doesn't reveal the bug in Visibilities with the index > 354. there is simply no such thing as 100% test coverage :)

Guzz-T commented 3 weeks ago

Updated source code to:

gerw commented 3 weeks ago

I still think it is more in the style of this library, if the compatibilities reside in their own file. Note that the constants have been moved to a separate file some time ago. In the same spirit, the compatibilities should be in a separate file and should not "pollute" other files.

I do not see the benefit of this new PR over #171

What do the others say in this regard? @Bouni @kbabioch

Guzz-T commented 3 weeks ago

Please note that the current implementation in #171 cannot handle further name changes. As an example, if you change a name from a to b to c. Then you first had to add a: b. However, as soon as you switch to c (and add b: c), the look-up with a no longer works because b would then no longer be found either. Maybe you could:

In addition, you may run into problems if entries from two different data vectors have the same name.

Of course it makes sense to put the constants in an extra file. However, the goal of that is to remove or reduce dependencies between the files.

I see the advantage of my implementation as:

The concrete implementation of the data vectors is now nothing more than the assignment of names to the indices. I see no reason that outdated names need to be outsourced.

Bouni commented 3 weeks ago

I just looked at both, this PR and #171

I like the idea of @Guzz-T to have names as a list and have the first element to be the prefered one and all after that the obsolete names.

On the other hand, I agree with @gerw that we should have these in a seperate constants file.

How about we merge both approaches?

We have a constants file like this (with a tubple of tuples for each, parameters, calculations and visibilities):

LUXTRONIK_PARAMETER_COMPATIBILITIES = (
    ("New_fancy_name", "ID_Einst_SuSilence", "Unknown_Parameter_1092"), 
    ("ID_Einst_SilenceTimer_0", "Unknown_Parameter_1093"),  
    ("ID_Einst_SilenceTimer_1", "Unknown_Parameter_1094"),
    ("ID_Einst_SilenceTimer_2", "Unknown_Parameter_1095"),
    ...
)

Then we change the 3 categories (parameters for example) like so:

...
self._data = {
            0: Unknown(),
            1: Celsius(True),
            2: Celsius(True),
            3: HeatingMode(True),
            4: HotWaterMode(True),
...

We then lookup the possible names by the number of the parameter, calculation or visibility.

If the lookup is done via name, we have to iterate over the const until we find the right entry, but that has to be done in either of your approaches anyway if I'm right.

What do you think?

Guzz-T commented 3 weeks ago

With this proposal, it is even more difficult for the user to look up the assignment of the names to the data types.

Should the user work with the constants at all? If not, then I would not implement constants.

gerw commented 3 weeks ago

I would also prefer if parameters.py would contain the name, because this is where the user could look up the names. Moreover, it would be nice if this file stays as simple at possible. What about changing compatibilities.pyto use something like

LUXTRONIK_COMPATIBILITIES = {
    "ID_Einst_SuSilence" : ["Unknown_Parameter_1092", "bar", "foo"],
    ....
}

where ID_Einst_SuSilence is the current (preferred name) and the list contains the old / obsolete names? Then, there is no need to change parameters.py.

Guzz-T commented 2 weeks ago

I would also prefer if parameters.py would contain the name, because this is where the user could look up the names.

For the same reason I would also include the “alternative” names in parameter.py.

only if you insist on outsourcing the “alternative” names. In this case I would like your current suggestion better.

If so, how should we deal with the following:

gerw commented 2 weeks ago

I would also prefer if parameters.py would contain the name, because this is where the user could look up the names.

For the same reason I would also include the “alternative” names in parameter.py.

But the user does not have to look up the old names?

* identical names in different data vectors (parameter and calculation for example)

Is this really an issue? Is there some example where we have a name clash? (I know that 0.3.14 uses Unknown_Parameters_# also for visibilities. This seems to be a weak argument, since I do not know any use for the visibilities.)

* Should we support functions like in this pull-request `Base.get_supported_names()` and `Base.check_name()`

I am not sure. In my opinion, it would be enough to access the current name of a property, but this should be possible by

parameters.get("outdated_name").name
Guzz-T commented 2 weeks ago

But the user does not have to look up the old names?

I mean, anyone who has worked with this repository is used to looking in the parameters.py (or the others) to see what fields and data types there are. The users who already use the old names will wonder why they are no longer there and their code still works (= very magic code).

Is this really an issue? Is there some example where we have a name clash? (I know that 0.3.14 uses Unknown_Parameters_# also for visibilities. This seems to be a weak argument, since I do not know any use for the visibilities.)

That's exactly what I thought of first. See BenPru's usage of visibilities.Unknown_Parameter_357 which is titled as V0357_SILENT_MODE_TIME_MENU. So we would need an entry "Unknown_Parameter_357": "Silent_mode_time_menu" for the visibilites and "Unknown_Parameter_357": "ID_Einst_SuMk225_zeit_0_2" for the parameters.

Guzz-T commented 2 weeks ago

[Update]: This comment focuses more on the topic of "Direct Interchangeability of the Two Versions."

[Original]: The longer I think about the topic, I think we also need to support earlier data types. Consider this example:

op_mode = lux.calculations.get("Unknown_Calculation_80")  # outdated name for "ID_WEB_WP_BZ_akt"
op_mode.raw = 1             # op_mode.value returns "1"
if op_mode.value == 1:     # evaluates to "true"
    ...

In this example, an object of type Unknown is returned. When switching to the latest version, you now get an object of type OperationMode. Then:

op_mode = lux.calculations.get("Unknown_Calculation_80") 
op_mode.raw = 1             # op_mode.value returns "hot water"
if op_mode.value == 1:     # evaluates to "false"
    ...

This construct would be a possible solution:

self._data = {
     ...
    80: [OperationMode("ID_WEB_WP_BZ_akt"), 
        Unknown("Unknown_Calculation_80")],
    ...

or

self._data = (
     ...
    ([38], OperationMode("ID_WEB_WP_BZ_akt")), 
    ([38], Unknown("Unknown_Calculation_80")),
    ([81..90], IPv4Address("ID_WEB_SoftStand")),
    ...

To be able to handle type changes without name changes, we could implement a factory pattern (something like this):

class CalculationsFactory:

    @classmethod
    def get_calculations_data(cls, interface_version):
        if interface_version == "v1":
            return {
                38: Unknown("ID_WEB_BZ_akt"),
                ...
                }
        if interface_version == "v2":
            return {
                38: OperationMode("ID_WEB_BZ_akt"),
                ...
                }

class Calculations(DataVector):

    def __init__(self, interface_version = "v2"):
        super().__init__()
        self._data = CalculationsFactory.get_calculations_data(interface_version)
Guzz-T commented 6 days ago

Some of the changes in this pull request go beyond just informing the user about the new names. My focus here was on the direct interchangeability of the versions. However, this is not strictly necessary. Do you see it differently?

As a result, I created pull request #191.