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

[mideaac] Initial contribution- second try #17749

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

apella12
Copy link
Contributor

This is a second attempt. The first attempt is closed #17395. Essentially all of the previous comments have been addressed. The main new change is the separation of the connection manager and the MideaACHandler, (largely rewritten by @lsiepel) these two files are basically new. I have worked and tested with this version (made some modifications) for several weeks and it seems to work as well as the first attempt now. Also there were a number of new supporting files added by @lsiepel.

For reference the previous intro is here.
I’d like to put this out for new binding review.
This binding was started by the original author as a forum topic but has since had other contributors. I picked it up when I installed a mini-split AC that used the Midea protocol earlier this year, originally to allow for longer polling frequencies. Then as a retirement challenge (no formal java training), decided to attempt to clean it up for official status. I cleaned up the summary report from over 150 items and believe I conformed to current developer guidelines (Java docs, naming, tests, etc.). Along the way added some additional functionality and corrected some code issues.
The discovery and security classes remain largely unchanged from the original author (who I haven’t been able to contact), and work well. The other classes I understand pretty well at this point and hopefully can address any concerns.
As a note, to conform to the guidelines there are breaking changes in naming from the version I published on the forum thread.

@apella12 apella12 requested a review from a team as a code owner November 15, 2024 15:08
@apella12 apella12 changed the title [Mideaac] Initial contribution- second try [mideaac] Initial contribution- second try Nov 15, 2024
@jlaur jlaur added the new binding If someone has started to work on a binding. For a new binding PR. label Nov 15, 2024
@apella12 apella12 force-pushed the separate-connection-manager branch 2 times, most recently from 8fae5fa to 44bb90f Compare December 17, 2024 21:34
@apella12 apella12 force-pushed the separate-connection-manager branch from 4ef6b08 to 071c1e7 Compare January 3, 2025 15:29
@lsiepel
Copy link
Contributor

lsiepel commented Jan 3, 2025

This is not forgotten, the time I have to spare is consumed by the ZUI binding at the moment. So don’t wait for me.

@apella12
Copy link
Contributor Author

apella12 commented Jan 3, 2025

@lsiepel Well I'm not really waiting. I'm working on other things too. There shouldn't be much to review after you rewrote the connection manager. I did a few things to make it more robust to run without the A/C connected to the cloud and fixed some things that you left hanging. I have had it polling for months without issues. It is not exactly A/C season, so no one is exactly clamoring for an update. Hopefully you or someone else can have a look in a couple of months as we get towards spring. EDIT: I did update the version to OH5 and change the date to 2025 in the headers

@apella12 apella12 force-pushed the separate-connection-manager branch from 7350e50 to 02ae747 Compare February 19, 2025 14:56
@lsiepel
Copy link
Contributor

lsiepel commented Feb 19, 2025

Forgot about this one, sorry. I might have some time to review in the next days.
It was tested and all works as expected?

@lsiepel lsiepel self-requested a review February 19, 2025 15:29
@apella12
Copy link
Contributor Author

Yes, it is working fine. I have been using it since the last commit. Decided to rebase today in case something in the OH5.0 causes a problem, but it passed checks

Copy link
Contributor

@lsiepel lsiepel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for not leaving this PR/binding. It has improved, i also see more room for improvement. Besides some new minor issues that i probably missed before, i think the DTO and connection structure can benefit from some more work and thinking.

} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
"Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error"));
logger.warn("Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this logging is not needed when the thing state is set.

Suggested change
logger.warn("Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See question above

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logger.warn("Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok in next commit

try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
logger.debug("An interupted error (socket retry) has occured {}", ex.getMessage());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stop processing and exit immediate when an InterruptException occurs.

Copy link
Contributor Author

@apella12 apella12 Feb 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I recall that won't work as I intended (with 3 tries). The exceptions aren't serious and are ephemeral in nature. I haven't had that INFO log message in months (after I went from 2 to three tries. That's why I forgot it was there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An InterruptException usually signals an external call to stop the proces / exit the application /shutdown. And therefor the method should exit fast cleaning up any resources but surely not trying to perform another retry attempt.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have separated the catches

Copy link
Contributor

@lsiepel lsiepel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marked some previous comments as resolved. Found some new ones, but nothing big. Let's slowly continue and narrow the remaining issues down and fix them together.

Comment on lines 167 to 186
if (!deviceIsConnected) {
logger.info("Connected to IP {}", ipAddress);
}
logger.debug("Connected to IP {}", ipAddress);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like double logging. I guess this would be better:

Suggested change
if (!deviceIsConnected) {
logger.info("Connected to IP {}", ipAddress);
}
logger.debug("Connected to IP {}", ipAddress);
logger.debug("Connected to IP {}: {}", ipAddress, deviceIsConnected);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What the intention here was one INFO for the first time and debug if it was recovering from some problem, but not disconnected. Otherwise there is nothing in the log to show it is running. Note starting date.
2025-01-24 16:45:59.796 [INFO ] [nternal.connection.ConnectionManager] - Connected to IP 192.168.0.246
This is when I updated to the rebased binding
2025-02-19 10:58:07.986 [INFO ] [nternal.connection.ConnectionManager] - Connected to IP 192.168.0.246
This is when I deleted the email and password this morning to recheck that it could be done
2025-02-20 08:54:25.405 [INFO ] [nternal.connection.ConnectionManager] - Connected to IP 192.168.0.246

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment to explain

sendCommand(command, callback);
} else {
droppedCommands = droppedCommands + 1;
logger.info("Problem with reading response, skipping {} skipped count since startup {}", command,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logger.info("Problem with reading response, skipping {} skipped count since startup {}", command,
logger.debug("Problem with reading response, skipping {} skipped count since startup {}", command,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. I used that to track health, but probably no value to user

connectionManager.sendCommand(CommandHelper.handleOffTimer(command, lastresponse), callbackLambda);
}
} catch (MideaConnectionException | MideaAuthenticationException e) {
logger.warn("Unable to proces command: {}", e.getMessage());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an authentication exception occurs, i guess it is permanenet. When a timeout occurs, it is transient and the binding should recover. You might want to handle both cases seperate.

Suggested change
logger.warn("Unable to proces command: {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok This was part of the code you rewrote and have never seen an exception, so don't know what else to say.

} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
"Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error"));
logger.warn("Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logger.warn("Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error");

if (!config.isValid()) {
logger.warn("Configuration invalid for {}", thing.getUID());
if (config.isDiscoveryNeeded()) {
logger.warn("Discovery needed, discovering....{}", thing.getUID());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logger.warn("Discovery needed, discovering....{}", thing.getUID());


## Discovery

Once the Air Conditioner is on the network (WiFi active) the other required parameters can be discovered automatically.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you provide a lot more information then in the readme. This is what chatgpt makes out of it. Please check and adapt the text so that it gives the user enough guidance to set it up correctly.l

The key parameters—like deviceId, token, key, and version—are automatically discovered once the air conditioner is connected to the WiFi and you’ve provided the necessary cloud and password (unless it's a version 2 device).

However, you’ll still need to manually enter a few parameters such as: Polling time, Socket timeout, Prompt tone.

Comment on lines 105 to 113
Frame label="AC Unit" {
Text item=outdoor_temperature label="Outdoor Temperature [%.1f °F]"
Text item=indoor_temperature label="Indoor Temperature [%.1f °F]"
Setpoint item=target_temperature label="Target Temperature [%.1f °F]" minValue=63.0 maxValue=78 step=1.0
Switch item=power label="Midea AC Power"
Switch item=temperature_unit label= "Temp Unit" mappings=[ON="Fahrenheit", OFF="Celsius"]
Selection item=fan_speed label="Midea AC Fan Speed"
Selection item=operational_mode label="Midea AC Mode"
Selection item=swing_mode label="Midea AC Louver Swing Mode"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Frame label="AC Unit" {
Text item=outdoor_temperature label="Outdoor Temperature [%.1f °F]"
Text item=indoor_temperature label="Indoor Temperature [%.1f °F]"
Setpoint item=target_temperature label="Target Temperature [%.1f °F]" minValue=63.0 maxValue=78 step=1.0
Switch item=power label="Midea AC Power"
Switch item=temperature_unit label= "Temp Unit" mappings=[ON="Fahrenheit", OFF="Celsius"]
Selection item=fan_speed label="Midea AC Fan Speed"
Selection item=operational_mode label="Midea AC Mode"
Selection item=swing_mode label="Midea AC Louver Swing Mode"
Frame label="AC Unit" {
Text item=outdoor_temperature label="Outdoor Temperature [%.1f °F]"
Text item=indoor_temperature label="Indoor Temperature [%.1f °F]"
Setpoint item=target_temperature label="Target Temperature [%.1f °F]" minValue=63.0 maxValue=78 step=1.0
Switch item=power label="Midea AC Power"
Switch item=temperature_unit label= "Temp Unit" mappings=[ON="Fahrenheit", OFF="Celsius"]
Selection item=fan_speed label="Midea AC Fan Speed"
Selection item=operational_mode label="Midea AC Mode"
Selection item=swing_mode label="Midea AC Louver Swing Mode"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

Comment on lines 106 to 114
Text item=outdoor_temperature label="Outdoor Temperature [%.1f °F]"
Text item=indoor_temperature label="Indoor Temperature [%.1f °F]"
Setpoint item=target_temperature label="Target Temperature [%.1f °F]" minValue=63.0 maxValue=78 step=1.0
Switch item=power label="Midea AC Power"
Switch item=temperature_unit label= "Temp Unit" mappings=[ON="Fahrenheit", OFF="Celsius"]
Selection item=fan_speed label="Midea AC Fan Speed"
Selection item=operational_mode label="Midea AC Mode"
Selection item=swing_mode label="Midea AC Louver Swing Mode"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created a new comment with a suggestion. I meant to add indentation not removing a curly brace.

try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
logger.debug("An interupted error (socket retry) has occured {}", ex.getMessage());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An InterruptException usually signals an external call to stop the proces / exit the application /shutdown. And therefor the method should exit fast cleaning up any resources but surely not trying to perform another retry attempt.

@apella12
Copy link
Contributor Author

apella12 commented Feb 20, 2025

In no particular order, 1) I'm not sure how to use the OH standard conversions and where to put the .toLowerCase since I do not see that in the documentation. 2) The exception in the retry I'm trying to address is simply a timeout. 3) Elsewhere, I was trying to avoid breaking something that I fixed earlier would cause me to go back and fix again. It has been months and I'm not recalling all the issues I had getting to the fix. 4) I also attached a log of the DTO actions, it appears to me everything is needed. The only logic is to pull from the cloud responses to compose the next message.

EDIT: Regaring other comments;
First about the blocking method in the initialize(). The issue is that V.3 discovery is two steps. First step is to get the IP, port, deviceId and set the defaults. The V.3 initialization starts but hits a wall until the cloud provider, email and password are entered on the UI page (V.2 OK). That springs the method to get the token and key. We could force textual configuration and the use of a third party platform to obtain the token and key, but that doesn’t seem to fit the OH model.

Second I retested a change to remind myself of the problem. I dropped the return after getTokenKeyCloud(cloudProvider); (line 218) and the initialize() after logger.debug("Token and Key obtained from cloud, saving, back to initialize"); (line 372). The token and key are filled in on the UI, the connection manager is started, but chokes on the authorization. Resaving the thing with a change corrects the problem (I just added a location), but that is messy. With the current code everything works seamlessly when deleting the token and key when the cloud provider, password and email are in place.

@apella12
Copy link
Contributor Author

Figured out how to leverage the OH core util hex/byte and separated the socket timeout exception from anything else. Seems to still work, but with the magnitude of changes am testing overnight. Have tested scan, retrieve key/token, normal polling and temperature changes. Had one timeout exception that worked as expected. 9 seconds = 4 for timeout and 5 delay for next try.

2025-02-21 17:19:56.320 [DEBUG] [nternal.connection.ConnectionManager] - Disconnecting from device at 192.168.0.246
2025-02-21 17:20:05.326 [DEBUG] [nternal.connection.ConnectionManager] - Socket retry count 1, Socket timeout connecting to 192.168.0.246: Connect timed out
2025-02-21 17:20:05.329 [DEBUG] [nternal.connection.ConnectionManager] - Device at IP: 192.168.0.246 requires authentication, going to authenticate

Normal connection is less that 10ms.

2025-02-21 17:09:53.751 [DEBUG] [nternal.connection.ConnectionManager] - Disconnecting from device at 192.168.0.246
2025-02-21 17:09:53.757 [DEBUG] [nternal.connection.ConnectionManager] - Device at IP: 192.168.0.246 requires authentication, going to authenticate

@apella12 apella12 force-pushed the separate-connection-manager branch 2 times, most recently from 99ac827 to 2bb3b9c Compare February 24, 2025 23:32
Mideaac binding after partial PR review.  Main remaining issue is the connection manager which currently needs to be embedded in the MideaACHandler to leverage the OH base thing handler.
Signed-off-by: Bob Eckhoff <[email protected]>
Working (sort of) version of split connection manager. Problem with the connection manager being null when the binding is stop/start.  Need reset to clear with clean-cache too.
Signed-off-by: Bob Eckhoff <[email protected]>
After initial changes, tested various scenarios and made changes so they are working like before.

Signed-off-by: Bob Eckhoff <[email protected]>
forgot to run spotless on the last update

Signed-off-by: Bob Eckhoff <[email protected]>
Java doc and possible mdns discovery.

Signed-off-by: Bob Eckhoff <[email protected]>
spotless
Signed-off-by: Bob Eckhoff <[email protected]>
Makes retries more robust. Three times for connection and one retry on the command.
Signed-off-by: Bob Eckhoff <[email protected]>
Changed V2 response to align with V3 process after extra decoding.
Signed-off-by: Bob Eckhoff <[email protected]>
Changed version and changed dates (in advance-hopefully ok)
Signed-off-by: Bob Eckhoff <[email protected]>
Changing dates early was a bad idea

Signed-off-by: Bob Eckhoff <[email protected]>
This reverts commit 44bb90f.

Signed-off-by: Bob Eckhoff <[email protected]>
Change to new Headers
Signed-off-by: Bob Eckhoff <[email protected]>
Addressed some PR comments. Some questions outstanding.

Signed-off-by: Bob Eckhoff <[email protected]>
Additional edits.
Signed-off-by: Bob Eckhoff <[email protected]>
Updated utilities to leverage OH core and separated retry timeout from other exceptions.

Signed-off-by: Bob Eckhoff <[email protected]>
Eliminated DTO folder and classes by addressing null issues in the cloud communications. Plus slight timing issue on retry.

Signed-off-by: Bob Eckhoff <[email protected]>
New standard format?

Signed-off-by: Bob Eckhoff <[email protected]>
Change connection log to either/or avoiding both on device being connected.

Signed-off-by: Bob Eckhoff <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new binding If someone has started to work on a binding. For a new binding PR.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants