Changes to get separate Connection Manager working

After initial changes, tested various scenarios and made changes so they are working like before.

Signed-off-by: Bob Eckhoff <katmandodo@yahoo.com>
This commit is contained in:
Bob Eckhoff 2024-11-10 11:15:10 -05:00
parent a2159bed2a
commit 99ed3d5000
12 changed files with 188 additions and 144 deletions

View File

@ -41,7 +41,7 @@ No binding configuration is required.
| pollingTime | Yes | Polling time in seconds. Minimum time is 30 seconds. | 60 | | pollingTime | Yes | Polling time in seconds. Minimum time is 30 seconds. | 60 |
| timeout | Yes | Connecting timeout. Minimum time is 2 second, maximum 10 seconds. | 4 | | timeout | Yes | Connecting timeout. Minimum time is 2 second, maximum 10 seconds. | 4 |
| promptTone | Yes | "Ding" tone when command is received and executed. | False | | promptTone | Yes | "Ding" tone when command is received and executed. | False |
| version | Yes | Version 3 has token, key and cloud requirements. | 3 | | version | Yes | Version 3 has token, key and cloud requirements. | 0 |
## Channels ## Channels

View File

@ -85,8 +85,8 @@ public class MideaACBindingConstants {
public static final String CONFIG_POLLING_TIME = "pollingTime"; public static final String CONFIG_POLLING_TIME = "pollingTime";
public static final String CONFIG_CONNECTING_TIMEOUT = "timeout"; public static final String CONFIG_CONNECTING_TIMEOUT = "timeout";
public static final String CONFIG_PROMPT_TONE = "promptTone"; public static final String CONFIG_PROMPT_TONE = "promptTone";
public static final String CONFIG_VERSION = "version";
public static final String PROPERTY_VERSION = "version";
public static final String PROPERTY_SN = "sn"; public static final String PROPERTY_SN = "sn";
public static final String PROPERTY_SSID = "ssid"; public static final String PROPERTY_SSID = "ssid";
public static final String PROPERTY_TYPE = "type"; public static final String PROPERTY_TYPE = "type";

View File

@ -27,7 +27,7 @@ public class MideaACConfiguration {
public int ipPort = 6444; public int ipPort = 6444;
public String deviceId = ""; public String deviceId = "0";
public String email = ""; public String email = "";
@ -43,7 +43,7 @@ public class MideaACConfiguration {
public int timeout = 4; public int timeout = 4;
public boolean promptTone; public boolean promptTone = false;
public int version = 0; public int version = 0;
@ -53,7 +53,8 @@ public class MideaACConfiguration {
* @return true(valid), false (not valid) * @return true(valid), false (not valid)
*/ */
public boolean isValid() { public boolean isValid() {
return !("0".equalsIgnoreCase(deviceId) || deviceId.isBlank() || ipPort <= 0 || ipAddress.isBlank()); return !("0".equalsIgnoreCase(deviceId) || deviceId.isBlank() || ipPort <= 0 || ipAddress.isBlank()
|| version <= 1);
} }
/** /**
@ -63,7 +64,7 @@ public class MideaACConfiguration {
*/ */
public boolean isDiscoveryNeeded() { public boolean isDiscoveryNeeded() {
return ("0".equalsIgnoreCase(deviceId) || deviceId.isBlank() || ipPort <= 0 || ipAddress.isBlank() return ("0".equalsIgnoreCase(deviceId) || deviceId.isBlank() || ipPort <= 0 || ipAddress.isBlank()
|| !Utils.validateIP(ipAddress)); || !Utils.validateIP(ipAddress) || version <= 1);
} }
/** /**

View File

@ -135,7 +135,7 @@ public class CommandHelper {
if (command instanceof DecimalType decimalCommand) { if (command instanceof DecimalType decimalCommand) {
logger.debug("Handle Target Temperature as DecimalType in degrees C"); logger.debug("Handle Target Temperature as DecimalType in degrees C");
commandSet.setTargetTemperature(limitTargetTemperatureToRange(decimalCommand.floatValue())); commandSet.setTargetTemperature(limitTargetTemperatureToRange(decimalCommand.floatValue()));
} else if (command instanceof QuantityType quantityCommand) { } else if (command instanceof QuantityType<?> quantityCommand) {
if (quantityCommand.getUnit().equals(ImperialUnits.FAHRENHEIT)) { if (quantityCommand.getUnit().equals(ImperialUnits.FAHRENHEIT)) {
quantityCommand = Objects.requireNonNull(quantityCommand.toUnit(SIUnits.CELSIUS)); quantityCommand = Objects.requireNonNull(quantityCommand.toUnit(SIUnits.CELSIUS));
} }

View File

@ -66,17 +66,14 @@ public class ConnectionManager {
private Security security; private Security security;
private final int version; private final int version;
private final boolean promptTone; private final boolean promptTone;
private boolean deviceIsConnected;
private int droppedCommands = 0;
/** /**
* True allows one short retry after connection problem * True allows one short retry after connection problem
*/ */
private boolean retry = true; private boolean retry = true;
/**
* Suppresses the connection message if was online before
*/
private boolean connectionMessage = true;
public ConnectionManager(String ipAddress, int ipPort, int timeout, String key, String token, String cloud, public ConnectionManager(String ipAddress, int ipPort, int timeout, String key, String token, String cloud,
String email, String password, String deviceId, int version, boolean promptTone) { String email, String password, String deviceId, int version, boolean promptTone) {
this.deviceIsConnected = false; this.deviceIsConnected = false;
@ -95,9 +92,6 @@ public class ConnectionManager {
this.security = new Security(cloudProvider); this.security = new Security(cloudProvider);
} }
private boolean deviceIsConnected;
private int droppedCommands = 0;
private Socket socket = new Socket(); private Socket socket = new Socket();
private InputStream inputStream = new ByteArrayInputStream(new byte[0]); private InputStream inputStream = new ByteArrayInputStream(new byte[0]);
private DataOutputStream writer = new DataOutputStream(System.out); private DataOutputStream writer = new DataOutputStream(System.out);
@ -121,25 +115,6 @@ public class ConnectionManager {
return str.trim().isEmpty(); return str.trim().isEmpty();
} }
/**
* Reset dropped commands from initialization in MideaACHandler
* Channel created for easy observation
* Dropped commands when no bytes to read after two tries or other
* byte reading problem. Device not responding.
*/
public void resetDroppedCommands() {
droppedCommands = 0;
}
/**
* Resets Dropped command
*
* @return dropped commands
*/
public int getDroppedCommands() {
return droppedCommands = 0;
}
/** /**
* After checking if the key and token need to be updated (Default = 0 Never) * After checking if the key and token need to be updated (Default = 0 Never)
* The socket is established with the writer and inputStream (for reading responses) * The socket is established with the writer and inputStream (for reading responses)
@ -155,17 +130,28 @@ public class ConnectionManager {
socket.setSoTimeout(timeout * 1000); socket.setSoTimeout(timeout * 1000);
socket.connect(new InetSocketAddress(ipAddress, ipPort), timeout * 1000); socket.connect(new InetSocketAddress(ipAddress, ipPort), timeout * 1000);
} catch (IOException e) { } catch (IOException e) {
logger.debug("IOException connecting to {}: {}", ipAddress, e.getMessage()); // Retry addresses most common wifi connection problems- wait 5 seconds and try again
deviceIsConnected = false;
if (retry) { if (retry) {
logger.debug("Retrying Socket, IOException connecting to {}: {}", ipAddress, e.getMessage());
try { try {
Thread.sleep(5000); Thread.sleep(5000);
} catch (InterruptedException ex) { } catch (InterruptedException ex) {
logger.debug("An interupted error (pause) has occured {}", ex.getMessage()); logger.debug("An interupted error (socket retry) has occured {}", ex.getMessage());
} }
connect(); retry = false;
try {
socket = new Socket();
socket.setSoTimeout(timeout * 1000);
socket.connect(new InetSocketAddress(ipAddress, ipPort), timeout * 1000);
} catch (IOException e2) {
deviceIsConnected = false;
logger.debug("Second try IOException connecting to {}: {}", ipAddress, e2.getMessage());
throw new MideaConnectionException(e2);
}
} else {
deviceIsConnected = false;
throw new MideaConnectionException(e);
} }
throw new MideaConnectionException(e);
} }
// Create streams // Create streams
@ -177,16 +163,9 @@ public class ConnectionManager {
deviceIsConnected = false; deviceIsConnected = false;
throw new MideaConnectionException(e); throw new MideaConnectionException(e);
} }
if (!deviceIsConnected || !connectionMessage) {
logger.info("Connected to IP {}", ipAddress);
resetConnectionMessage();
}
logger.debug("Connected to IP {}", ipAddress);
deviceIsConnected = true;
resetRetry();
if (version == 3) { if (version == 3) {
logger.debug("Device {} require authentication, going to authenticate", ipAddress); logger.debug("Device at IP: {} requires authentication, going to authenticate", ipAddress);
try { try {
authenticate(); authenticate();
} catch (MideaAuthenticationException | MideaConnectionException e) { } catch (MideaAuthenticationException | MideaConnectionException e) {
@ -194,8 +173,13 @@ public class ConnectionManager {
throw e; throw e;
} }
} }
// requestStatus(getDoPoll());
if (!deviceIsConnected) {
logger.info("Connected to IP {}", ipAddress);
}
logger.debug("Connected to IP {}", ipAddress);
deviceIsConnected = true; deviceIsConnected = true;
retry = true;
} }
/** /**
@ -212,7 +196,7 @@ public class ConnectionManager {
logger.trace("Cloud {}", cloud); logger.trace("Cloud {}", cloud);
if (!isBlank(token) && !isBlank(key) && !"".equals(cloud)) { if (!isBlank(token) && !isBlank(key) && !"".equals(cloud)) {
logger.debug("Device {} authenticating", ipAddress); logger.debug("Device at IP: {} authenticating", ipAddress);
doV3Handshake(); doV3Handshake();
} else { } else {
throw new MideaAuthenticationException("Token, Key and / or cloud provider missing"); throw new MideaAuthenticationException("Token, Key and / or cloud provider missing");
@ -228,13 +212,13 @@ public class ConnectionManager {
private void doV3Handshake() throws MideaConnectionException, MideaAuthenticationException { private void doV3Handshake() throws MideaConnectionException, MideaAuthenticationException {
byte[] request = security.encode8370(Utils.hexStringToByteArray(token), MsgType.MSGTYPE_HANDSHAKE_REQUEST); byte[] request = security.encode8370(Utils.hexStringToByteArray(token), MsgType.MSGTYPE_HANDSHAKE_REQUEST);
try { try {
logger.trace("Device {} writing handshake_request: {}", ipAddress, Utils.bytesToHex(request)); logger.trace("Device at IP: {} writing handshake_request: {}", ipAddress, Utils.bytesToHex(request));
write(request); write(request);
byte[] response = read(); byte[] response = read();
if (response != null && response.length > 0) { if (response != null && response.length > 0) {
logger.trace("Device {} response for handshake_request length: {}", ipAddress, response.length); logger.trace("Device at IP: {} response for handshake_request length:{}", ipAddress, response.length);
if (response.length == 72) { if (response.length == 72) {
boolean success = security.tcpKey(Arrays.copyOfRange(response, 8, 72), boolean success = security.tcpKey(Arrays.copyOfRange(response, 8, 72),
Utils.hexStringToByteArray(key)); Utils.hexStringToByteArray(key));
@ -247,7 +231,6 @@ public class ConnectionManager {
} catch (InterruptedException e) { } catch (InterruptedException e) {
logger.debug("An interupted error (success) has occured {}", e.getMessage()); logger.debug("An interupted error (success) has occured {}", e.getMessage());
} }
// requestStatus(getDoPoll()); need to handle
} else { } else {
throw new MideaAuthenticationException("Invalid Key. Correct Key in configuration."); throw new MideaAuthenticationException("Invalid Key. Correct Key in configuration.");
} }
@ -255,7 +238,7 @@ public class ConnectionManager {
throw new MideaAuthenticationException("Authentication failed!"); throw new MideaAuthenticationException("Authentication failed!");
} else { } else {
logger.warn("Authentication reponse unexpected data length ({} instead of 72)!", response.length); logger.warn("Authentication reponse unexpected data length ({} instead of 72)!", response.length);
throw new MideaAuthenticationException("Invalid Key. Correct Key in configuration."); throw new MideaAuthenticationException("Unexpected authentication response length");
} }
} }
} catch (IOException e) { } catch (IOException e) {
@ -290,7 +273,7 @@ public class ConnectionManager {
* Normal device response in 0.75 - 1 second range * Normal device response in 0.75 - 1 second range
* If still empty, send the bytes again. If there are bytes, the read method is called. * If still empty, send the bytes again. If there are bytes, the read method is called.
* If the socket times out with no response the command is dropped. There will be another poll * If the socket times out with no response the command is dropped. There will be another poll
* in the time set by the user (30 seconds min) or the set command can be retried * in the time set by the user (30 seconds min). A Set command will need to be resent.
* *
* @param command either the set or polling command * @param command either the set or polling command
* @throws MideaAuthenticationException * @throws MideaAuthenticationException
@ -325,7 +308,7 @@ public class ConnectionManager {
} catch (InterruptedException e) { } catch (InterruptedException e) {
logger.debug("An interupted error (retrycommand2) has occured {}", e.getMessage()); logger.debug("An interupted error (retrycommand2) has occured {}", e.getMessage());
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
// Note, but continue anyway. Command will be dropped // Note, but continue anyway for second write.
} }
if (inputStream.available() == 0) { if (inputStream.available() == 0) {
@ -340,7 +323,7 @@ public class ConnectionManager {
if (version == 3) { if (version == 3) {
Decryption8370Result result = security.decode8370(responseBytes); Decryption8370Result result = security.decode8370(responseBytes);
for (byte[] response : result.getResponses()) { for (byte[] response : result.getResponses()) {
logger.debug("Response length:{} IP address:{} ", response.length, ipAddress); logger.debug("Response length: {} IP address: {} ", response.length, ipAddress);
if (response.length > 40 + 16) { if (response.length > 40 + 16) {
byte[] data = security.aesDecrypt(Arrays.copyOfRange(response, 40, response.length - 16)); byte[] data = security.aesDecrypt(Arrays.copyOfRange(response, 40, response.length - 16));
@ -383,12 +366,12 @@ public class ConnectionManager {
default: default:
logger.debug("Invalid response type: {}", data[0x9]); logger.debug("Invalid response type: {}", data[0x9]);
} }
logger.trace("Response Type: {} and bodyType:{}", responseType, bodyType2); logger.trace("Response Type: {} and bodyType: {}", responseType, bodyType2);
// The response data from the appliance includes a packet header which we don't want // The response data from the appliance includes a packet header which we don't want
data = Arrays.copyOfRange(data, 10, data.length); data = Arrays.copyOfRange(data, 10, data.length);
byte bodyType = data[0x0]; byte bodyType = data[0x0];
logger.trace("Response Type expected: {} and bodyType:{}", responseType, bodyType); logger.trace("Response Type expected: {} and bodyType: {}", responseType, bodyType);
logger.trace("Bytes in HEX, decoded and stripped without header: length: {}, data: {}", logger.trace("Bytes in HEX, decoded and stripped without header: length: {}, data: {}",
data.length, Utils.bytesToHex(data)); data.length, Utils.bytesToHex(data));
logger.debug("Bytes in BINARY, decoded and stripped without header: length: {}, data: {}", logger.debug("Bytes in BINARY, decoded and stripped without header: length: {}, data: {}",
@ -401,7 +384,7 @@ public class ConnectionManager {
} }
if (bodyType != -64) { if (bodyType != -64) {
if (bodyType == 30) { if (bodyType == 30) {
logger.warn("Error response 0x1E received {} from IP Address {}", bodyType, logger.warn("Error response 0x1E received {} from IP Address:{}", bodyType,
ipAddress); ipAddress);
return; return;
} }
@ -410,7 +393,7 @@ public class ConnectionManager {
} }
lastResponse = new Response(data, version, responseType, bodyType); lastResponse = new Response(data, version, responseType, bodyType);
try { try {
logger.trace("data length is {} version is {} IP address is {}", data.length, logger.trace("Data length is {}, version is {}, IP address is {}", data.length,
version, ipAddress); version, ipAddress);
if (callback != null) { if (callback != null) {
callback.updateChannels(lastResponse); callback.updateChannels(lastResponse);
@ -432,42 +415,42 @@ public class ConnectionManager {
Utils.bytesToHex(data)); Utils.bytesToHex(data));
lastResponse = new Response(data, version, "", (byte) 0x00); lastResponse = new Response(data, version, "", (byte) 0x00);
logger.debug("V2 data length is {} version is {} Ip Address is {}", data.length, version, logger.debug("Data length is {}, version is {}, Ip Address is {}", data.length, version,
ipAddress); ipAddress);
if (callback != null) { if (callback != null) {
callback.updateChannels(lastResponse); callback.updateChannels(lastResponse);
} }
} else { } else {
droppedCommands = droppedCommands + 1; droppedCommands = droppedCommands + 1;
logger.debug("Problem with reading V2 response, skipping command {} dropped count{}", command, logger.debug("Problem with reading V2 response, skipping {} skipped count since startup {}", command,
droppedCommands); droppedCommands);
} }
} }
return; return;
} else { } else {
droppedCommands = droppedCommands + 1; droppedCommands = droppedCommands + 1;
logger.debug("Problem with reading response, skipping command {} dropped count{}", command, logger.debug("Problem with reading response, skipping {} skipped count since startup {}", command,
droppedCommands); droppedCommands);
return; return;
} }
} catch (SocketException e) { } catch (SocketException e) {
logger.debug("SocketException writing to {}: {}", ipAddress, e.getMessage());
droppedCommands = droppedCommands + 1; droppedCommands = droppedCommands + 1;
logger.debug("Socket exception, skipping command {} dropped count{}", command, droppedCommands); logger.debug("Socket exception on IP: {}, skipping command {} skipped count since startup {}", ipAddress, command,
droppedCommands);
throw new MideaConnectionException(e); throw new MideaConnectionException(e);
} catch (IOException e) { } catch (IOException e) {
logger.debug(" Send IOException writing to {}: {}", ipAddress, e.getMessage());
droppedCommands = droppedCommands + 1; droppedCommands = droppedCommands + 1;
logger.debug("Socket exception, skipping command {} dropped count{}", command, droppedCommands); logger.debug("IO exception on IP: {}, skipping command {} skipped count since startup {}", ipAddress, command,
droppedCommands);
throw new MideaConnectionException(e); throw new MideaConnectionException(e);
} }
} }
/** /**
* Closes all elements of the connection before starting a new one * Closes all elements of the connection before starting a new one
* Makes sure writer, inputStream and socket are closed before each command is started
*/ */
public synchronized void disconnect() { public synchronized void disconnect() {
// Make sure writer, inputStream and socket are closed before each command is started
logger.debug("Disconnecting from device at {}", ipAddress); logger.debug("Disconnecting from device at {}", ipAddress);
InputStream inputStream = this.inputStream; InputStream inputStream = this.inputStream;
@ -498,7 +481,7 @@ public class ConnectionManager {
try { try {
int len = inputStream.read(bytes); int len = inputStream.read(bytes);
if (len > 0) { if (len > 0) {
logger.debug("Response received length: {} Device IP {}", len, ipAddress); logger.debug("Response received length: {} from device at IP: {}", len, ipAddress);
bytes = Arrays.copyOfRange(bytes, 0, len); bytes = Arrays.copyOfRange(bytes, 0, len);
return bytes; return bytes;
} }
@ -527,25 +510,7 @@ public class ConnectionManager {
} }
/** /**
* Reset Retry controls the short 5 second delay * Disconnects from the AC device
* Before starting 30 second delays. (More severe Wifi issue)
* It is reset after a successful connection
*/
private void resetRetry() {
retry = true;
}
/**
* Limit logging of INFO connection messages to
* only when the device was Offline in its prior
* state
*/
private void resetConnectionMessage() {
connectionMessage = true;
}
/**
* Disconnects from the device
* *
* @param force * @param force
*/ */

View File

@ -70,7 +70,7 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService {
private Security security; private Security security;
/** /**
* Discovery Service * Discovery Service Uses the default decryption for all devices
*/ */
public MideaACDiscoveryService() { public MideaACDiscoveryService() {
super(SUPPORTED_THING_TYPES_UIDS, discoveryTimeoutSeconds, false); super(SUPPORTED_THING_TYPES_UIDS, discoveryTimeoutSeconds, false);
@ -152,7 +152,7 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService {
} }
} }
} catch (SocketTimeoutException e) { } catch (SocketTimeoutException e) {
logger.debug("Discovering poller timeout..."); logger.trace("Discovering poller timeout...");
} catch (IOException e) { } catch (IOException e) {
logger.debug("Error during discovery: {}", e.getMessage()); logger.debug("Error during discovery: {}", e.getMessage());
} finally { } finally {
@ -243,13 +243,14 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService {
final String ipAddress = packet.getAddress().getHostAddress(); final String ipAddress = packet.getAddress().getHostAddress();
byte[] data = Arrays.copyOfRange(packet.getData(), 0, packet.getLength()); byte[] data = Arrays.copyOfRange(packet.getData(), 0, packet.getLength());
logger.debug("Midea AC discover data ({}) from {}: '{}'", data.length, ipAddress, Utils.bytesToHex(data)); logger.trace("Midea AC discover data ({}) from {}: '{}'", data.length, ipAddress, Utils.bytesToHex(data));
if (data.length >= 104 && (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("5A5A") if (data.length >= 104 && (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("5A5A")
|| Utils.bytesToHex(Arrays.copyOfRange(data, 8, 10)).equals("5A5A"))) { || Utils.bytesToHex(Arrays.copyOfRange(data, 8, 10)).equals("5A5A"))) {
logger.trace("Device supported"); logger.trace("Device supported");
String mSmartId, mSmartVersion = "", mSmartip = "", mSmartPort = "", mSmartSN = "", mSmartSSID = "", String mSmartId, mSmartip = "", mSmartSN = "", mSmartSSID = "", mSmartType = "", mSmartPort = "",
mSmartType = ""; mSmartVersion = "";
if (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("5A5A")) { if (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("5A5A")) {
mSmartVersion = "2"; mSmartVersion = "2";
} }
@ -260,7 +261,7 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService {
data = Arrays.copyOfRange(data, 8, data.length - 16); data = Arrays.copyOfRange(data, 8, data.length - 16);
} }
logger.trace("Version: {}", mSmartVersion); logger.debug("Version: {}", mSmartVersion);
byte[] id = Arrays.copyOfRange(data, 20, 26); byte[] id = Arrays.copyOfRange(data, 20, 26);
logger.trace("Id Bytes: {}", Utils.bytesToHex(id)); logger.trace("Id Bytes: {}", Utils.bytesToHex(id));
@ -273,10 +274,10 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService {
logger.debug("Id: '{}'", mSmartId); logger.debug("Id: '{}'", mSmartId);
byte[] encryptData = Arrays.copyOfRange(data, 40, data.length - 16); byte[] encryptData = Arrays.copyOfRange(data, 40, data.length - 16);
logger.debug("Encrypt data: '{}'", Utils.bytesToHex(encryptData)); logger.trace("Encrypt data: '{}'", Utils.bytesToHex(encryptData));
byte[] reply = security.aesDecrypt(encryptData); byte[] reply = security.aesDecrypt(encryptData);
logger.debug("Length: {}, Reply: '{}'", reply.length, Utils.bytesToHex(reply)); logger.trace("Length: {}, Reply: '{}'", reply.length, Utils.bytesToHex(reply));
mSmartip = Byte.toUnsignedInt(reply[3]) + "." + Byte.toUnsignedInt(reply[2]) + "." mSmartip = Byte.toUnsignedInt(reply[3]) + "." + Byte.toUnsignedInt(reply[2]) + "."
+ Byte.toUnsignedInt(reply[1]) + "." + Byte.toUnsignedInt(reply[0]); + Byte.toUnsignedInt(reply[1]) + "." + Byte.toUnsignedInt(reply[0]);
@ -343,7 +344,7 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService {
properties.put(CONFIG_IP_ADDRESS, ipAddress); properties.put(CONFIG_IP_ADDRESS, ipAddress);
properties.put(CONFIG_IP_PORT, port); properties.put(CONFIG_IP_PORT, port);
properties.put(CONFIG_DEVICEID, id); properties.put(CONFIG_DEVICEID, id);
properties.put(PROPERTY_VERSION, version); properties.put(CONFIG_VERSION, version);
properties.put(PROPERTY_SN, sn); properties.put(PROPERTY_SN, sn);
properties.put(PROPERTY_SSID, ssid); properties.put(PROPERTY_SSID, ssid);
properties.put(PROPERTY_TYPE, type); properties.put(PROPERTY_TYPE, type);

View File

@ -14,6 +14,7 @@ package org.openhab.binding.mideaac.internal.handler;
import static org.openhab.binding.mideaac.internal.MideaACBindingConstants.*; import static org.openhab.binding.mideaac.internal.MideaACBindingConstants.*;
import java.math.BigDecimal;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@ -75,11 +76,13 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler
private final Logger logger = LoggerFactory.getLogger(MideaACHandler.class); private final Logger logger = LoggerFactory.getLogger(MideaACHandler.class);
private final CloudsDTO clouds; private final CloudsDTO clouds;
private final boolean imperialUnits; private final boolean imperialUnits;
private boolean isPollRunning = false;
private final HttpClient httpClient; private final HttpClient httpClient;
private MideaACConfiguration config = new MideaACConfiguration(); private MideaACConfiguration config = new MideaACConfiguration();
private Map<String, String> properties = new HashMap<>(); private Map<String, String> properties = new HashMap<>();
private @Nullable ConnectionManager connectionManager; // Default parameters are the same as in the MideaACConfiguration class
private ConnectionManager connectionManager = new ConnectionManager("", 6444, 4, "", "", "", "", "", "", 0, false);
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private @Nullable ScheduledFuture<?> scheduledTask = null; private @Nullable ScheduledFuture<?> scheduledTask = null;
@ -113,27 +116,22 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler
} }
/** /**
* This method handles the Channels that can be set (non-read only) * This method handles the AC Channels that can be set (non-read only)
* First the Routine polling is stopped so there is no conflict * The command set is formed using the previous command to only
* Then connects and authorizes (if necessary) and returns here to * change the item requested and leave the others the same.
* create the command set which is then sent to the device. * The command set which is then sent to the device via the connectionManager.
*/ */
@Override @Override
public void handleCommand(ChannelUID channelUID, Command command) { public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Handling channelUID {} with command {}", channelUID.getId(), command.toString()); logger.debug("Handling channelUID {} with command {}", channelUID.getId(), command.toString());
ConnectionManager connectionManager = this.connectionManager; ConnectionManager connectionManager = this.connectionManager;
if (connectionManager == null) {
logger.warn("The connection manager was unexpectedly null, please report a bug");
return;
}
if (command instanceof RefreshType) { if (command instanceof RefreshType) {
try { try {
connectionManager.getStatus(callbackLambda); connectionManager.getStatus(callbackLambda);
} catch (MideaAuthenticationException e) { } catch (MideaAuthenticationException e) {
logger.warn("Unable to proces command: {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
} catch (MideaConnectionException | MideaException e) { } catch (MideaConnectionException | MideaException e) {
logger.warn("Unable to proces command: {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} }
return; return;
@ -178,12 +176,14 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler
* Initialize is called on first pass or when a device parameter is changed * Initialize is called on first pass or when a device parameter is changed
* The basic check is if the information from Discovery (or the user update) * The basic check is if the information from Discovery (or the user update)
* is valid. Because V2 devices do not require a cloud provider (or token/key) * is valid. Because V2 devices do not require a cloud provider (or token/key)
* The check is for the IP, port and deviceID. This method also resets the dropped * The first check is for the IP, port and deviceID. The second part
* commands, disconnects the socket and stops the connection monitor (if these were * checks the security configuration if required (V3 device).
* running)
*/ */
@Override @Override
public void initialize() { public void initialize() {
if (isPollRunning) {
stopScheduler();
}
config = getConfigAs(MideaACConfiguration.class); config = getConfigAs(MideaACConfiguration.class);
if (!config.isValid()) { if (!config.isValid()) {
@ -196,10 +196,13 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler
try { try {
discoveryService.discoverThing(config.ipAddress, this); discoveryService.discoverThing(config.ipAddress, this);
return;
} catch (Exception e) { } catch (Exception e) {
logger.error("Discovery failure for {}: {}", thing.getUID(), e.getMessage()); logger.error("Discovery failure for {}: {}", thing.getUID(), e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Discovery failure. Check configuration.");
return;
} }
return;
} else { } else {
logger.debug("MideaACHandler config of {} is invalid. Check configuration", thing.getUID()); logger.debug("MideaACHandler config of {} is invalid. Check configuration", thing.getUID());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
@ -215,8 +218,10 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler
logger.info("Retrieving Token and/or Key from cloud"); logger.info("Retrieving Token and/or Key from cloud");
CloudProviderDTO cloudProvider = CloudProviderDTO.getCloudProvider(config.cloud); CloudProviderDTO cloudProvider = CloudProviderDTO.getCloudProvider(config.cloud);
getTokenKeyCloud(cloudProvider); getTokenKeyCloud(cloudProvider);
return;
} else { } else {
logger.warn("Configuration invalid for {}", thing.getUID()); logger.warn("Configuration invalid for {} and no account info to retrieve from cloud", thing.getUID());
return;
} }
} else { } else {
logger.debug("Security Configuration (V3 Device) valid for {}", thing.getUID()); logger.debug("Security Configuration (V3 Device) valid for {}", thing.getUID());
@ -228,21 +233,29 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler
config.ipPort, config.timeout, config.key, config.token, config.cloud, config.email, config.password, config.ipPort, config.timeout, config.key, config.token, config.cloud, config.email, config.password,
config.deviceId, config.version, config.promptTone); config.deviceId, config.version, config.promptTone);
// startScheduler(2, config.pollingTime, TimeUnit.SECONDS); startScheduler(2, config.pollingTime, TimeUnit.SECONDS);
scheduler.scheduleWithFixedDelay(this::pollJob, 2, config.pollingTime, TimeUnit.SECONDS);
} }
public void startScheduler(long initialDelay, long delay, TimeUnit unit) { /**
scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, initialDelay, delay, unit); * Starts the Scheduler for the Polling
logger.debug("Scheduled task started"); *
* @param initialDelay Seconds before first Poll
* @param delay Seconds between Polls
* @param unit Seconds
*/
private void startScheduler(long initialDelay, long delay, TimeUnit unit) {
if (scheduledTask == null) {
isPollRunning = true;
scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, initialDelay, delay, unit);
logger.debug("Scheduled task started");
} else {
logger.debug("Scheduler already running");
}
} }
private void pollJob() { private void pollJob() {
ConnectionManager connectionManager = this.connectionManager; ConnectionManager connectionManager = this.connectionManager;
if (connectionManager == null) {
logger.warn("The connection manager was unexpectedly null, please report a bug");
return;
}
try { try {
connectionManager.getStatus(callbackLambda); connectionManager.getStatus(callbackLambda);
updateStatus(ThingStatus.ONLINE); updateStatus(ThingStatus.ONLINE);
@ -315,13 +328,15 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler
Object propertyIpPort = Objects.requireNonNull(discoveryProps.get(CONFIG_IP_PORT)); Object propertyIpPort = Objects.requireNonNull(discoveryProps.get(CONFIG_IP_PORT));
configuration.put(CONFIG_IP_PORT, propertyIpPort.toString()); configuration.put(CONFIG_IP_PORT, propertyIpPort.toString());
Object propertyVersion = Objects.requireNonNull(discoveryProps.get(CONFIG_VERSION));
BigDecimal bigDecimalVersion = new BigDecimal((String) propertyVersion);
logger.trace("Property Version in Handler {}", bigDecimalVersion.intValue());
configuration.put(CONFIG_VERSION, bigDecimalVersion.intValue());
updateConfiguration(configuration); updateConfiguration(configuration);
properties = editProperties(); properties = editProperties();
Object propertyVersion = Objects.requireNonNull(discoveryProps.get(PROPERTY_VERSION));
properties.put(PROPERTY_VERSION, propertyVersion.toString());
Object propertySN = Objects.requireNonNull(discoveryProps.get(PROPERTY_SN)); Object propertySN = Objects.requireNonNull(discoveryProps.get(PROPERTY_SN));
properties.put(PROPERTY_SN, propertySN.toString()); properties.put(PROPERTY_SN, propertySN.toString());
@ -332,10 +347,14 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler
properties.put(PROPERTY_TYPE, propertyType.toString()); properties.put(PROPERTY_TYPE, propertyType.toString());
updateProperties(properties); updateProperties(properties);
initialize(); initialize();
} }
/**
* Gets the token and key from the Cloud
*
* @param cloudProvider Cloud Provider account
*/
public void getTokenKeyCloud(CloudProviderDTO cloudProvider) { public void getTokenKeyCloud(CloudProviderDTO cloudProvider) {
CloudDTO cloud = getClouds().get(config.email, config.password, cloudProvider); CloudDTO cloud = getClouds().get(config.email, config.password, cloudProvider);
if (cloud != null) { if (cloud != null) {
@ -350,7 +369,7 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler
logger.trace("Token: {}", tk.token()); logger.trace("Token: {}", tk.token());
logger.trace("Key: {}", tk.key()); logger.trace("Key: {}", tk.key());
logger.info("Token and Key obtained from cloud, saving, initializing"); logger.info("Token and Key obtained from cloud, saving, back to initialize");
initialize(); initialize();
} else { } else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format( updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
@ -360,22 +379,20 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler
} }
} }
public void stopScheduler() { private void stopScheduler() {
ScheduledFuture<?> localScheduledTask = this.scheduledTask; ScheduledFuture<?> localScheduledTask = this.scheduledTask;
if (localScheduledTask != null && !localScheduledTask.isCancelled()) { if (localScheduledTask != null && !localScheduledTask.isCancelled()) {
localScheduledTask.cancel(true); localScheduledTask.cancel(true);
logger.debug("Scheduled task cancelled."); logger.debug("Scheduled task cancelled.");
isPollRunning = false;
scheduledTask = null; scheduledTask = null;
} }
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdownNow();
logger.debug("Scheduler service shut down.");
}
} }
@Override @Override
public void dispose() { public void dispose() {
// stopScheduler(); stopScheduler();
connectionManager.dispose(true);
} }
} }

View File

@ -68,6 +68,7 @@ public class Response {
logger.debug("Swing Mode: {}", getSwingMode()); logger.debug("Swing Mode: {}", getSwingMode());
logger.debug("Sleep Function: {}", getSleepFunction()); logger.debug("Sleep Function: {}", getSleepFunction());
logger.debug("Turbo Mode: {}", getTurboMode()); logger.debug("Turbo Mode: {}", getTurboMode());
logger.debug("Eco Mode: {}", getEcoMode());
logger.debug("Indoor Temperature: {}", getIndoorTemperature()); logger.debug("Indoor Temperature: {}", getIndoorTemperature());
logger.debug("Outdoor Temperature: {}", getOutdoorTemperature()); logger.debug("Outdoor Temperature: {}", getOutdoorTemperature());
logger.debug("LED Display: {}", getDisplayOn()); logger.debug("LED Display: {}", getDisplayOn());
@ -77,7 +78,6 @@ public class Response {
logger.trace("Prompt Tone: {}", getPromptTone()); logger.trace("Prompt Tone: {}", getPromptTone());
logger.trace("Appliance Error: {}", getApplianceError()); logger.trace("Appliance Error: {}", getApplianceError());
logger.trace("Auxiliary Heat: {}", getAuxHeat()); logger.trace("Auxiliary Heat: {}", getAuxHeat());
logger.trace("Eco Mode: {}", getEcoMode());
logger.trace("Fahrenheit: {}", getFahrenheit()); logger.trace("Fahrenheit: {}", getFahrenheit());
logger.trace("Humidity: {}", getHumidity()); logger.trace("Humidity: {}", getHumidity());
logger.trace("Alternate Target Temperature {}", getAlternateTargetTemperature()); logger.trace("Alternate Target Temperature {}", getAlternateTargetTemperature());

View File

@ -22,7 +22,7 @@ thing-type.config.mideaac.ac.email.description = Email for cloud account chosen
thing-type.config.mideaac.ac.ipAddress.label = IP Address thing-type.config.mideaac.ac.ipAddress.label = IP Address
thing-type.config.mideaac.ac.ipAddress.description = IP Address of the device. thing-type.config.mideaac.ac.ipAddress.description = IP Address of the device.
thing-type.config.mideaac.ac.ipPort.label = IP Port thing-type.config.mideaac.ac.ipPort.label = IP Port
thing-type.config.mideaac.ac.ipPort.description = IP port of the device (for V2: 6444). thing-type.config.mideaac.ac.ipPort.description = IP port of the device.
thing-type.config.mideaac.ac.key.label = Key thing-type.config.mideaac.ac.key.label = Key
thing-type.config.mideaac.ac.key.description = Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not known, enter email and password for Cloud to retrieve it). thing-type.config.mideaac.ac.key.description = Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not known, enter email and password for Cloud to retrieve it).
thing-type.config.mideaac.ac.password.label = Password thing-type.config.mideaac.ac.password.label = Password
@ -36,7 +36,7 @@ thing-type.config.mideaac.ac.timeout.description = Connecting timeout. Minimum t
thing-type.config.mideaac.ac.token.label = Token thing-type.config.mideaac.ac.token.label = Token
thing-type.config.mideaac.ac.token.description = Secret Token (length 128 HEX) used for secure connection authentication used with devices v3 (if not known, enter email and password for Cloud to retrieve it). thing-type.config.mideaac.ac.token.description = Secret Token (length 128 HEX) used for secure connection authentication used with devices v3 (if not known, enter email and password for Cloud to retrieve it).
thing-type.config.mideaac.ac.version.label = AC Version thing-type.config.mideaac.ac.version.label = AC Version
thing-type.config.mideaac.ac.version.description = Version 3 requires Token, Key and Cloud provider. Version 2 doesn't. Leave blank to discover thing-type.config.mideaac.ac.version.description = Version 3 requires Token, Key and Cloud provider. Version 2 doesn't.
# channel types # channel types

View File

@ -41,10 +41,10 @@
<label>IP Address</label> <label>IP Address</label>
<description>IP Address of the device.</description> <description>IP Address of the device.</description>
</parameter> </parameter>
<parameter name="ipPort" type="text" required="true"> <parameter name="ipPort" type="decimal" required="true">
<context>ipPort</context> <context>ipPort</context>
<label>IP Port</label> <label>IP Port</label>
<description>IP port of the device (for V2: 6444).</description> <description>IP port of the device.</description>
<default>6444</default> <default>6444</default>
</parameter> </parameter>
<parameter name="deviceId" type="text" required="true"> <parameter name="deviceId" type="text" required="true">
@ -106,11 +106,11 @@
<description>After sending a command device will play "ding" tone when command is received and executed.</description> <description>After sending a command device will play "ding" tone when command is received and executed.</description>
<default>false</default> <default>false</default>
</parameter> </parameter>
<parameter name="version" type="text" required="true"> <parameter name="version" type="decimal" required="true">
<context>version</context> <context>version</context>
<label>AC Version</label> <label>AC Version</label>
<description>Version 3 requires Token, Key and Cloud provider. Version 2 doesn't. Leave blank to discover</description> <description>Version 3 requires Token, Key and Cloud provider. Version 2 doesn't.</description>
<default>3</default> <default>0</default>
</parameter> </parameter>
</config-description> </config-description>

View File

@ -31,37 +31,84 @@ public class MideaACConfigurationTest {
MideaACConfiguration config = new MideaACConfiguration(); MideaACConfiguration config = new MideaACConfiguration();
/** /**
* Test for valid Configs * Test for valid step 1 Configs
*/ */
@Test @Test
public void testValidConfigs() { public void testValidConfigs() {
config.ipAddress = "192.168.0.1"; config.ipAddress = "192.168.0.1";
config.ipPort = 6444; config.ipPort = 6444;
config.deviceId = "1234567890"; config.deviceId = "1234567890";
config.version = 3;
assertTrue(config.isValid()); assertTrue(config.isValid());
assertFalse(config.isDiscoveryNeeded()); assertFalse(config.isDiscoveryNeeded());
} }
/** /**
* Test for non-valid configs * Test for non-valid step 1 configs
*/ */
@Test @Test
public void testnonValidConfigs() { public void testnonValidConfigs() {
config.ipAddress = "192.168.0.1"; config.ipAddress = "192.168.0.1";
config.ipPort = 0; config.ipPort = 0;
config.deviceId = "1234567890"; config.deviceId = "1234567890";
config.version = 3;
assertFalse(config.isValid()); assertFalse(config.isValid());
assertTrue(config.isDiscoveryNeeded()); assertTrue(config.isDiscoveryNeeded());
} }
/** /**
* Test for bad IP configs * Test for valid Security Configs
*/
@Test
public void testValidSecurityConfigs() {
config.key = "97c65a4eed4f49fda06a1a51d5cbd61d2c9b81d103ca4ca689f352a07a16fae6";
config.token = "D24046B597DB9C8A7CA029660BC606F3FD7EBF12693E73B2EF1FFE4C3B7CA00C824E408C9F3CE972CC0D3F8250AD79D0E67B101B47AC2DD84B396E52EA05193F";
config.cloud = "NetHome Plus";
assertTrue(config.isV3ConfigValid());
}
/**
* Test for Invalid Security Configs
*/
@Test
public void testInvalidSecurityConfigs() {
config.key = "97c65a4eed4f49fda06a1a51d5cbd61d2c9b81d103ca4ca689f352a07a16fae6";
config.token = "D24046B597DB9C8A7CA029660BC606F3FD7EBF12693E73B2EF1FFE4C3B7CA00C824E408C9F3CE972CC0D3F8250AD79D0E67B101B47AC2DD84B396E52EA05193F";
config.cloud = "";
assertFalse(config.isV3ConfigValid());
}
/**
* Test for if key and token are obtainable from cloud
*/
@Test
public void testIfTokenAndKeyCanBeObtainedFromCloud() {
config.email = "someemail.com";
config.password = "somestrongpassword";
config.cloud = "NetHome Plus";
assertTrue(config.isTokenKeyObtainable());
}
/**
* Test for if key and token cannot be obtaines from cloud
*/
@Test
public void testIfTokenAndKeyCanNotBeObtainedFromCloud() {
config.email = "";
config.password = "somestrongpassword";
config.cloud = "NetHome Plus";
assertFalse(config.isTokenKeyObtainable());
}
/**
* Test for bad IP v.4 address
*/ */
@Test @Test
public void testBadIpConfigs() { public void testBadIpConfigs() {
config.ipAddress = "192.1680.1"; config.ipAddress = "192.1680.1";
config.ipPort = 6444; config.ipPort = 6444;
config.deviceId = "1234567890"; config.deviceId = "1234567890";
config.version = 3;
assertTrue(config.isValid()); assertTrue(config.isValid());
assertTrue(config.isDiscoveryNeeded()); assertTrue(config.isDiscoveryNeeded());
} }

View File

@ -40,6 +40,19 @@ public class MideaACDiscoveryServiceTest {
String mSmartId = "", mSmartVersion = "", mSmartip = "", mSmartPort = "", mSmartSN = "", mSmartSSID = "", String mSmartId = "", mSmartVersion = "", mSmartip = "", mSmartPort = "", mSmartSN = "", mSmartSSID = "",
mSmartType = ""; mSmartType = "";
/**
* Test Version
*/
@Test
public void testVersion() {
if (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("8370")) {
mSmartVersion = "3";
} else {
mSmartVersion = "2";
}
assertEquals("3", mSmartVersion);
}
/** /**
* Test Id * Test Id
*/ */