Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
676d1e5
Midea AC after partial PR review
apella12 Oct 29, 2024
2abb211
Working version of split connection manager
apella12 Nov 6, 2024
51c3cd5
Changes to get separate Connection Manager working
apella12 Nov 10, 2024
1c4ae3e
Apply spotless changes
apella12 Nov 10, 2024
20a6c27
New PR candidate
apella12 Nov 15, 2024
ca8619f
Spotless changes
apella12 Nov 15, 2024
31f23e2
Make retries more robust
apella12 Nov 23, 2024
44f2a8e
Align V2 response with V3 reponse
apella12 Nov 25, 2024
d03fe24
Change to OH5.0 snapshot
apella12 Dec 16, 2024
72bead9
Changing dates was a bad idea
apella12 Dec 16, 2024
0ada337
Revert "Changing dates was a bad idea"
apella12 Jan 3, 2025
dcb1511
Change to new Headers
apella12 Jan 8, 2025
c6750ad
Address Comments partial
apella12 Feb 19, 2025
46a1ca5
Additional edits
apella12 Feb 20, 2025
757a257
Update utilities and connection manager
apella12 Feb 21, 2025
00ce795
Eliminate DTO folder and classes
apella12 Feb 24, 2025
6354dd3
Update pom with spotless
apella12 Feb 24, 2025
fc50345
Change connection log
apella12 Feb 24, 2025
3f2aaf1
Minor tweaks from extended testing
apella12 Mar 21, 2025
23eee74
Add scheduled token and key updates
apella12 Mar 23, 2025
88ff93e
Validated the automatic token key update steps
apella12 Mar 31, 2025
4444cf3
Cleanup unused code in Cloud.java
apella12 Apr 3, 2025
e17c852
Add the capability command
apella12 Apr 8, 2025
0024a0b
Reorganized files
apella12 Apr 9, 2025
69ab6e9
Add default cloud
apella12 Apr 21, 2025
a02f6ce
General Cleanup and documentation
apella12 Apr 28, 2025
4303e3b
Document clarifications
apella12 Apr 29, 2025
e87fe49
spotless missed
apella12 Apr 29, 2025
12ca6da
Add capability follow-up and energy Poll
apella12 May 7, 2025
5e16f7d
Add energy Polling Refresh
apella12 May 8, 2025
e96426b
Improve config descriptions
apella12 May 9, 2025
d1a4eb8
Correct LED command and change energy polling
apella12 May 20, 2025
64b451e
Add target humidity support for Midea AC binding
apella12 Jul 9, 2025
885150a
Add support for maximum humidity and filter status channels
apella12 Jul 14, 2025
48fb70e
Refactor and update Midea AC binding documentation and code
apella12 Jul 15, 2025
dfe2fc0
Minor clean-ups
apella12 Jul 16, 2025
7108254
Update palm to 5.1
apella12 Jul 23, 2025
1e7383a
Add tags to channels
apella12 Jul 24, 2025
7b778af
Add CODEOWNER for mideaac binding
apella12 Aug 24, 2025
2af62c0
Address some of the October review comments
apella12 Nov 1, 2025
d6be125
Review update 2. i18n still WIP
apella12 Nov 2, 2025
592d9b4
Improve configuration validation, error handling andi18n messages
apella12 Nov 4, 2025
4b6d554
Improve AC thing initialization documentation and i18n keys
apella12 Nov 5, 2025
fe1dafc
Refactor initialize
lsiepel Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions bundles/org.openhab.binding.mideaac/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ Note: After discovery, the thing properties dropdown on the Thing UI page will
| screen-display | Switch | If device supports across LAN, turns off the LED display. | | Yes |
| maximum-humidity | Number | If device supports, allows setting the maximum humidity in DRY mode | | Yes
| humidity | Number | If device supports, the indoor room humidity. | Yes | Yes |
| kilowatt-hours | Number | If device supports, cumulative KWH usage | Yes | Yes |
| amperes | Number | If device supports, current amperage usage | Yes | Yes |
| watts | Number | If device supports, current wattage reading | Yes | Yes |
| energy-consumption | Number | If device supports, cumulative Kilowatt-Hours usage | Yes | Yes |
| current-draw | Number | If device supports, instantaneous amperage usage | Yes | Yes |
| power-consumption | Number | If device supports, instantaneous wattage reading | Yes | Yes |
| appliance-error | Switch | If device supports, appliance error notification | Yes | Yes |
| filter-status | Switch | If device supports, notification that filter needs cleaning | Yes | Yes |
| auxiliary-heat | Switch | If device supports, auxiliary heat (On or Off) | Yes | Yes |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ public class MideaACBindingConstants {
public static final String CHANNEL_HUMIDITY = "humidity";
public static final String CHANNEL_SCREEN_DISPLAY = "screen-display";
public static final String CHANNEL_FILTER_STATUS = "filter-status";
public static final String CHANNEL_KILOWATT_HOURS = "kilowatt-hours";
public static final String CHANNEL_AMPERES = "amperes";
public static final String CHANNEL_WATTS = "watts";
public static final String CHANNEL_ENERGY_CONSUMPTION = "energy-consumption";
public static final String CHANNEL_CURRENT_DRAW = "current-draw";
public static final String CHANNEL_POWER_CONSUMPTION = "power-consumption";

public static final Unit<Temperature> API_TEMPERATURE_UNIT = SIUnits.CELSIUS;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public boolean isValid() {
* @return true(discovery needed), false (not needed)
*/
public boolean isDiscoveryPossible() {
return (Utils.validateIP(ipAddress));
return Utils.validateIP(ipAddress);
}

/**
Expand All @@ -128,7 +128,7 @@ public boolean isDiscoveryPossible() {
* @return true (yes they can), false (they cannot)
*/
public boolean isTokenKeyObtainable() {
return (!email.isBlank() && !password.isBlank() && !cloud.isBlank());
return !email.isBlank() && !password.isBlank() && !cloud.isBlank();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,6 @@ public class Utils {

static byte[] empty = new byte[0];

/**
* Converts byte array to upper case hex string
*
* @param bytes bytes to convert
* @return string of upper case hex chars
*/
public static String bytesToHex(byte[] bytes) {
return HexUtils.bytesToHex(bytes);
}

/**
* Converts byte array to binary string
*
Expand All @@ -68,16 +58,6 @@ public static String bytesToBinary(byte[] bytes) {
return s1;
}

/**
* Converts byte array to lower case hex string
*
* @param bytes bytes to convert
* @return string of lower case hex chars
*/
public static String bytesToHexLowercase(byte[] bytes) {
return HexUtils.bytesToHex(bytes).toLowerCase();
}

/**
* Validates the IP address format
*
Expand Down Expand Up @@ -143,22 +123,6 @@ public static byte[] strxor(byte[] array1, byte[] array2) {
return result;
}

/**
* Create String of the V.3 Token
* String length is the nbytes characters long
*
* @param nbytes number of bytes
* @return String
*/
public static String tokenHex(int nbytes) {
Random r = new Random();
StringBuffer sb = new StringBuffer();
for (int n = 0; n < nbytes; n++) {
sb.append(Integer.toHexString(r.nextInt()));
}
return sb.toString().substring(0, nbytes);
}

/**
* Create URL safe token
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.openhab.binding.mideaac.internal.Utils;
import org.openhab.binding.mideaac.internal.security.Security;
import org.openhab.binding.mideaac.internal.security.TokenKey;
import org.openhab.core.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -116,7 +117,7 @@ public Cloud(String email, String password, CloudProvider cloudProvider, HttpCli
// This adds the first 16 characters of a 16 byte string
// if Cloud provider uses proxied and wasn't added by the method()
if (!data.has("reqId") && !cloudProvider.proxied().isBlank()) {
data.addProperty("reqId", Utils.tokenHex(16));
data.addProperty("reqId", StringUtils.getRandomHex(16));
}

String url = cloudProvider.apiurl() + endpoint;
Expand Down Expand Up @@ -176,11 +177,13 @@ public Cloud(String email, String password, CloudProvider cloudProvider, HttpCli
try {
cr = request.send();
} catch (InterruptedException e) {
logger.warn("an interupted error has occurred{}", e.getMessage());
Thread.currentThread().interrupt(); // Restore interrupt flag
logger.warn("Request interrupted: {}", e.getMessage());
return null; // Return quickly
} catch (TimeoutException e) {
logger.warn("a timeout error has occurred{}", e.getMessage());
logger.warn("Request timed out: {}", e.getMessage());
} catch (ExecutionException e) {
logger.warn("an execution error has occurred{}", e.getMessage());
logger.warn("Request execution failed: {}", e.getMessage());
}

if (cr != null) {
Expand All @@ -203,7 +206,7 @@ public Cloud(String email, String password, CloudProvider cloudProvider, HttpCli
if (code != 0) {
errMsg = msg;
logger.warn("Error {} logging to Cloud: {}", code, msg);
return null;
throw new LoginFailedException("Login failed with error code " + code + ": " + msg);
} else {
logger.debug("Api response ok: {} ({})", code, msg);
if (!cloudProvider.proxied().isBlank()) {
Expand Down Expand Up @@ -257,7 +260,7 @@ public boolean login() {
iotData.addProperty("loginAccount", loginAccount);
iotData.addProperty("password", security.encryptPassword(loginId, password));
iotData.addProperty("pushToken", Utils.tokenUrlsafe(120));
iotData.addProperty("reqId", Utils.tokenHex(16));
iotData.addProperty("reqId", StringUtils.getRandomHex(16));
iotData.addProperty("src", cloudProvider.appid());
iotData.addProperty("stamp", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()));
newData.add("iotData", iotData);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/

package org.openhab.binding.mideaac.internal.cloud;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* The {@link LoginFailedException} is used to indicate login failures to Midea
* cloud services.
*
* @author Bob Eckhoff - Initial contribution
*/

@NonNullByDefault
public class LoginFailedException extends RuntimeException {
private static final long serialVersionUID = 1L;

public LoginFailedException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.openhab.binding.mideaac.internal.security.Decryption8370Result;
import org.openhab.binding.mideaac.internal.security.Security;
import org.openhab.binding.mideaac.internal.security.Security.MsgType;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -168,6 +169,8 @@ public synchronized void connect()
Thread.sleep(5000);
} catch (InterruptedException ex) {
logger.debug("An interupted error (socket retry) has occured {}", ex.getMessage());
Thread.currentThread().interrupt();
throw new MideaConnectionException("Socket connection interrupted");
}
logger.debug("Socket retry count {}, Socket timeout connecting to {}: {}", retrySocket, ipAddress,
e.getMessage());
Expand Down Expand Up @@ -242,7 +245,7 @@ public void authenticate() throws MideaConnectionException, MideaAuthenticationE
private void doV3Handshake() throws MideaConnectionException, MideaAuthenticationException {
byte[] request = security.encode8370(Utils.hexStringToByteArray(token), MsgType.MSGTYPE_HANDSHAKE_REQUEST);
try {
logger.trace("Device at IP: {} writing handshake_request: {}", ipAddress, Utils.bytesToHex(request));
logger.trace("Device at IP: {} writing handshake_request: {}", ipAddress, HexUtils.bytesToHex(request));

write(request);
byte[] response = read();
Expand Down Expand Up @@ -315,8 +318,8 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal
throws MideaConnectionException, MideaAuthenticationException, MideaException, IOException {
ensureConnected();

if (command instanceof CommandSet) {
((CommandSet) command).setPromptTone(promptTone);
if (command instanceof CommandSet cmdSet) {
cmdSet.setPromptTone(promptTone);
}
Packet packet = new Packet(command, deviceId, security);
packet.compose();
Expand All @@ -340,7 +343,7 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal
} catch (InterruptedException e) {
logger.debug("An interupted error (write command2) has occured {}", e.getMessage());
Thread.currentThread().interrupt();
// Note, but continue anyway for second write if needed.
throw new MideaConnectionException("Command interrupted during wait");
}

// Input stream is checked after 1.5 seconds
Expand All @@ -363,7 +366,7 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal
if (response.length > 40 + 16) {
data = security.aesDecrypt(Arrays.copyOfRange(response, 40, response.length - 16));
logger.trace("Bytes in HEX, decoded and with header: length: {}, data: {}", data.length,
Utils.bytesToHex(data));
HexUtils.bytesToHex(data));
}
// The response data from the appliance includes a packet header which we don't want
if (data != null && data.length > 10) {
Expand All @@ -383,7 +386,7 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal
if (responseBytes.length > 40 + 16) {
data = security.aesDecrypt(Arrays.copyOfRange(responseBytes, 40, responseBytes.length - 16));
logger.trace("V2 Bytes decoded with header: length: {}, data: {}", data.length,
Utils.bytesToHex(data));
HexUtils.bytesToHex(data));
}
// The response data from the appliance includes a packet header which we don't want
if (data != null && data.length > 10) {
Expand Down Expand Up @@ -424,7 +427,7 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal
private void handleResponse(byte[] data, byte bodyType, @Nullable Callback callback) throws MideaException {
logger.trace("Response bodyType: {}", bodyType);
logger.debug("Bytes in HEX, decoded and stripped without header: length: {}, data: {}", data.length,
Utils.bytesToHex(data));
HexUtils.bytesToHex(data));
logger.trace("Bytes in BINARY, decoded and stripped without header: length: {}, data: {}", data.length,
Utils.bytesToBinary(data));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.util.HexUtils;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -258,28 +259,28 @@ public DiscoveryResult discoveryPacketReceived(DatagramPacket packet) {
final String ipAddress = packet.getAddress().getHostAddress();
byte[] data = Arrays.copyOfRange(packet.getData(), 0, packet.getLength());

logger.trace("Midea AC discover data ({}) from {}: '{}'", data.length, ipAddress, Utils.bytesToHex(data));
logger.trace("Midea AC discover data ({}) from {}: '{}'", data.length, ipAddress, HexUtils.bytesToHex(data));

if (data.length >= 104 && (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("5A5A")
|| Utils.bytesToHex(Arrays.copyOfRange(data, 8, 10)).equals("5A5A"))) {
if (data.length >= 104 && (HexUtils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("5A5A")
|| HexUtils.bytesToHex(Arrays.copyOfRange(data, 8, 10)).equals("5A5A"))) {
logger.trace("Device supported");
String mSmartId, mSmartip = "", mSmartSN = "", mSmartSSID = "", mSmartType = "", mSmartPort = "",
mSmartVersion = "";

if (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("5A5A")) {
if (HexUtils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("5A5A")) {
mSmartVersion = "2";
}
if (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("8370")) {
if (HexUtils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("8370")) {
mSmartVersion = "3";
}
if (Utils.bytesToHex(Arrays.copyOfRange(data, 8, 10)).equals("5A5A")) {
if (HexUtils.bytesToHex(Arrays.copyOfRange(data, 8, 10)).equals("5A5A")) {
data = Arrays.copyOfRange(data, 8, data.length - 16);
}

logger.debug("Version: {}", mSmartVersion);

byte[] id = Arrays.copyOfRange(data, 20, 26);
logger.trace("Id Bytes: {}", Utils.bytesToHex(id));
logger.trace("Id Bytes: {}", HexUtils.bytesToHex(id));

byte[] idReverse = Utils.reverse(id);

Expand All @@ -289,10 +290,10 @@ public DiscoveryResult discoveryPacketReceived(DatagramPacket packet) {
logger.debug("Id: '{}'", mSmartId);

byte[] encryptData = Arrays.copyOfRange(data, 40, data.length - 16);
logger.trace("Encrypt data: '{}'", Utils.bytesToHex(encryptData));
logger.trace("Encrypt data: '{}'", HexUtils.bytesToHex(encryptData));

byte[] reply = security.aesDecrypt(encryptData);
logger.trace("Length: {}, Reply: '{}'", reply.length, Utils.bytesToHex(reply));
logger.trace("Length: {}, Reply: '{}'", reply.length, HexUtils.bytesToHex(reply));

mSmartip = Byte.toUnsignedInt(reply[3]) + "." + Byte.toUnsignedInt(reply[2]) + "."
+ Byte.toUnsignedInt(reply[1]) + "." + Byte.toUnsignedInt(reply[0]);
Expand Down Expand Up @@ -323,7 +324,7 @@ public DiscoveryResult discoveryPacketReceived(DatagramPacket packet) {
mSmartSSID, mSmartType, new TreeMap<>(), // Placeholder for capabilities
new TreeMap<>())) // Placeholder for numericCapabilities
.build();
} else if (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 6)).equals("3C3F786D6C20")) {
} else if (HexUtils.bytesToHex(Arrays.copyOfRange(data, 0, 6)).equals("3C3F786D6C20")) {
logger.debug("Midea AC v1 device was detected, supported, but not implemented yet.");
return null;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
import java.util.Arrays;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mideaac.internal.Utils;
import org.openhab.binding.mideaac.internal.security.Crc8;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -281,7 +281,7 @@ public CommandBase() {
* Pulls the elements of the Base command together
*/
public void compose() {
logger.trace("Base Bytes before crypt {}", Utils.bytesToHex(data));
logger.trace("Base Bytes before crypt {}", HexUtils.bytesToHex(data));
byte crc8 = (byte) Crc8.calculate(Arrays.copyOfRange(data, 10, data.length));
byte[] newData1 = new byte[data.length + 1];
System.arraycopy(data, 0, newData1, 0, data.length);
Expand All @@ -308,7 +308,6 @@ private static byte checksum(byte[] bytes) {
for (byte value : bytes) {
sum = (byte) (sum + value);
}
sum = (byte) ((255 - (sum % 256)) + 1);
return (byte) sum;
return (byte) ((255 - (sum % 256)) + 1);
}
}
Loading