Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[hdpowerview] Preparation for Generation 3 API #13352

Closed
wants to merge 5 commits into from

Conversation

andrewfg
Copy link
Contributor

@andrewfg andrewfg commented Sep 3, 2022

Background

See #12678

This is the first of (probably) three Pull Requests for integrating Hunter Douglas Generation 3 hubs into the binding (which currently only supports Generation 1 & 2 hubs).

Content of this Pull Request

The current binding has several "hub facing" DTO classes that are very specific to Generation 1 & 2 hubs. This Pull Request restructures those classes into two parts, namely..

  1. An abstract base class that declares the common interface methods.
  2. An implementation class that implements the Generation 1 & 2 hub specific code.

This means that the "OH facing" parts of the code can reference the abstract base classes independently of the actual implementation of the "hub facing" classes.

Since this Pull Request provides only the Generation 1 & 2 implementation classes, the functionality should not change as a result of this PR (i.e. only the code structure has changed). And the next forthcoming Pull Request will provide additional classes that implement the Generation 3 hub specific code.

Specifics of Changes

The following classes have been restructured into base plus implementation classes..

  • HDPowerViewWebTargets => HDPowerViewWebTargets (base) and HDPowerViewWebTargetsV1 (implementation)
  • ShadeData => ShadeData (base) and ShadeDataV1 (implementation)
  • ShadePosition => ShadePosition (base) and ShadePositionV1 (implementation)
  • Scene => Scene (base) and SceneV1 (implementation)
  • ScheduledEvent => ScheduledEvent (base) and ScheduledEventV1 (implementation)

The HDPowerViewWebTargets base class has been refactored so that it can generically support deserialization of any version of the implementation classes. i.e. the current V1 implementation classes, the forthcoming V3 implementation classes, and indeed any future version yet to come.

And various minor changes have been made in the following classes as follows..

  • Handler classes check the implementation class version to avoid code that Generation 3 will not support (e.g. battery voltage)
  • Some builder classes are hardwired to the V1 implementations
  • JUnit test classes are hard wired to test the V1 implementations

Java Package Structure

In cases where the classes have been split into base and implementation classes, the base class remains in the package where the original combined class was located, and the split V1 implementation class is now in a ._v1 sub package. And in future the V3 implementations will be added to a ._v3 sub package.

Signed-off-by: Andrew Fiddian-Green [email protected]

@andrewfg andrewfg self-assigned this Sep 3, 2022
@andrewfg andrewfg marked this pull request as draft September 3, 2022 14:21
@andrewfg andrewfg changed the title [hdpowerview] Preparation for generation 3 [hdpowerview] Preparation for Generation 3 API Sep 3, 2022
@andrewfg andrewfg added the enhancement An enhancement or new feature for an existing add-on label Sep 3, 2022
@andrewfg andrewfg marked this pull request as ready for review September 3, 2022 14:36
Signed-off-by: Andrew Fiddian-Green <[email protected]>
Signed-off-by: Andrew Fiddian-Green <[email protected]>
@andrewfg
Copy link
Contributor Author

For testing purposes the Jar file is here..

org.openhab.binding.hdpowerview-3.4.0-SNAPSHOT.jar.zip

@andrewfg andrewfg added rebuild Triggers Jenkins PR build and removed rebuild Triggers Jenkins PR build labels Sep 16, 2022
@lolodomo
Copy link
Contributor

lolodomo commented Oct 20, 2022

Partial review done with no comment.
Only remains to review the 2 WebTargets classes.

@jlaur
Copy link
Contributor

jlaur commented Oct 20, 2022

Partial review done with no comment.
Only remains to review the 2 WebTargets classes.

@andrewfg - I recently started looking into this also, continuing from #12678. I'm preparing some notes and also wanted to prepare some implementation propositions, but didn't get to this yet.

Since you started the review, @lolodomo, I'll just for now share some general preliminary comments.

First, the new API is actually a completely new API. It seems to me that it is more different than it is similar. Therefore my first suggestion would be to keep the DTO's completely separate. In the current approach they are entangled, which makes it harder to maintain. Bugs can be introduced in Gen 2 when making changes for Gen 3 and vice versa. On top of that there is a runtime performance penalty.

This leads to the next layers: WebTargets and handlers. Currently DTO's are exposed (passed) to the handlers. This might be the reason for current approach? But actually situation is quite similar here. So if these would be separated also, the DTO problem would be solved.

In other words, I think it would be worthwhile for the long term to make a more distinct separation in the implementation of the two API's. I could contribute, and of course also provide extended explanations and thoughts.

@andrewfg, sorry for taking such a long time. Really wanted to provide some more concrete proposal before commenting on your approach.

@jlaur
Copy link
Contributor

jlaur commented Oct 20, 2022

Draft notes comparing shade.

shade Gen 2:

  • position
  • secondary
  • vane
  • lowBattery
  • batteryLevel
  • batteryVoltage
  • signalStrength (0-4)
  • hubRssi
  • repeaterRssi

Properties:

  • capabilities
  • batteryKind

shade Gen 3:

  • position
  • secondary
  • tilt
  • velocity
  • lowBattery
  • batteryLevel
  • signalStrengthRssi (dBm)

Properties:

  • capabilities
  • bleName
  • batteryKind (powerType)

Difference - for example batterykind:

Gen 2:

  • JSON: batteryKind
  • Values: 1 = hardwired, 2 = battery, 3 = rechargeable battery

Gen 3:

  • JSON: powerType
  • Values: 0 = battery, 1 = hardwired, 2 = rechargeable

@andrewfg
Copy link
Contributor Author

andrewfg commented Oct 20, 2022

the new API is actually a completely new API. It seems to me that it is more different than it is similar.
Therefore my first suggestion would be to keep the DTO's completely separate.

Hmm. I am not sure that I completely agree with you on this.

The exercise with HDPowerview Gen1/2 vs Gen 3 is a very similar exercise to the one needed for Philips Hue API V1 vs CLIP 2 #13570. Originally for the Hue case I started with the intention to 'overlay' the new and old DTO's on top of each other. However in the Hue case, the DTO's, and indeed the architecture is really totally different, and for that reason I had to abandon the 'overlaying' approach and start with essentially a whole new architecture. By contrast in the HDPowerview I WAS able to maintain the 'overlaying' approach, and therefore for that reason, I am in two minds about whether I agree with you or not.

Notwithstanding the above, during the writing of #13570 (which was a MASSIVE exercise) I did learn some things, that I might reverse apply to the HDPowerview case..

@lolodomo
Copy link
Contributor

@jlaur : I was not aware you were studying this PR. You should have tell us.
As your analysis is largely more in depth than mine, I let you finish the review.

@andrewfg
Copy link
Contributor Author

@jlaur just a couple of pointers..

@andrewfg
Copy link
Contributor Author

andrewfg commented Oct 20, 2022

^
Also @jlaur remember that the class structure document might help you - EDIT: BLUE and GREEN columns are #13352 and ORANGE column is #13355

Class Structure.pdf

image

@jlaur
Copy link
Contributor

jlaur commented Oct 20, 2022

  • primarily it simply refactors the Gen 1/2 DTO classes from a 'spaghetti' form (with private inner DTO classes embedded inside other outer DTO classes), into a flat form where each inner and outer DTO class is its own separate Java class.

This part could even be extracted to a separate PR which I would approve and merge immediately. I completely agree with this cleanup operation.

@andrewfg
Copy link
Contributor Author

^
Ok. Let me think about that.

@andrewfg
Copy link
Contributor Author

^
I think on reflection that this PR is actually the “easy” PR that you could approve and merge easily. The second PR is the one with the new stuff in it. Probably what makes you think that this PR is “difficult” is the fact that I moved many classes into different folders for purposes on nomenclature. So it looks like there are many deleted files and many new ones, when in fact that is not so. Therefore I think I will try to reformulate the folder structure so that instead of seeing a whole file deleted and added, you would instead just see the diffs within existing files in the same folders. WDYT?

@andrewfg
Copy link
Contributor Author

^
More specifically many of the prior classes were split into a 'base' (abstract/interface/tag) class (and a 'xxV1' class; whereby the base class took over the old class name and the v1 class appears as a new file. My proposal would be to change it so that the 'base' (abstract/interface/tag) classes would have the name 'xxxAbstract' and the 'V1' classes would revert to its old original class name. That way would enable you to see the unchanged functionality within the class structore of v1 functionality without confusion over their class names having changed. Or something like that.

@andrewfg
Copy link
Contributor Author

^
I think I should probably do the above anyway.

@jlaur
Copy link
Contributor

jlaur commented Oct 22, 2022

^ I think on reflection that this PR is actually the “easy” PR that you could approve and merge easily. The second PR is the one with the new stuff in it. Probably what makes you think that this PR is “difficult” is the fact that I moved many classes into different folders for purposes on nomenclature. So it looks like there are many deleted files and many new ones, when in fact that is not so. Therefore I think I will try to reformulate the folder structure so that instead of seeing a whole file deleted and added, you would instead just see the diffs within existing files in the same folders. WDYT?

IMHO you should not change any code just in order to reduce diffs. Code is as it is. :-) For that purpose commits can be used, i.e. moving files in one commit and changing them in a separate commit. Then a PR can be easier to read commit by commit. But that's more a general comment/tip. That said, the structure needs some minor tweaking to avoid checkstyle issues, for example "_v1" does not conform to naming convention because of the underscore.

I don't have problems reading the changes, the reason why I cannot quickly approve this PR is because we need to discuss the architectural approach as mentioned here.

As an example. let's say I want to understand what's going on from receiving a payload and until it lands in the handler. As an example, ShadeData. These are the steps I could take:

In method pollShades() in HDPowerViewHubHandler:

        Shades shades = webTargets.getShades();

Step into this method. It's abtract. Now find relevant implementation: HDPowerViewWebTargetsV1:

            Shades shades = gson.fromJson(json, Shades.class);

Seems straight-forward. Hmm, but something is fishy - in HDPowerViewShadeHandler:

    protected void onReceiveUpdate(ShadeData shadeData) {
        isGeneration1 = shadeData.version() == 1;

Let's check the version method:

    public abstract int version();

Okay, so now let's find the relevant implementation: ShadeDataV1. But how did deserialization work then? Some digging:

    public HDPowerViewWebTargetsV1(HttpClient httpClient, String ipAddress) {
        super(httpClient, ipAddress, SceneV1.class, ShadeDataV1.class, ShadePositionV1.class, ScheduledEventV1.class);

Let's track this in super constructor:

    public HDPowerViewWebTargets(HttpClient httpClient, String ipAddress, Class<? extends Scene> sceneClass,
            Class<? extends ShadeData> shadeDataClass, Class<? extends ShadePosition> shadePositionClass,
            Class<? extends ScheduledEvent> scheduledEventClass) {
        this.httpClient = httpClient;
        this.sceneClass = sceneClass;
        this.shadeDataClass = shadeDataClass;
        this.shadePositionClass = shadePositionClass;
        this.scheduledEventClass = scheduledEventClass;
        this.gson = new GsonBuilder()
        // @formatter:off
                .registerTypeAdapter(Scene.class, new SceneDeserializer())
                .registerTypeAdapter(ShadeData.class, new ShadeDataDeserializer())
                .registerTypeAdapter(ShadePosition.class, new ShadePositionDeserializer())
                .registerTypeAdapter(ShadePosition.class, new ShadePositionSerializer())
                .registerTypeAdapter(ScheduledEvent.class, new ScheduledEventDeserializer())
        // @formatter:on
                .create();
    }

So we have a type adapter registered, and we store a derivative of ShadeData. Let's look:

    private class ShadeDataDeserializer implements JsonDeserializer<ShadeData> {
        @Override
        public @Nullable ShadeData deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
                throws JsonParseException {
            JsonObject jsonObject = json.getAsJsonObject();
            return context.deserialize(jsonObject, shadeDataClass);
        }
    }

For me that seems like an overly complex solution for trying to avoid a bit of duplication from two slightly overlapping, but different, API's. It's hard to read/understand (more classes to look into), hard to debug and more error-prone.

We can leave this for a moment. I was planning to create a concrete example of an alternative more decoupled architectural approach, and I would still like to do that, I just need a bit of time. My hope is that it would make the discussion easier/more understandable, as code says more than a thousand words. :-)

@andrewfg
Copy link
Contributor Author

andrewfg commented Oct 23, 2022

^
Ok. Just to be clear, are you saying that instead of thie following..

                                / ===> V1 DTO subclass
abstract/base/tag DTO class ==>
                                \ ===> V2 DTO subclass

... you want this instead ??? ..

===> V1 DTO class

===> V2 DTO class

.. if that is the case, (which would be the way I am doing it for the Hue CLIP 2), then we would have completely different DTO's for Gen 3, and therefore also a completely new thing handler for Shades. Or?


EDIT: and if the above would be the case, (i.e. completely new thing handler), then it is also a completely new bridge handler, so .. err .. basically .. a completely new binding, with no commonalities in code between the two versions. Or ??

@jlaur
Copy link
Contributor

jlaur commented Oct 23, 2022

^ Ok. Just to be clear, are you saying that instead of thie following..

                                / ===> V1 DTO subclass
abstract/base/tag DTO class ==>
                                \ ===> V2 DTO subclass

... you want this instead ??? ..

===> V1 DTO class

===> V2 DTO class

.. if that is the case, (which would be the way I am doing it for the Hue CLIP 2), then we would have completely different DTO's for Gen 3

Yes, exactly.

and therefore also a completely new thing handler for Shades. Or?

Not necessarily, but after looking at what we already have for Gen 2, I'm tended to prefer that approach. However, it would also be possible to introduce new classes (as opposed to the DTO classes) for sharing common objects, but I think we wouldn't benefit much from it. I went through all shade handler methods recently, and didn't find much of common use, i.e. most methods are quite implementation specific and tied to Gen 2 IMHO. The same goes for WebTargets which is just a controller connecting the handler with the endpoints through DTO's - and these endpoints and DTO's are different. It also contains some handling of "maintenance period", but that's probably also Gen 2-specific.

EDIT: and if the above would be the case, (i.e. completely new thing handler), then it is also a completely new bridge handler, so .. err .. basically .. a completely new binding, with no commonalities in code between the two versions. Or ??

Yes and no. :-) It would still be the same binding, but the discovery would detect either Gen 2 Hub or Gen 3 Gateway and create a corresponding bridge. For Gen 3 this bridge would also contain some SSE options probably, and later there might be other differences from the current hub bridge.

This doesn't mean the bridge or shade handlers cannot share code. Of course they can, and we should of course avoid any kind of duplication. But please have a look and see if you can find actual code snippet candidates for being shared?

@andrewfg
Copy link
Contributor Author

Ok. Let me think about it. But my first reaction is that probably we should delete this PR ("preparation for..") because essentially the old code should be kept as close to what it is already, and the second PR would then become "as replacement of.. resp. in addition to.." the existing code. Or ??

@andrewfg
Copy link
Contributor Author

This PR is no longer required, so closing now.

@andrewfg andrewfg closed this Oct 30, 2022
@andrewfg andrewfg deleted the hdpowerview-generation3-pr1 branch August 25, 2024 11:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement An enhancement or new feature for an existing add-on
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants