/** * Copyright (c) 2010-2016 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.samsungac.internal; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.SocketTimeoutException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import org.apache.commons.ssl.KeyMaterial; import org.apache.commons.ssl.SSLClient; import org.apache.commons.ssl.TrustMaterial; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Class to that connects to the air conditioner, using IP-address and * MAC-address. * This class talks to the air conditioner and makes sure the connections * is up and running. Otherwise it will reconnect. * Also if no token is given to the constructor, a token will be requested from * the air conditioner at the first login * * @author Stein Tore Tøsse * @since 1.6.0 * */ public class AirConditioner { private static final Logger logger = LoggerFactory.getLogger(AirConditioner.class); private static final int DEFAULT_PORT = 2878; private String IP; private int PORT = DEFAULT_PORT; private String MAC; private String TOKEN_STRING; private String CERTIFICATE_FILE_NAME; private String CERTIFICATE_PASSWORD = ""; private Map<CommandEnum, String> statusMap = new HashMap<CommandEnum, String>(); private SSLSocket socket; /** * This is the method to call first, it will try to connect to the given IP-, and MAC- * address. If no token is specified, it will try to ask the air conditioner to give * it a token. * * When a token has been received from the air conditioner, we will try to login with this token. * If a connection is established, the method will return itself. * * @return An instance of itself, which holds the state of the air conditioner * @throws Exception If something goes wrong while trying to connect */ public AirConditioner login() throws Exception { try { connect(); getToken(); loginWithToken(); } catch (Exception e) { logger.debug("Disconnecting... with exception: {}", e.toString()); disconnect(); throw e; } return this; } /** * Method should be called when all communication has finished. * For example when OpenHAB is being shut down. * * Will only disconnect if we are already connected. */ public void disconnect() { try { if (socket != null) { socket.close(); } socket = null; logger.debug("Disconnected from AC: {}", IP); } catch (IOException e) { logger.warn("Could not disconnect from Air Conditioner with IP: {}", IP, e); } finally { socket = null; } } /** * * @return true if connected to air conditioner, otherwise false */ public boolean isConnected() { return socket != null && socket.isConnected(); } private Map<CommandEnum, String> loginWithToken() throws Exception { if (TOKEN_STRING != null) { writeLine("<Request Type=\"AuthToken\"><User Token=\"" + TOKEN_STRING + "\" /></Request>"); handleResponse(); } else { throw new Exception("Must connect and retrieve a token before login in"); } return getStatus(); } private void getToken() throws Exception { while (TOKEN_STRING == null) { handleResponse(); Thread.sleep(2000); } logger.debug("Token has been acquired: '{}'", TOKEN_STRING); } /** * Handle response when we are not waiting for a specific answer. * * @throws Exception */ private void handleResponse() throws Exception { handleResponse(null, null, null); } /** * Handling of the responses is done by reading a response from the air conditioner, * until there's no more responses to read. This is because the air conditioner will * send us messages each time some presses the remote or some state of the air conditioner * changes. * * @param commandId An id of the command we are waiting for a response on. Not mandatory * @throws Exception Is thrown if we cannot parse the response from the air conditioner */ private void handleResponse(String commandId, CommandEnum command, String value) throws Exception { String line; while ((line = readLine(socket)) != null) { if (line == null || ResponseParser.isFirstLine(line)) { continue; } if (ResponseParser.isNotLoggedInResponse(line)) { if (TOKEN_STRING != null) { return; } writeLine("<Request Type=\"GetToken\" />"); continue; } if (ResponseParser.isFailedAuthenticationResponse(line)) { throw new Exception("failed to connect: '" + line + "'"); } if (commandId != null && ResponseParser.isCorrectCommandResponse(line, commandId)) { logger.debug("Correct command response: '{}'", line); if (command != null && statusMap.get(command).equals(value)) { return; } else { logger.debug("Continue, cause '{}' is not like '{}'", value, statusMap.get(command)); continue; } } if (ResponseParser.isResponseWithToken(line)) { TOKEN_STRING = ResponseParser.parseTokenFromResponse(line); logger.debug("Received TOKEN from AC: '{}'", TOKEN_STRING); return; } if (ResponseParser.isReadyForTokenResponse(line)) { logger.debug("NO TOKEN SET! Please switch off and on the air conditioner within 30 seconds"); return; } if (ResponseParser.isSuccessfulLoginResponse(line)) { logger.debug("SuccessfulLoginResponse: '{}'", line); return; } if (ResponseParser.isDeviceState(line)) { logger.debug("Response is device state '{}'", line); statusMap.clear(); statusMap = ResponseParser.parseStatusResponse(line); continue; } if (ResponseParser.isDeviceControl(line)) { logger.debug("DeviceControl: '{}'", line); continue; } if (ResponseParser.isUpdateStatus(line)) { logger.debug("Response is update status: '{}'", line); Pattern pattern = Pattern.compile("Attr ID=\"(.*)\" Value=\"(.*)\""); Matcher matcher = pattern.matcher(line); if (matcher.groupCount() == 2) { try { matcher.find(); CommandEnum cmd = CommandEnum.valueOf(matcher.group(1)); if (cmd != null) { statusMap.put(cmd, matcher.group(2)); logger.debug("Setting: {} to {} ", cmd.name(), matcher.group(2)); } } catch (IllegalStateException e) { logger.info("IllegalStateException when trying to update status, with response: {}", line, e); } } continue; } if (commandId != null && !ResponseParser.isCorrectCommandResponse(line, commandId)) { logger.debug("Response with incrorrect commandId: '{}' should have been: '{}'", line, commandId); continue; } logger.debug("Got response:'{}'", line); } } private void writeLine(String line) throws Exception { logger.debug("Sending request:'{}'", line); if (!isConnected()) { login(); } BufferedWriter writer; try { writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); writer.write(line); writer.newLine(); writer.flush(); } catch (Exception e) { logger.debug("Could not write line. Disconnecting.", e); disconnect(); throw (e); } } String readLine(SSLSocket socket) throws Exception { if (!isConnected()) { login(); } BufferedReader r = new BufferedReader(new InputStreamReader(socket.getInputStream())); try { return r.readLine(); } catch (SocketTimeoutException e) { logger.debug("Nothing more to read from AC"); } catch (SSLException e) { logger.debug("Got SSL Exception. Disconnecting."); disconnect(); } return null; } private void connect() throws Exception { if (isConnected()) { return; } else { logger.debug("Disconnected so we'll try again"); disconnect(); } if (CERTIFICATE_FILE_NAME != null && new File(CERTIFICATE_FILE_NAME).isFile()) { if (CERTIFICATE_PASSWORD == null) { CERTIFICATE_PASSWORD = ""; } try { SSLClient client = new SSLClient(); client.addTrustMaterial(TrustMaterial.DEFAULT); client.setCheckHostname(false); client.setKeyMaterial(new KeyMaterial(CERTIFICATE_FILE_NAME, CERTIFICATE_PASSWORD.toCharArray())); client.setConnectTimeout(10000); socket = (SSLSocket) client.createSocket(IP, PORT); socket.setSoTimeout(2000); socket.startHandshake(); } catch (Exception e) { throw new Exception("Could not connect using certificate: " + CERTIFICATE_FILE_NAME, e); } } else { try { SSLContext ctx = SSLContext.getInstance("TLS"); final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { } public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { } } }; ctx.init(null, trustAllCerts, null); socket = (SSLSocket) ctx.getSocketFactory().createSocket(IP, PORT); socket.setSoTimeout(2000); socket.startHandshake(); } catch (Exception e) { throw new Exception("Cannot connect to " + IP + ":" + PORT, e); } } handleResponse(); } /** * Method to send a command to the air conditioner. Will generate a "unique" id for each * command we send, so that we can wait and check the return value of our sent command. * * @param command The command to send to the air conditioner * @param value Value to change to * @return the generated command id * @throws Exception If we cannot write to the air conditioner or if we cannot handle the response */ public Map<CommandEnum, String> sendCommand(CommandEnum command, String value) throws Exception { logger.debug("Sending command: '{}' with value: '{}'", command.toString(), value); String id = "cmd" + Math.round(Math.random() * 10000); writeLine("<Request Type=\"DeviceControl\"><Control CommandID=\"" + id + "\" DUID=\"" + MAC + "\"><Attr ID=\"" + command + "\" Value=\"" + value + "\" /></Control></Request>"); handleResponse(id, command, value); return statusMap; } /** * Get the status for each of the commands in {@link CommandEnum} * * @return A Map of the current air conditioner status * @throws Exception If we cannot send a command or if there is a problem parsing the results */ public Map<CommandEnum, String> getStatus() throws Exception { try { writeLine("<Request Type=\"DeviceState\" DUID=\"" + MAC + "\"></Request>"); handleResponse(); } catch (Exception e) { throw new Exception("Could not update status for air conditioner with IP: " + IP, e); } return statusMap; } /** * * @return the configured IP-address of the air conditioner */ public String getIpAddress() { return IP; } /** * * @param ipAddress The IP-address of the air conditioner */ public void setIpAddress(String ipAddress) { IP = ipAddress; } /** * * @param port The TCP/IP port number on which the air conditioner is listening */ public void setPort(int port) { PORT = port; } /** * * @param macAddress The MAC-address of the air conditioner */ public void setMacAddress(String macAddress) { MAC = macAddress; } /** * * @param token The token to use when connecting to the air conditioner */ public void setToken(String token) { TOKEN_STRING = token; } /** * * @param fileName for the certificate to use */ public void setCertificateFileName(String fileName) { CERTIFICATE_FILE_NAME = fileName; } /** * * @param password for the certificate */ public void setCertificatePassword(String password) { CERTIFICATE_PASSWORD = password; } @Override public String toString() { return "Samsung AC: [" + (IP != null ? IP : "") + ":" + PORT + ", MAC: " + (MAC != null ? MAC : "") + ", TOKEN: " + (TOKEN_STRING != null ? TOKEN_STRING : "") + "]"; } }