diff --git a/bundles/binding/org.openhab.binding.s300th/src/main/java/org/openhab/binding/s300th/internal/S300THBinding.java b/bundles/binding/org.openhab.binding.s300th/src/main/java/org/openhab/binding/s300th/internal/S300THBinding.java index 179e4d12cfe..ca1775a3178 100644 --- a/bundles/binding/org.openhab.binding.s300th/src/main/java/org/openhab/binding/s300th/internal/S300THBinding.java +++ b/bundles/binding/org.openhab.binding.s300th/src/main/java/org/openhab/binding/s300th/internal/S300THBinding.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2015, openHAB.org and others. + * Copyright (c) 2010-2016, openHAB.org and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -17,7 +17,6 @@ import org.openhab.core.binding.AbstractActiveBinding; import org.openhab.core.items.Item; import org.openhab.core.library.types.DecimalType; -import org.openhab.core.library.types.OnOffType; import org.openhab.io.transport.cul.CULDeviceException; import org.openhab.io.transport.cul.CULHandler; import org.openhab.io.transport.cul.CULListener; @@ -28,234 +27,243 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; - /** * Implement this class if you are going create an actively polling service like * querying a Website/Device. - * + * * @author Till Klocke * @since 1.4.0 */ -public class S300THBinding extends AbstractActiveBinding implements ManagedService, CULListener { - - private static final Logger logger = LoggerFactory.getLogger(S300THBinding.class); - - private final static String KS_300_ADDRESS = "ks300"; - - /** - * the refresh interval which is used to poll values from the S300TH server - * (optional, defaults to 60000ms) - */ - private long refreshInterval = 60000; - - private final static String KEY_DEVICE_NAME = "device"; - - private String deviceName; - - private CULHandler cul; - - public S300THBinding() { - } - - public void activate() { - } - - public void deactivate() { - closeCUL(); - } - - private void setNewDeviceName(String deviceName) { - if (deviceName == null) { - logger.error("Device name was null"); - return; - } - if (this.deviceName != null && this.deviceName.equals(deviceName)) { - return; - } - closeCUL(); - this.deviceName = deviceName; - openCUL(); - } - - private void openCUL() { - try { - cul = CULManager.getOpenCULHandler(deviceName, CULMode.SLOW_RF); - cul.registerListener(this); - } catch (CULDeviceException e) { - logger.error("Can't open CUL handler for device " + deviceName, e); - } - } - - private void closeCUL() { - if (cul != null) { - cul.unregisterListener(this); - CULManager.close(cul); - } - } - - /** - * @{inheritDoc - */ - @Override - protected long getRefreshInterval() { - return refreshInterval; - } - - /** - * @{inheritDoc - */ - @Override - protected String getName() { - return "S300TH Refresh Service"; - } - - /** - * @{inheritDoc - */ - @Override - protected void execute() { - // Ignore - } - - /** - * @{inheritDoc - */ - @Override - public void updated(Dictionary config) throws ConfigurationException { - logger.debug("Received new config"); - if (config != null) { - - // to override the default refresh interval one has to add a - // parameter to openhab.cfg like - // :refresh= - String refreshIntervalString = (String) config.get("refresh"); - if (StringUtils.isNotBlank(refreshIntervalString)) { - refreshInterval = Long.parseLong(refreshIntervalString); - } - String deviceName = (String) config.get(KEY_DEVICE_NAME); - if (StringUtils.isEmpty(deviceName)) { - logger.error("No device name configured"); - setProperlyConfigured(false); - throw new ConfigurationException(KEY_DEVICE_NAME, "The device name can't be empty"); - } else { - setNewDeviceName(deviceName); - } - - setProperlyConfigured(true); - // read further config parameters here ... - - } - } - - @Override - public void dataReceived(String data) { - if (data.startsWith("K")) { - int firstByte = Integer.parseInt(data.substring(1, 2), 16); - int typByte = Integer.parseInt(data.substring(2, 3), 16) & 7; - int sfirstByte = firstByte & 7; - - if (sfirstByte == 7) { - logger.debug("Received WS7000 message, but parsing for WS7000 is not implemented"); - // TODO parse different sensors from WS7000 (?) - } else { - if (data.length() > 8 && data.length() < 13) { - // S300TH default size = 9 characters - parseS300THData(data); - } else if (data.length() > 14 && data.length() < 20) { - // KS300 default size = 15 characters. - // sometime we got values with more characters. - parseKS300Data(data); - } else { - logger.warn("Received unparseable message: " + data); - } - } - } - } - - /** - * Parse KS 300 data - * - * @param data - */ - private void parseKS300Data(String data) { - // TODO parse address and other bytes - int rainValue = ParseUtils.parseKS300RainCounter(data); - double windValue = ParseUtils.parseKS300Wind(data); - double humidity = ParseUtils.parseKS300Humidity(data); - double temperature = ParseUtils.parseTemperature(data); - boolean isRaining = ParseUtils.isKS300Raining(data); - - logger.debug("Received data '" + data + "' from device with address ks300 : temperature: " + temperature - + " humidity: " + humidity + " wind: " + windValue + " rain: " + rainValue + " isRain: " + isRaining ); - - for (Datapoint datapoint : Datapoint.values()) { - S300THBindingConfig config = findConfig(KS_300_ADDRESS, datapoint); - if (config == null) { - continue; - } - double value = 0.0; - switch (datapoint) { - case TEMPERATURE: - value = temperature; - break; - case HUMIDITY: - value = humidity; - break; - case WIND: - value = windValue; - break; - case RAIN: - value = rainValue; - break; - case IS_RAINING: - value = isRaining ? 1 : 0; - break; - } - updateItem(config.item, value); - } - } - - /** - * Parse S300TH data - * - * @param data - */ - private void parseS300THData(String data) { - String address = ParseUtils.parseS300THAddress(data); - double temperature = ParseUtils.parseTemperature(data); - double humidity = ParseUtils.parseS300THHumidity(data); - logger.debug("Received data '" + data + "' from device with address " + address + " : temperature: " + temperature - + " humidity: " + humidity); - - S300THBindingConfig temperatureConfig = findConfig(address, Datapoint.TEMPERATURE); - if (temperatureConfig != null) { - updateItem(temperatureConfig.item, temperature); - } - S300THBindingConfig humidityConfig = findConfig(address, Datapoint.HUMIDITY); - if (humidityConfig != null) { - updateItem(humidityConfig.item, humidity); - } - } - - private void updateItem(Item item, double value) { - DecimalType type = new DecimalType(value); - eventPublisher.postUpdate(item.getName(), type); - } - - private S300THBindingConfig findConfig(String address, Datapoint datapoint) { - for (S300THBindingProvider provider : this.providers) { - S300THBindingConfig config = provider.getBindingConfigForAddressAndDatapoint(address, datapoint); - if (config != null) { - return config; - } - } - return null; - } - - @Override - public void error(Exception e) { - logger.error("Received error from CUL instead fo data", e); - - } +public class S300THBinding extends AbstractActiveBindingimplements ManagedService, CULListener { + + private static final Logger logger = LoggerFactory.getLogger(S300THBinding.class); + + private final static String KS_300_ADDRESS = "ks300"; + + /** + * the refresh interval which is used to poll values from the S300TH server + * (optional, defaults to 60000ms) + */ + private long refreshInterval = 60000; + + private final static String KEY_DEVICE_NAME = "device"; + + private String deviceName; + + private CULHandler cul; + + public S300THBinding() { + } + + @Override + public void activate() { + } + + @Override + public void deactivate() { + closeCUL(); + } + + private void setNewDeviceName(String deviceName) { + if (deviceName == null) { + logger.error("Device name was null"); + return; + } + if (this.deviceName != null && this.deviceName.equals(deviceName)) { + return; + } + closeCUL(); + this.deviceName = deviceName; + openCUL(); + } + + private void openCUL() { + try { + cul = CULManager.getOpenCULHandler(deviceName, CULMode.SLOW_RF); + cul.registerListener(this); + } catch (CULDeviceException e) { + logger.error("Can't open CUL handler for device " + deviceName, e); + } + } + + private void closeCUL() { + if (cul != null) { + cul.unregisterListener(this); + CULManager.close(cul); + } + } + + /** + * @{inheritDoc + */ + @Override + protected long getRefreshInterval() { + return refreshInterval; + } + + /** + * @{inheritDoc + */ + @Override + protected String getName() { + return "S300TH Refresh Service"; + } + + /** + * @{inheritDoc + */ + @Override + protected void execute() { + // Ignore + } + + protected void addBindingProvider(S300THBindingProvider bindingProvider) { + super.addBindingProvider(bindingProvider); + } + + protected void removeBindingProvider(S300THBindingProvider bindingProvider) { + super.removeBindingProvider(bindingProvider); + } + + /** + * {@inheritDoc} + */ + @Override + public void updated(Dictionary config) throws ConfigurationException { + logger.debug("Received new config"); + if (config != null) { + + // to override the default refresh interval one has to add a + // parameter to openhab.cfg like + // :refresh= + String refreshIntervalString = (String) config.get("refresh"); + if (StringUtils.isNotBlank(refreshIntervalString)) { + refreshInterval = Long.parseLong(refreshIntervalString); + } + String deviceName = (String) config.get(KEY_DEVICE_NAME); + if (StringUtils.isEmpty(deviceName)) { + logger.error("No device name configured"); + setProperlyConfigured(false); + throw new ConfigurationException(KEY_DEVICE_NAME, "The device name can't be empty"); + } else { + setNewDeviceName(deviceName); + } + + setProperlyConfigured(true); + // read further config parameters here ... + + } + } + + @Override + public void dataReceived(String data) { + if (data.startsWith("K")) { + int firstByte = Integer.parseInt(data.substring(1, 2), 16); + int typByte = Integer.parseInt(data.substring(2, 3), 16) & 7; + int sfirstByte = firstByte & 7; + + if (sfirstByte == 7) { + logger.debug("Received WS7000 message, but parsing for WS7000 is not implemented"); + // TODO parse different sensors from WS7000 (?) + } else { + if (data.length() > 8 && data.length() < 13) { + // S300TH default size = 9 characters + parseS300THData(data); + } else if (data.length() > 14 && data.length() < 20) { + // KS300 default size = 15 characters. + // sometime we got values with more characters. + parseKS300Data(data); + } else { + logger.warn("Received unparseable message: " + data); + } + } + } + } + + /** + * Parse KS 300 data + * + * @param data + */ + private void parseKS300Data(String data) { + // TODO parse address and other bytes + int rainValue = ParseUtils.parseKS300RainCounter(data); + double windValue = ParseUtils.parseKS300Wind(data); + double humidity = ParseUtils.parseKS300Humidity(data); + double temperature = ParseUtils.parseTemperature(data); + boolean isRaining = ParseUtils.isKS300Raining(data); + + logger.debug("Received data '" + data + "' from device with address ks300 : temperature: " + temperature + + " humidity: " + humidity + " wind: " + windValue + " rain: " + rainValue + " isRain: " + isRaining); + + for (Datapoint datapoint : Datapoint.values()) { + S300THBindingConfig config = findConfig(KS_300_ADDRESS, datapoint); + if (config == null) { + continue; + } + double value = 0.0; + switch (datapoint) { + case TEMPERATURE: + value = temperature; + break; + case HUMIDITY: + value = humidity; + break; + case WIND: + value = windValue; + break; + case RAIN: + value = rainValue; + break; + case IS_RAINING: + value = isRaining ? 1 : 0; + break; + } + updateItem(config.item, value); + } + } + + /** + * Parse S300TH data + * + * @param data + */ + private void parseS300THData(String data) { + String address = ParseUtils.parseS300THAddress(data); + double temperature = ParseUtils.parseTemperature(data); + double humidity = ParseUtils.parseS300THHumidity(data); + logger.debug("Received data '" + data + "' from device with address " + address + " : temperature: " + + temperature + " humidity: " + humidity); + + S300THBindingConfig temperatureConfig = findConfig(address, Datapoint.TEMPERATURE); + if (temperatureConfig != null) { + updateItem(temperatureConfig.item, temperature); + } + S300THBindingConfig humidityConfig = findConfig(address, Datapoint.HUMIDITY); + if (humidityConfig != null) { + updateItem(humidityConfig.item, humidity); + } + } + + private void updateItem(Item item, double value) { + DecimalType type = new DecimalType(value); + eventPublisher.postUpdate(item.getName(), type); + } + + private S300THBindingConfig findConfig(String address, Datapoint datapoint) { + for (S300THBindingProvider provider : this.providers) { + S300THBindingConfig config = provider.getBindingConfigForAddressAndDatapoint(address, datapoint); + if (config != null) { + return config; + } + } + return null; + } + + @Override + public void error(Exception e) { + logger.error("Received error from CUL instead of data", e); + + } } diff --git a/bundles/binding/org.openhab.binding.samsungac/src/main/java/org/binding/openhab/samsungac/communicator/AirConditioner.java b/bundles/binding/org.openhab.binding.samsungac/src/main/java/org/binding/openhab/samsungac/communicator/AirConditioner.java index 0f290fbb94b..120c6e8ddcd 100644 --- a/bundles/binding/org.openhab.binding.samsungac/src/main/java/org/binding/openhab/samsungac/communicator/AirConditioner.java +++ b/bundles/binding/org.openhab.binding.samsungac/src/main/java/org/binding/openhab/samsungac/communicator/AirConditioner.java @@ -77,7 +77,7 @@ public AirConditioner login() throws Exception { getToken(); loginWithToken(); } catch (Exception e) { - logger.debug("Disconneting...", e); + logger.debug("Disconnecting...", e); disconnect(); throw e; } diff --git a/bundles/binding/org.openhab.binding.samsungtv/src/main/java/de/quist/samy/remocon/RemoteSession.java b/bundles/binding/org.openhab.binding.samsungtv/src/main/java/de/quist/samy/remocon/RemoteSession.java index f4ee1ac3812..654ecd27ca5 100755 --- a/bundles/binding/org.openhab.binding.samsungtv/src/main/java/de/quist/samy/remocon/RemoteSession.java +++ b/bundles/binding/org.openhab.binding.samsungtv/src/main/java/de/quist/samy/remocon/RemoteSession.java @@ -23,8 +23,8 @@ import java.net.UnknownHostException; import java.util.Arrays; import java.util.concurrent.TimeoutException; -import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Base64; import org.openhab.binding.samsungtv.internal.SamsungTvConnection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,254 +32,259 @@ /** * Copied from https://github.com/keremkusmezer/SamyGo-Android-Remote/tree/master/src/de/quist/samy/remocon, * since there is no binary build available anymore. Thanks to Tom Quist! - * + * * @author Tom Quist */ public class RemoteSession { - private static Logger logger = LoggerFactory.getLogger(SamsungTvConnection.class); - - private static final String APP_STRING = "iphone.iapp.samsung"; - private static final String TV_APP_STRING = "iphone..iapp.samsung"; - - private static final char[] ALLOWED_BYTES = new char[] {0x64, 0x00, 0x01, 0x00}; - private static final char[] DENIED_BYTES = new char[] {0x64, 0x00, 0x00, 0x00}; - private static final char[] TIMEOUT_BYTES = new char[] {0x65, 0x00}; - - public static final String ALLOWED = "ALLOWED"; - public static final String DENIED = "DENIED"; - public static final String TIMEOUT = "TIMEOUT"; - - private String applicationName; - private String uniqueId; - private String host; - private int port; - - private Socket socket; - - private InputStreamReader reader; - - private BufferedWriter writer; - - - private RemoteSession(String applicationName, String uniqueId, String host, int port) { - this.applicationName = applicationName; - this.uniqueId = uniqueId; - if (uniqueId == null) { - uniqueId = ""; - } - this.host = host; - this.port = port; - } - - public static RemoteSession create(String applicationName, String uniqueId, String host, int port) throws IOException, ConnectionDeniedException, TimeoutException { - RemoteSession session = new RemoteSession(applicationName, uniqueId, host, port); - String result = session.initialize(); - if (result.equals(ALLOWED)) { - return session; - } else if (result.equals(DENIED)) { - throw new ConnectionDeniedException(); - } else if (result.equals(TIMEOUT)) { - throw new TimeoutException(); - } else { - // for now we just assume to be connected - return session; - } - } - - private String initialize() throws UnknownHostException, IOException { - logger.debug("Creating socket for host " + host + " on port " + port); - - socket = new Socket(); - socket.connect(new InetSocketAddress(host, port), 5000); - - logger.debug("Socket successfully created and connected"); - InetAddress localAddress = socket.getLocalAddress(); - logger.debug("Local address is " + localAddress.getHostAddress()); - - logger.debug("Sending registration message"); - writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); - writer.append((char)0x00); - writeText(writer, APP_STRING); - writeText(writer, getRegistrationPayload(localAddress.getHostAddress())); - writer.flush(); - - InputStream in = socket.getInputStream(); - reader = new InputStreamReader(in); - String result = readRegistrationReply(reader); - //sendPart2(); - int i; - while ((i = in.available()) > 0) { - in.skip(i); - } - return result; - } - - @SuppressWarnings("unused") - private void sendPart2() throws IOException { - writer.append((char)0x00); - writeText(writer, TV_APP_STRING); - writeText(writer, new String(new char[] {0xc8, 0x00})); - } - - private void checkConnection() throws UnknownHostException, IOException { - if (socket.isClosed() || !socket.isConnected()) { - logger.debug("Connection closed, trying to reconnect..."); - try { - socket.close(); - } catch (IOException e) { - // Ignore any exception - } - initialize(); - logger.debug("Reconnected to server"); - } - } - - public void destroy() { - try { - socket.close(); - } catch (IOException e) { - // Ignore exception - } - } - - private String readRegistrationReply(Reader reader) throws IOException { - logger.debug("Reading registration reply"); - reader.read(); // Unknown byte 0x02 - String text1 = readText(reader); // Read "unknown.livingroom.iapp.samsung" for new RC and "iapp.samsung" for already registered RC - logger.debug("Received ID: " + text1); - char[] result = readCharArray(reader); // Read result sequence - if (Arrays.equals(result, ALLOWED_BYTES)) { - logger.debug("Registration successfull"); - return ALLOWED; - } else if (Arrays.equals(result, DENIED_BYTES)) { - logger.warn("Registration denied"); - return DENIED; - } else if (Arrays.equals(result, TIMEOUT_BYTES)) { - logger.warn("Registration timed out"); - return TIMEOUT; - } else { - StringBuilder sb = new StringBuilder(); - for (char c : result) { - sb.append(Integer.toHexString(c)); - sb.append(' '); - } - String hexReturn = sb.toString(); - { - logger.error("Received unknown registration reply: "+hexReturn); - } - return hexReturn; - } - } - - private String getRegistrationPayload(String ip) throws IOException { - StringWriter writer = new StringWriter(); - writer.append((char)0x64); - writer.append((char) 0x00); - writeBase64Text(writer, ip); - writeBase64Text(writer, uniqueId); - writeBase64Text(writer, applicationName); - writer.flush(); - return writer.toString(); - } - - private static String readText(Reader reader) throws IOException { - char[] buffer = readCharArray(reader); - return new String(buffer); - } - - private static char[] readCharArray(Reader reader) throws IOException { - if (reader.markSupported()) reader.mark(1024); - int length = reader.read(); - int delimiter = reader.read(); - if (delimiter != 0) { - if (reader.markSupported()) reader.reset(); - throw new IOException("Unsupported reply exception"); - } - char[] buffer = new char[length]; - reader.read(buffer); - return buffer; - } - - private static Writer writeText(Writer writer, String text) throws IOException { - return writer.append((char)text.length()).append((char) 0x00).append(text); - } - - private static Writer writeBase64Text(Writer writer, String text) throws IOException { - String b64 = new String(Base64.encodeBase64(text.getBytes())); - return writeText(writer, b64); - } - - @SuppressWarnings("unused") - private void internalSendKey(Key key) throws IOException { - writer.append((char)0x00); - writeText(writer, TV_APP_STRING); - writeText(writer, getKeyPayload(key)); - writer.flush(); - int i = reader.read(); // Unknown byte 0x00 - String t = readText(reader); // Read "iapp.samsung" - char[] c = readCharArray(reader); - } - - public void sendKey(Key key) throws IOException { - logger.debug("Sending key " + key.getValue() + "..."); - checkConnection(); - try { - internalSendKey(key); - } catch (SocketException e) { - logger.debug("Could not send key because the server closed the connection. Reconnecting..."); - initialize(); - logger.debug("Sending key " + key.getValue() + " again..."); - internalSendKey(key); - } - logger.debug("Successfully sent key " + key.getValue()); - } - - private String getKeyPayload(Key key) throws IOException { - StringWriter writer = new StringWriter(); - writer.append((char)0x00); - writer.append((char)0x00); - writer.append((char)0x00); - writeBase64Text(writer, key.getValue()); - writer.flush(); - return writer.toString(); - } - - @SuppressWarnings("unused") - private void internalSendText(String text) throws IOException { - writer.append((char)0x01); - writeText(writer, TV_APP_STRING); - writeText(writer, getTextPayload(text)); - writer.flush(); - if (!reader.ready()) { - return; - } - int i = reader.read(); // Unknown byte 0x02 - String t = readText(reader); // Read "iapp.samsung" - char[] c = readCharArray(reader); - } - - public void sendText(String text) throws IOException { - logger.debug("Sending text \"" + text + "\"..."); - checkConnection(); - try { - internalSendText(text); - } catch (SocketException e) { - logger.debug("Could not send key because the server closed the connection. Reconnecting..."); - initialize(); - logger.debug("Sending text \"" + text + "\" again..."); - internalSendText(text); - } - logger.debug("Successfully sent text \"" + text + "\""); - } - - private String getTextPayload(String text) throws IOException { - StringWriter writer = new StringWriter(); - writer.append((char)0x01); - writer.append((char)0x00); - writeBase64Text(writer, text); - writer.flush(); - return writer.toString(); - } + private static Logger logger = LoggerFactory.getLogger(SamsungTvConnection.class); + + private static final String APP_STRING = "iphone.iapp.samsung"; + private static final String TV_APP_STRING = "iphone..iapp.samsung"; + + private static final char[] ALLOWED_BYTES = new char[] { 0x64, 0x00, 0x01, 0x00 }; + private static final char[] DENIED_BYTES = new char[] { 0x64, 0x00, 0x00, 0x00 }; + private static final char[] TIMEOUT_BYTES = new char[] { 0x65, 0x00 }; + + public static final String ALLOWED = "ALLOWED"; + public static final String DENIED = "DENIED"; + public static final String TIMEOUT = "TIMEOUT"; + + private String applicationName; + private String uniqueId; + private String host; + private int port; + + private Socket socket; + + private InputStreamReader reader; + + private BufferedWriter writer; + + private RemoteSession(String applicationName, String uniqueId, String host, int port) { + this.applicationName = applicationName; + this.uniqueId = uniqueId; + if (uniqueId == null) { + uniqueId = ""; + } + this.host = host; + this.port = port; + } + + public static RemoteSession create(String applicationName, String uniqueId, String host, int port) + throws IOException, ConnectionDeniedException, TimeoutException { + RemoteSession session = new RemoteSession(applicationName, uniqueId, host, port); + String result = session.initialize(); + if (result.equals(ALLOWED)) { + return session; + } else if (result.equals(DENIED)) { + throw new ConnectionDeniedException(); + } else if (result.equals(TIMEOUT)) { + throw new TimeoutException(); + } else { + // for now we just assume to be connected + return session; + } + } + + private String initialize() throws UnknownHostException, IOException { + logger.debug("Creating socket for host " + host + " on port " + port); + + socket = new Socket(); + socket.connect(new InetSocketAddress(host, port), 5000); + + logger.debug("Socket successfully created and connected"); + InetAddress localAddress = socket.getLocalAddress(); + logger.debug("Local address is " + localAddress.getHostAddress()); + + logger.debug("Sending registration message"); + writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); + writer.append((char) 0x00); + writeText(writer, APP_STRING); + writeText(writer, getRegistrationPayload(localAddress.getHostAddress())); + writer.flush(); + + InputStream in = socket.getInputStream(); + reader = new InputStreamReader(in); + String result = readRegistrationReply(reader); + // sendPart2(); + int i; + while ((i = in.available()) > 0) { + in.skip(i); + } + return result; + } + + @SuppressWarnings("unused") + private void sendPart2() throws IOException { + writer.append((char) 0x00); + writeText(writer, TV_APP_STRING); + writeText(writer, new String(new char[] { 0xc8, 0x00 })); + } + + private void checkConnection() throws UnknownHostException, IOException { + if (socket.isClosed() || !socket.isConnected()) { + logger.debug("Connection closed, trying to reconnect..."); + try { + socket.close(); + } catch (IOException e) { + // Ignore any exception + } + initialize(); + logger.debug("Reconnected to server"); + } + } + + public void destroy() { + try { + socket.close(); + } catch (IOException e) { + // Ignore exception + } + } + + private String readRegistrationReply(Reader reader) throws IOException { + logger.debug("Reading registration reply"); + reader.read(); // Unknown byte 0x02 + String text1 = readText(reader); // Read "unknown.livingroom.iapp.samsung" for new RC and "iapp.samsung" for + // already registered RC + logger.debug("Received ID: " + text1); + char[] result = readCharArray(reader); // Read result sequence + if (Arrays.equals(result, ALLOWED_BYTES)) { + logger.debug("Registration successful"); + return ALLOWED; + } else if (Arrays.equals(result, DENIED_BYTES)) { + logger.warn("Registration denied"); + return DENIED; + } else if (Arrays.equals(result, TIMEOUT_BYTES)) { + logger.warn("Registration timed out"); + return TIMEOUT; + } else { + StringBuilder sb = new StringBuilder(); + for (char c : result) { + sb.append(Integer.toHexString(c)); + sb.append(' '); + } + String hexReturn = sb.toString(); + { + logger.error("Received unknown registration reply: " + hexReturn); + } + return hexReturn; + } + } + + private String getRegistrationPayload(String ip) throws IOException { + StringWriter writer = new StringWriter(); + writer.append((char) 0x64); + writer.append((char) 0x00); + writeBase64Text(writer, ip); + writeBase64Text(writer, uniqueId); + writeBase64Text(writer, applicationName); + writer.flush(); + return writer.toString(); + } + + private static String readText(Reader reader) throws IOException { + char[] buffer = readCharArray(reader); + return new String(buffer); + } + + private static char[] readCharArray(Reader reader) throws IOException { + if (reader.markSupported()) { + reader.mark(1024); + } + int length = reader.read(); + int delimiter = reader.read(); + if (delimiter != 0) { + if (reader.markSupported()) { + reader.reset(); + } + throw new IOException("Unsupported reply exception"); + } + char[] buffer = new char[length]; + reader.read(buffer); + return buffer; + } + + private static Writer writeText(Writer writer, String text) throws IOException { + return writer.append((char) text.length()).append((char) 0x00).append(text); + } + + private static Writer writeBase64Text(Writer writer, String text) throws IOException { + String b64 = new String(Base64.encodeBase64(text.getBytes())); + return writeText(writer, b64); + } + + @SuppressWarnings("unused") + private void internalSendKey(Key key) throws IOException { + writer.append((char) 0x00); + writeText(writer, TV_APP_STRING); + writeText(writer, getKeyPayload(key)); + writer.flush(); + int i = reader.read(); // Unknown byte 0x00 + String t = readText(reader); // Read "iapp.samsung" + char[] c = readCharArray(reader); + } + + public void sendKey(Key key) throws IOException { + logger.debug("Sending key " + key.getValue() + "..."); + checkConnection(); + try { + internalSendKey(key); + } catch (SocketException e) { + logger.debug("Could not send key because the server closed the connection. Reconnecting..."); + initialize(); + logger.debug("Sending key " + key.getValue() + " again..."); + internalSendKey(key); + } + logger.debug("Successfully sent key " + key.getValue()); + } + + private String getKeyPayload(Key key) throws IOException { + StringWriter writer = new StringWriter(); + writer.append((char) 0x00); + writer.append((char) 0x00); + writer.append((char) 0x00); + writeBase64Text(writer, key.getValue()); + writer.flush(); + return writer.toString(); + } + + @SuppressWarnings("unused") + private void internalSendText(String text) throws IOException { + writer.append((char) 0x01); + writeText(writer, TV_APP_STRING); + writeText(writer, getTextPayload(text)); + writer.flush(); + if (!reader.ready()) { + return; + } + int i = reader.read(); // Unknown byte 0x02 + String t = readText(reader); // Read "iapp.samsung" + char[] c = readCharArray(reader); + } + + public void sendText(String text) throws IOException { + logger.debug("Sending text \"" + text + "\"..."); + checkConnection(); + try { + internalSendText(text); + } catch (SocketException e) { + logger.debug("Could not send key because the server closed the connection. Reconnecting..."); + initialize(); + logger.debug("Sending text \"" + text + "\" again..."); + internalSendText(text); + } + logger.debug("Successfully sent text \"" + text + "\""); + } + + private String getTextPayload(String text) throws IOException { + StringWriter writer = new StringWriter(); + writer.append((char) 0x01); + writer.append((char) 0x00); + writeBase64Text(writer, text); + writer.flush(); + return writer.toString(); + } } diff --git a/bundles/binding/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/protocol/Ethm1Module.java b/bundles/binding/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/protocol/Ethm1Module.java index 1c89bf3a664..6ccf089cee3 100644 --- a/bundles/binding/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/protocol/Ethm1Module.java +++ b/bundles/binding/org.openhab.binding.satel/src/main/java/org/openhab/binding/satel/internal/protocol/Ethm1Module.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2015, openHAB.org and others. + * Copyright (c) 2010-2016, openHAB.org and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -21,232 +21,231 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.openhab.binding.satel.internal.protocol.EncryptionHelper; - /** * Represents Satel ETHM-1 module. Implements method required to connect and * communicate with that module over TCP/IP protocol. The module must have * integration protocol enable in DLOADX configuration options. - * + * * @author Krzysztof Goworek * @since 1.7.0 */ public class Ethm1Module extends SatelModule { - private static final Logger logger = LoggerFactory.getLogger(Ethm1Module.class); - - private String host; - private int port; - private String encryptionKey; - - /** - * Creates new instance with host, port, timeout and encryption key set to - * specified values. - * - * @param host - * host name or IP of ETHM-1 module - * @param port - * TCP port the module listens on - * @param timeout - * timeout value in milliseconds for connect/read/write - * operations - * @param encryptionKey - * encryption key for encrypted communication - */ - public Ethm1Module(String host, int port, int timeout, String encryptionKey) { - super(timeout); - - this.host = host; - this.port = port; - this.encryptionKey = encryptionKey; - } - - @Override - protected CommunicationChannel connect() { - logger.info("Connecting to ETHM-1 module at {}:{}", this.host, this.port); - - try { - Socket socket = new Socket(); - socket.connect(new InetSocketAddress(this.host, this.port), this.getTimeout()); - logger.info("ETHM-1 module connected successfuly"); - - if (StringUtils.isBlank(this.encryptionKey)) { - return new TCPCommunicationChannel(socket); - } else { - return new EncryptedCommunicationChannel(socket, this.encryptionKey); - } - } catch (IOException e) { - logger.error("IO error occurred during connecting socket", e); - } - - return null; - } - - private class TCPCommunicationChannel implements CommunicationChannel { - - private Socket socket; - - public TCPCommunicationChannel(Socket socket) { - this.socket = socket; - } - - @Override - public InputStream getInputStream() throws IOException { - return this.socket.getInputStream(); - } - - @Override - public OutputStream getOutputStream() throws IOException { - return this.socket.getOutputStream(); - } - - @Override - public void disconnect() { - logger.info("Closing connection to ETHM-1 module"); - try { - this.socket.close(); - } catch (IOException e) { - logger.error("IO error occurred during closing socket", e); - } - } - } - - private class EncryptedCommunicationChannel extends TCPCommunicationChannel { - - private EncryptionHelper aesHelper; - private Random rand; - private byte id_s; - private byte id_r; - private int rollingCounter; - private InputStream inputStream; - private OutputStream outputStream; - - public EncryptedCommunicationChannel(final Socket socket, String encryptionKey) throws IOException { - super(socket); - - try { - this.aesHelper = new EncryptionHelper(encryptionKey); - } catch (Exception e) { - throw new IOException("General encryption failure", e); - } - this.rand = new Random(); - this.id_s = 0; - this.id_r = 0; - this.rollingCounter = 0; - - this.inputStream = new InputStream() { - private ByteArrayInputStream inputBuffer = null; - - @Override - public int read() throws IOException { - if (inputBuffer == null || inputBuffer.available() == 0) { - // read message and decrypt it - byte[] data = readMessage(socket.getInputStream()); - // create new buffer - inputBuffer = new ByteArrayInputStream(data, 6, data.length - 6); - - } - return inputBuffer.read(); - } - }; - - this.outputStream = new OutputStream() { - private ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(256); - - @Override - public void write(int b) throws IOException { - outputBuffer.write(b); - } - - @Override - public void flush() throws IOException { - writeMessage(outputBuffer.toByteArray(), socket.getOutputStream()); - outputBuffer.reset(); - } - }; - } - - @Override - public InputStream getInputStream() throws IOException { - return this.inputStream; - } - - @Override - public OutputStream getOutputStream() throws IOException { - return this.outputStream; - } - - private synchronized byte[] readMessage(InputStream is) throws IOException { - logger.trace("Receiving data from ETHM-1"); - // read number of bytes - int bytesCount = is.read(); - logger.trace("Read count of bytes: {}", bytesCount); - if (bytesCount == -1) { - throw new IOException("End of input stream reached"); - } - byte[] data = new byte[bytesCount]; - // read encrypted data - int bytesRead = is.read(data); - if (bytesCount != bytesRead) { - throw new IOException( - String.format("Too few bytes read. Read: %d, expected: %d", bytesRead, bytesCount)); - } - // decrypt data - logger.trace("Decrypting data: {}", bytesToHex(data)); - try { - this.aesHelper.decrypt(data); - } catch (Exception e) { - throw new IOException("Decryption exception", e); - } - logger.debug("Decrypted data: {}", bytesToHex(data)); - // validate message - this.id_r = data[4]; - if (this.id_s != data[5]) { - throw new IOException(String.format("Invalid 'id_s' value. Got: %d, expected: %d", data[5], this.id_s)); - } - - return data; - } - - private synchronized void writeMessage(byte[] message, OutputStream os) throws IOException { - // prepare data for encryption - int bytesCount = 6 + message.length; - if (bytesCount < 16) { - bytesCount = 16; - } - byte[] data = new byte[bytesCount]; - int randomValue = this.rand.nextInt(); - data[0] = (byte) (randomValue >> 8); - data[1] = (byte) (randomValue & 0xff); - data[2] = (byte) (this.rollingCounter >> 8); - data[3] = (byte) (this.rollingCounter & 0xff); - data[4] = this.id_s = (byte) this.rand.nextInt(); - data[5] = this.id_r; - ++this.rollingCounter; - System.arraycopy(message, 0, data, 6, message.length); - - // encrypt data - logger.debug("Encrypting data: {}", bytesToHex(data)); - try { - this.aesHelper.encrypt(data); - } catch (Exception e) { - throw new IOException("Encryption exception", e); - } - logger.trace("Encrypted data: {}", bytesToHex(data)); - - // write encrypted data to output stream - os.write(bytesCount); - os.write(data); - os.flush(); - } - } - - private static String bytesToHex(byte[] bytes) { - StringBuilder result = new StringBuilder(); - for (int i = 0; i < bytes.length; ++i) { - if (i > 0) - result.append(" "); - result.append(String.format("%02X", bytes[i])); - } - return result.toString(); - } + private static final Logger logger = LoggerFactory.getLogger(Ethm1Module.class); + + private String host; + private int port; + private String encryptionKey; + + /** + * Creates new instance with host, port, timeout and encryption key set to + * specified values. + * + * @param host + * host name or IP of ETHM-1 module + * @param port + * TCP port the module listens on + * @param timeout + * timeout value in milliseconds for connect/read/write + * operations + * @param encryptionKey + * encryption key for encrypted communication + */ + public Ethm1Module(String host, int port, int timeout, String encryptionKey) { + super(timeout); + + this.host = host; + this.port = port; + this.encryptionKey = encryptionKey; + } + + @Override + protected CommunicationChannel connect() { + logger.info("Connecting to ETHM-1 module at {}:{}", this.host, this.port); + + try { + Socket socket = new Socket(); + socket.connect(new InetSocketAddress(this.host, this.port), this.getTimeout()); + logger.info("ETHM-1 module connected successfully"); + + if (StringUtils.isBlank(this.encryptionKey)) { + return new TCPCommunicationChannel(socket); + } else { + return new EncryptedCommunicationChannel(socket, this.encryptionKey); + } + } catch (IOException e) { + logger.error("IO error occurred during connecting socket", e); + } + + return null; + } + + private class TCPCommunicationChannel implements CommunicationChannel { + + private Socket socket; + + public TCPCommunicationChannel(Socket socket) { + this.socket = socket; + } + + @Override + public InputStream getInputStream() throws IOException { + return this.socket.getInputStream(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return this.socket.getOutputStream(); + } + + @Override + public void disconnect() { + logger.info("Closing connection to ETHM-1 module"); + try { + this.socket.close(); + } catch (IOException e) { + logger.error("IO error occurred during closing socket", e); + } + } + } + + private class EncryptedCommunicationChannel extends TCPCommunicationChannel { + + private EncryptionHelper aesHelper; + private Random rand; + private byte id_s; + private byte id_r; + private int rollingCounter; + private InputStream inputStream; + private OutputStream outputStream; + + public EncryptedCommunicationChannel(final Socket socket, String encryptionKey) throws IOException { + super(socket); + + try { + this.aesHelper = new EncryptionHelper(encryptionKey); + } catch (Exception e) { + throw new IOException("General encryption failure", e); + } + this.rand = new Random(); + this.id_s = 0; + this.id_r = 0; + this.rollingCounter = 0; + + this.inputStream = new InputStream() { + private ByteArrayInputStream inputBuffer = null; + + @Override + public int read() throws IOException { + if (inputBuffer == null || inputBuffer.available() == 0) { + // read message and decrypt it + byte[] data = readMessage(socket.getInputStream()); + // create new buffer + inputBuffer = new ByteArrayInputStream(data, 6, data.length - 6); + + } + return inputBuffer.read(); + } + }; + + this.outputStream = new OutputStream() { + private ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(256); + + @Override + public void write(int b) throws IOException { + outputBuffer.write(b); + } + + @Override + public void flush() throws IOException { + writeMessage(outputBuffer.toByteArray(), socket.getOutputStream()); + outputBuffer.reset(); + } + }; + } + + @Override + public InputStream getInputStream() throws IOException { + return this.inputStream; + } + + @Override + public OutputStream getOutputStream() throws IOException { + return this.outputStream; + } + + private synchronized byte[] readMessage(InputStream is) throws IOException { + logger.trace("Receiving data from ETHM-1"); + // read number of bytes + int bytesCount = is.read(); + logger.trace("Read count of bytes: {}", bytesCount); + if (bytesCount == -1) { + throw new IOException("End of input stream reached"); + } + byte[] data = new byte[bytesCount]; + // read encrypted data + int bytesRead = is.read(data); + if (bytesCount != bytesRead) { + throw new IOException( + String.format("Too few bytes read. Read: %d, expected: %d", bytesRead, bytesCount)); + } + // decrypt data + logger.trace("Decrypting data: {}", bytesToHex(data)); + try { + this.aesHelper.decrypt(data); + } catch (Exception e) { + throw new IOException("Decryption exception", e); + } + logger.debug("Decrypted data: {}", bytesToHex(data)); + // validate message + this.id_r = data[4]; + if (this.id_s != data[5]) { + throw new IOException(String.format("Invalid 'id_s' value. Got: %d, expected: %d", data[5], this.id_s)); + } + + return data; + } + + private synchronized void writeMessage(byte[] message, OutputStream os) throws IOException { + // prepare data for encryption + int bytesCount = 6 + message.length; + if (bytesCount < 16) { + bytesCount = 16; + } + byte[] data = new byte[bytesCount]; + int randomValue = this.rand.nextInt(); + data[0] = (byte) (randomValue >> 8); + data[1] = (byte) (randomValue & 0xff); + data[2] = (byte) (this.rollingCounter >> 8); + data[3] = (byte) (this.rollingCounter & 0xff); + data[4] = this.id_s = (byte) this.rand.nextInt(); + data[5] = this.id_r; + ++this.rollingCounter; + System.arraycopy(message, 0, data, 6, message.length); + + // encrypt data + logger.debug("Encrypting data: {}", bytesToHex(data)); + try { + this.aesHelper.encrypt(data); + } catch (Exception e) { + throw new IOException("Encryption exception", e); + } + logger.trace("Encrypted data: {}", bytesToHex(data)); + + // write encrypted data to output stream + os.write(bytesCount); + os.write(data); + os.flush(); + } + } + + private static String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < bytes.length; ++i) { + if (i > 0) { + result.append(" "); + } + result.append(String.format("%02X", bytes[i])); + } + return result.toString(); + } } diff --git a/bundles/binding/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpBinding.java b/bundles/binding/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpBinding.java index 7b7df5d6863..ffdefac1ff5 100644 --- a/bundles/binding/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpBinding.java +++ b/bundles/binding/org.openhab.binding.snmp/src/main/java/org/openhab/binding/snmp/internal/SnmpBinding.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2015, openHAB.org and others. + * Copyright (c) 2010-2016, openHAB.org and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -37,7 +37,6 @@ import org.snmp4j.Snmp; import org.snmp4j.event.ResponseEvent; import org.snmp4j.event.ResponseListener; -import org.snmp4j.mp.SnmpConstants; import org.snmp4j.security.Priv3DES; import org.snmp4j.security.SecurityProtocols; import org.snmp4j.smi.Address; @@ -53,393 +52,398 @@ /** * The SNMP binding listens to SNMP Traps on the configured port and posts new * events of type ({@link StringType} to the event bus. - * + * * @author Thomas.Eichstaedt-Engelen * @author Chris Jackson - modified binding to support polling SNMP OIDs (SNMP GET) and setting values (SNMP SET). * @author Jan N. Klug - modified binding to change protocol version * @since 0.9.0 */ public class SnmpBinding extends AbstractActiveBinding - implements ManagedService, CommandResponder, ResponseListener { - - private Snmp snmp; - private static final Logger logger = - LoggerFactory.getLogger(SnmpBinding.class); - - private static DefaultUdpTransportMapping transport; - - private static final int SNMP_DEFAULT_PORT = 162; - /** The local port to bind on and listen to SNMP Traps */ - private static int port = SNMP_DEFAULT_PORT; - - /** The SNMP community to filter SNMP Traps */ - private static String community; - - private static int timeout = 1500; - private static int retries = 0; - - /** - * the interval to find new refresh candidates (defaults to 1000 - * milliseconds) - */ - private int granularity = 1000; - - private Map lastUpdateMap = new HashMap(); - - - public void activate() { - logger.debug("SNMP binding activated"); - super.activate(); - } - - public void deactivate() { - stopListening(); - logger.debug("SNMP binding deactivated"); - } - - /** - * @{inheritDoc - */ - @Override - protected long getRefreshInterval() { - return granularity; - } - - /** - * @{inheritDoc - */ - @Override - protected String getName() { - return "SNMP Refresh Service"; - } - - /** - * Configures a {@link DefaultUdpTransportMapping} and starts listening on - * SnmpBinding.port for incoming SNMP Traps. - */ - private void listen() { - UdpAddress address = new UdpAddress(SnmpBinding.port); - try { - if (transport != null) { - transport.close(); - transport = null; - } - if (snmp != null) { - snmp.close(); - snmp = null; - } - - transport = new DefaultUdpTransportMapping(address); - - // add all security protocols - SecurityProtocols.getInstance().addDefaultProtocols(); - SecurityProtocols.getInstance().addPrivacyProtocol(new Priv3DES()); - - // Create Target - if (SnmpBinding.community != null) { - CommunityTarget target = new CommunityTarget(); - target.setCommunity(new OctetString(SnmpBinding.community)); - } - - snmp = new Snmp(transport); - - transport.listen(); - logger.debug("SNMP binding is listening on " + address); - } catch (IOException ioe) { - logger.error("SNMP binding couldn't listen to " + address, ioe); - } - } - - /** - * Stops listening for incoming SNMP Traps - */ - private void stopListening() { - if (transport != null) { - try { - transport.close(); - } catch (IOException ioe) { - logger.error("couldn't close connection", ioe); - } - transport = null; - } - - if (snmp != null) { - try { - snmp.close(); - } catch (IOException ioe) { - logger.error("couldn't close snmp", ioe); - } - snmp = null; - } - } - - /** - * Will be called whenever a {@link PDU} is received on the given port - * specified in the listen() method. It extracts a {@link Variable} - * according to the configured OID prefix and sends its value to the event - * bus. - */ - public void processPdu(CommandResponderEvent event) { - Address addr = event.getPeerAddress(); - if (addr == null) { - return; - } - - String s = addr.toString().split("/")[0]; - if (s == null) { - logger.error("TRAP: failed to translate address {}", addr); - dispatchPdu(addr, event.getPDU()); - } else { - // Need to change the port to 161, which is what the bindings are configured for since - // at least some SNMP devices send traps from a random port number. Otherwise the trap - // won't be found as the address check will fail. It feels like there should be a better - // way to do this!!! - Address address = GenericAddress.parse("udp:" + s + "/161"); - dispatchPdu(address, event.getPDU()); - } - } - - /** - * Called when a response from a GET is received - * @see org.snmp4j.event.ResponseListener#onResponse(org.snmp4j.event.ResponseEvent ) - */ - @Override - public void onResponse(ResponseEvent event) { - dispatchPdu(event.getPeerAddress(), event.getResponse()); - } - - private void dispatchPdu(Address address, PDU pdu) { - if (pdu != null & address != null) { - logger.debug("Received PDU from '{}' '{}'", address, pdu); - for (SnmpBindingProvider provider : providers) { - for (String itemName : provider.getItemNames()) { - // Check the IP address - if (!provider.getAddress(itemName).equals(address)) { - continue; - } - - // Check the OID - OID oid = provider.getOID(itemName); - Variable variable = pdu.getVariable(oid); - if (variable != null) { - Class itemType = provider.getItemType(itemName); - - // Do any transformations - String value = variable.toString(); - try { - value = provider.doTransformation(itemName, value); - } catch (TransformationException e) { - logger.error("Transformation error with item {}: {}", itemName, e); - } - - // Change to a state - State state = null; - if (itemType.isAssignableFrom(StringItem.class)) { - state = StringType.valueOf(value); - } else if (itemType.isAssignableFrom(NumberItem.class)) { - state = DecimalType.valueOf(value); - } else if (itemType.isAssignableFrom(SwitchItem.class)) { - state = OnOffType.valueOf(value); - } - - if (state != null) { - eventPublisher.postUpdate(itemName, state); - } else { - logger.debug( - "'{}' couldn't be parsed to a State. Valid State-Types are String and Number", - variable.toString()); - } - } else { - logger.trace("PDU doesn't contain a variable with OID ‘{}‘", oid.toString()); - } - } - } - } - } - - /** - * @{inheritDoc - */ - @Override - public void internalReceiveCommand(String itemName, Command command) { - logger.debug("SNMP receive command {} from {}", itemName, command); - - SnmpBindingProvider providerCmd = null; - - for (SnmpBindingProvider provider : this.providers) { - OID oid = provider.getOID(itemName, command); - if (oid != null) { - providerCmd = provider; - break; - } - } - - if (providerCmd == null) { - logger.warn("No match for binding provider [itemName={}, command={}]", itemName, command); - return; - } - - logger.debug("SNMP command for {} to {}", itemName, providerCmd.toString()); - - // Set up the target - CommunityTarget target = new CommunityTarget(); - target.setCommunity(providerCmd.getCommunity(itemName, command)); - target.setAddress(providerCmd.getAddress(itemName, command)); - target.setRetries(retries); - target.setTimeout(timeout); - target.setVersion(providerCmd.getSnmpVersion(itemName, command)); - - Variable var = providerCmd.getValue(itemName, command); - OID oid = providerCmd.getOID(itemName, command); - VariableBinding varBind = new VariableBinding(oid,var); - - // Create the PDU - PDU pdu = new PDU(); - pdu.add(varBind); - pdu.setType(PDU.SET); - pdu.setRequestID(new Integer32(1)); - - logger.debug("SNMP: Send CMD PDU {} {}", providerCmd.getAddress(itemName, command), pdu); - - if (snmp == null) { - logger.error("SNMP: snmp not initialised - aborting request"); - } - else { - sendPDU(target, pdu); - } - } - - /** - * @{inheritDoc - */ - @Override - public void execute() { - for (SnmpBindingProvider provider : providers) { - for (String itemName : provider.getInBindingItemNames()) { - int refreshInterval = provider.getRefreshInterval(itemName); - - Long lastUpdateTimeStamp = lastUpdateMap.get(itemName); - if (lastUpdateTimeStamp == null) { - lastUpdateTimeStamp = 0L; - } - - long age = System.currentTimeMillis() - lastUpdateTimeStamp; - boolean needsUpdate; - if (refreshInterval == 0) { - needsUpdate = false; - } else { - needsUpdate = age >= refreshInterval; - } - - if (needsUpdate) { - logger.debug("Item '{}' is about to be refreshed", itemName); - - // Set up the target - CommunityTarget target = new CommunityTarget(); - target.setCommunity(provider.getCommunity(itemName)); - target.setAddress(provider.getAddress(itemName)); - target.setRetries(retries); - target.setTimeout(timeout); - target.setVersion(provider.getSnmpVersion(itemName)); - - // Create the PDU - PDU pdu = new PDU(); - pdu.add(new VariableBinding(provider.getOID(itemName))); - pdu.setType(PDU.GET); - - logger.debug("SNMP: Send PDU {} {}", provider.getAddress(itemName), pdu); - - if (snmp == null) { - logger.error("SNMP: snmp not initialised - aborting request"); - } else { - sendPDU(target, pdu); - } - - lastUpdateMap.put(itemName, System.currentTimeMillis()); - } - } - } - - } - - /** - * {@inheritDoc} - */ - public void updated(Dictionary config) throws ConfigurationException { - boolean mapping = false; - stopListening(); - - if (config != null) { - mapping = true; - - SnmpBinding.community = (String) config.get("community"); - if (StringUtils.isBlank(SnmpBinding.community)) { - SnmpBinding.community = "public"; - logger.info( - "didn't find SNMP community configuration -> listen to SNMP community {}", - SnmpBinding.community); - } - - String portString = (String) config.get("port"); - if (StringUtils.isNotBlank(portString) && portString.matches("\\d*")) { - SnmpBinding.port = Integer.valueOf(portString).intValue(); - } else { - SnmpBinding.port = SNMP_DEFAULT_PORT; - logger.info( - "Didn't find SNMP port configuration or configuration is invalid -> listen to SNMP default port {}", - SnmpBinding.port); - } - - String timeoutString = (String) config.get("timeout"); - if (StringUtils.isNotBlank(timeoutString)) { - SnmpBinding.timeout = Integer.valueOf(timeoutString).intValue(); - if (SnmpBinding.timeout < 0 | SnmpBinding.retries > 5) { - logger.info("SNMP timeout value is invalid (" + SnmpBinding.timeout + "). Using default value."); - SnmpBinding.timeout = 1500; - } - } else { - SnmpBinding.timeout = 1500; - logger.info( - "Didn't find SNMP timeout or configuration is invalid -> timeout set to {}", - SnmpBinding.timeout); - } - - String retriesString = (String) config.get("retries"); - if (StringUtils.isNotBlank(retriesString)) { - SnmpBinding.retries = Integer.valueOf(retriesString).intValue(); - if (SnmpBinding.retries < 0 | SnmpBinding.retries > 5) { - logger.info("SNMP retries value is invalid (" - + SnmpBinding.retries + "). Using default value."); - SnmpBinding.retries = 0; - } - } else { - SnmpBinding.retries = 0; - logger.info( - "Didn't find SNMP retries or configuration is invalid -> retries set to {}", - SnmpBinding.retries); - } - - } - - for (SnmpBindingProvider provider : providers) { - if (provider.getInBindingItemNames() != null) { - mapping = true; - } - } - - // Did we find either a trap request, or any bindings - if (mapping) { - listen(); - } - setProperlyConfigured(true); - } - - private void sendPDU(CommunityTarget target, PDU pdu) { - try { - snmp.send(pdu, target, null, this); - } catch (IOException e) { - logger.error("Error sending PDU", e); - } - } - + implements ManagedService, CommandResponder, ResponseListener { + + private Snmp snmp; + private static final Logger logger = LoggerFactory.getLogger(SnmpBinding.class); + + private static DefaultUdpTransportMapping transport; + + private static final int SNMP_DEFAULT_PORT = 162; + /** The local port to bind on and listen to SNMP Traps */ + private static int port = SNMP_DEFAULT_PORT; + + /** The SNMP community to filter SNMP Traps */ + private static String community; + + private static int timeout = 1500; + private static int retries = 0; + + /** + * the interval to find new refresh candidates (defaults to 1000 + * milliseconds) + */ + private int granularity = 1000; + + private Map lastUpdateMap = new HashMap(); + + @Override + public void activate() { + logger.debug("SNMP binding activated"); + super.activate(); + } + + @Override + public void deactivate() { + stopListening(); + logger.debug("SNMP binding deactivated"); + } + + /** + * @{inheritDoc + */ + @Override + protected long getRefreshInterval() { + return granularity; + } + + /** + * @{inheritDoc + */ + @Override + protected String getName() { + return "SNMP Refresh Service"; + } + + /** + * Configures a {@link DefaultUdpTransportMapping} and starts listening on + * SnmpBinding.port for incoming SNMP Traps. + */ + private void listen() { + UdpAddress address = new UdpAddress(SnmpBinding.port); + try { + if (transport != null) { + transport.close(); + transport = null; + } + if (snmp != null) { + snmp.close(); + snmp = null; + } + + transport = new DefaultUdpTransportMapping(address); + + // add all security protocols + SecurityProtocols.getInstance().addDefaultProtocols(); + SecurityProtocols.getInstance().addPrivacyProtocol(new Priv3DES()); + + // Create Target + if (SnmpBinding.community != null) { + CommunityTarget target = new CommunityTarget(); + target.setCommunity(new OctetString(SnmpBinding.community)); + } + + snmp = new Snmp(transport); + + transport.listen(); + logger.debug("SNMP binding is listening on " + address); + } catch (IOException ioe) { + logger.error("SNMP binding couldn't listen to " + address, ioe); + } + } + + /** + * Stops listening for incoming SNMP Traps + */ + private void stopListening() { + if (transport != null) { + try { + transport.close(); + } catch (IOException ioe) { + logger.error("couldn't close connection", ioe); + } + transport = null; + } + + if (snmp != null) { + try { + snmp.close(); + } catch (IOException ioe) { + logger.error("couldn't close snmp", ioe); + } + snmp = null; + } + } + + /** + * Will be called whenever a {@link PDU} is received on the given port + * specified in the listen() method. It extracts a {@link Variable} + * according to the configured OID prefix and sends its value to the event + * bus. + */ + @Override + public void processPdu(CommandResponderEvent event) { + Address addr = event.getPeerAddress(); + if (addr == null) { + return; + } + + String s = addr.toString().split("/")[0]; + if (s == null) { + logger.error("TRAP: failed to translate address {}", addr); + dispatchPdu(addr, event.getPDU()); + } else { + // Need to change the port to 161, which is what the bindings are configured for since + // at least some SNMP devices send traps from a random port number. Otherwise the trap + // won't be found as the address check will fail. It feels like there should be a better + // way to do this!!! + Address address = GenericAddress.parse("udp:" + s + "/161"); + dispatchPdu(address, event.getPDU()); + } + } + + /** + * Called when a response from a GET is received + * + * @see org.snmp4j.event.ResponseListener#onResponse(org.snmp4j.event.ResponseEvent ) + */ + @Override + public void onResponse(ResponseEvent event) { + dispatchPdu(event.getPeerAddress(), event.getResponse()); + } + + private void dispatchPdu(Address address, PDU pdu) { + if (pdu != null & address != null) { + logger.debug("Received PDU from '{}' '{}'", address, pdu); + for (SnmpBindingProvider provider : providers) { + for (String itemName : provider.getItemNames()) { + // Check the IP address + if (!provider.getAddress(itemName).equals(address)) { + continue; + } + + // Check the OID + OID oid = provider.getOID(itemName); + Variable variable = pdu.getVariable(oid); + if (variable != null) { + Class itemType = provider.getItemType(itemName); + + // Do any transformations + String value = variable.toString(); + try { + value = provider.doTransformation(itemName, value); + } catch (TransformationException e) { + logger.error("Transformation error with item {}: {}", itemName, e); + } + + // Change to a state + State state = null; + if (itemType.isAssignableFrom(StringItem.class)) { + state = StringType.valueOf(value); + } else if (itemType.isAssignableFrom(NumberItem.class)) { + state = DecimalType.valueOf(value); + } else if (itemType.isAssignableFrom(SwitchItem.class)) { + state = OnOffType.valueOf(value); + } + + if (state != null) { + eventPublisher.postUpdate(itemName, state); + } else { + logger.debug("'{}' couldn't be parsed to a State. Valid State-Types are String and Number", + variable.toString()); + } + } else { + logger.trace("PDU doesn't contain a variable with OID '{}'", oid.toString()); + } + } + } + } + } + + /** + * @{inheritDoc + */ + @Override + public void internalReceiveCommand(String itemName, Command command) { + logger.debug("SNMP receive command {} from {}", itemName, command); + + SnmpBindingProvider providerCmd = null; + + for (SnmpBindingProvider provider : this.providers) { + OID oid = provider.getOID(itemName, command); + if (oid != null) { + providerCmd = provider; + break; + } + } + + if (providerCmd == null) { + logger.warn("No match for binding provider [itemName={}, command={}]", itemName, command); + return; + } + + logger.debug("SNMP command for {} to {}", itemName, providerCmd.toString()); + + // Set up the target + CommunityTarget target = new CommunityTarget(); + target.setCommunity(providerCmd.getCommunity(itemName, command)); + target.setAddress(providerCmd.getAddress(itemName, command)); + target.setRetries(retries); + target.setTimeout(timeout); + target.setVersion(providerCmd.getSnmpVersion(itemName, command)); + + Variable var = providerCmd.getValue(itemName, command); + OID oid = providerCmd.getOID(itemName, command); + VariableBinding varBind = new VariableBinding(oid, var); + + // Create the PDU + PDU pdu = new PDU(); + pdu.add(varBind); + pdu.setType(PDU.SET); + pdu.setRequestID(new Integer32(1)); + + logger.debug("SNMP: Send CMD PDU {} {}", providerCmd.getAddress(itemName, command), pdu); + + if (snmp == null) { + logger.error("SNMP: snmp not initialised - aborting request"); + } else { + sendPDU(target, pdu); + } + } + + /** + * @{inheritDoc + */ + @Override + public void execute() { + for (SnmpBindingProvider provider : providers) { + for (String itemName : provider.getInBindingItemNames()) { + int refreshInterval = provider.getRefreshInterval(itemName); + + Long lastUpdateTimeStamp = lastUpdateMap.get(itemName); + if (lastUpdateTimeStamp == null) { + lastUpdateTimeStamp = 0L; + } + + long age = System.currentTimeMillis() - lastUpdateTimeStamp; + boolean needsUpdate; + if (refreshInterval == 0) { + needsUpdate = false; + } else { + needsUpdate = age >= refreshInterval; + } + + if (needsUpdate) { + logger.debug("Item '{}' is about to be refreshed", itemName); + + // Set up the target + CommunityTarget target = new CommunityTarget(); + target.setCommunity(provider.getCommunity(itemName)); + target.setAddress(provider.getAddress(itemName)); + target.setRetries(retries); + target.setTimeout(timeout); + target.setVersion(provider.getSnmpVersion(itemName)); + + // Create the PDU + PDU pdu = new PDU(); + pdu.add(new VariableBinding(provider.getOID(itemName))); + pdu.setType(PDU.GET); + + logger.debug("SNMP: Send PDU {} {}", provider.getAddress(itemName), pdu); + + if (snmp == null) { + logger.error("SNMP: snmp not initialised - aborting request"); + } else { + sendPDU(target, pdu); + } + + lastUpdateMap.put(itemName, System.currentTimeMillis()); + } + } + } + + } + + protected void addBindingProvider(SnmpBindingProvider bindingProvider) { + super.addBindingProvider(bindingProvider); + } + + protected void removeBindingProvider(SnmpBindingProvider bindingProvider) { + super.removeBindingProvider(bindingProvider); + } + + /** + * {@inheritDoc} + */ + @Override + public void updated(Dictionary config) throws ConfigurationException { + boolean mapping = false; + stopListening(); + + if (config != null) { + mapping = true; + + SnmpBinding.community = (String) config.get("community"); + if (StringUtils.isBlank(SnmpBinding.community)) { + SnmpBinding.community = "public"; + logger.info("didn't find SNMP community configuration -> listen to SNMP community {}", + SnmpBinding.community); + } + + String portString = (String) config.get("port"); + if (StringUtils.isNotBlank(portString) && portString.matches("\\d*")) { + SnmpBinding.port = Integer.valueOf(portString).intValue(); + } else { + SnmpBinding.port = SNMP_DEFAULT_PORT; + logger.info( + "Didn't find SNMP port configuration or configuration is invalid -> listen to SNMP default port {}", + SnmpBinding.port); + } + + String timeoutString = (String) config.get("timeout"); + if (StringUtils.isNotBlank(timeoutString)) { + SnmpBinding.timeout = Integer.valueOf(timeoutString).intValue(); + if (SnmpBinding.timeout < 0 | SnmpBinding.retries > 5) { + logger.info("SNMP timeout value is invalid (" + SnmpBinding.timeout + "). Using default value."); + SnmpBinding.timeout = 1500; + } + } else { + SnmpBinding.timeout = 1500; + logger.info("Didn't find SNMP timeout or configuration is invalid -> timeout set to {}", + SnmpBinding.timeout); + } + + String retriesString = (String) config.get("retries"); + if (StringUtils.isNotBlank(retriesString)) { + SnmpBinding.retries = Integer.valueOf(retriesString).intValue(); + if (SnmpBinding.retries < 0 | SnmpBinding.retries > 5) { + logger.info("SNMP retries value is invalid (" + SnmpBinding.retries + "). Using default value."); + SnmpBinding.retries = 0; + } + } else { + SnmpBinding.retries = 0; + logger.info("Didn't find SNMP retries or configuration is invalid -> retries set to {}", + SnmpBinding.retries); + } + + } + + for (SnmpBindingProvider provider : providers) { + if (provider.getInBindingItemNames() != null) { + mapping = true; + } + } + + // Did we find either a trap request, or any bindings + if (mapping) { + listen(); + } + setProperlyConfigured(true); + } + + private void sendPDU(CommunityTarget target, PDU pdu) { + try { + snmp.send(pdu, target, null, this); + } catch (IOException e) { + logger.error("Error sending PDU", e); + } + } + } diff --git a/bundles/binding/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosXMLParser.java b/bundles/binding/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosXMLParser.java index 2181c3210a0..28f868fd8f3 100644 --- a/bundles/binding/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosXMLParser.java +++ b/bundles/binding/org.openhab.binding.sonos/src/main/java/org/openhab/binding/sonos/internal/SonosXMLParser.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2015, openHAB.org and others. + * Copyright (c) 2010-2016, openHAB.org and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -20,8 +20,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - * - * + * + * */ package org.openhab.binding.sonos.internal; @@ -33,7 +33,6 @@ import java.util.List; import java.util.Map; -import org.apache.commons.lang.StringUtils; import org.joda.time.DateTime; import org.joda.time.LocalTime; import org.joda.time.Period; @@ -55,803 +54,823 @@ import org.xml.sax.helpers.XMLReaderFactory; /** - * XMLParser is a set of helper classes and methods to parse XML string that are returned by Sonos players in the network, or that + * XMLParser is a set of helper classes and methods to parse XML string that are returned by Sonos players in the + * network, or that * are used to parse string returned after a call to a specific UPNP variable request - * - * @author Karel Goderis + * + * @author Karel Goderis * @since 1.1.0 - * + * */ public class SonosXMLParser { - - static final Logger logger = LoggerFactory - .getLogger(SonosXMLParser.class); - - private static MessageFormat METADATA_FORMAT = new MessageFormat( - "" + - "" + - "{2}" + - "{3}" + - "" + - "{4}" + - ""); - - private enum Element { - TITLE, - CLASS, - ALBUM, - ALBUM_ART_URI, - CREATOR, - RES, - TRACK_NUMBER, - RESMD, - DESC - } - - private enum CurrentElement { - item, - res, - streamContent, - albumArtURI, - title, - upnpClass, - creator, - album, - albumArtist, - desc; - } - - /** - * @param xml - * @return a list of alarms from the given xml string. - * @throws IOException - * @throws SAXException - */ - public static List getAlarmsFromStringResult(String xml) throws SAXException { - XMLReader reader = XMLReaderFactory.createXMLReader(); - AlarmHandler handler = new AlarmHandler(); - reader.setContentHandler(handler); - try { - reader.parse(new InputSource(new StringReader(xml))); - } catch (IOException e) { - logger.error("Could not parse Alarms from String {}",xml); - } - return handler.getAlarms(); - } - - - /** - * @param xml - * @return a list of Entrys from the given xml string. - * @throws IOException - * @throws SAXException - */ - public static List getEntriesFromString(String xml) throws SAXException { - XMLReader reader = XMLReaderFactory.createXMLReader(); - EntryHandler handler = new EntryHandler(); - reader.setContentHandler(handler); - try { - reader.parse(new InputSource(new StringReader(xml))); - } catch (IOException e) { - logger.error("Could not parse Entries from String {}",xml); - } - return handler.getArtists(); - } - - /** - * Returns the meta data which is needed to play Pandora - * (and others?) favorites - * @param xml - * @return The value of the desc xml tag - * @throws SAXException - */ - public static SonosResourceMetaData getEmbededMetaDataFromResource(String xml) throws SAXException { - XMLReader reader = XMLReaderFactory.createXMLReader(); - EmbededMetaDataHandler handler = new EmbededMetaDataHandler(); - reader.setContentHandler(handler); - try { - reader.parse(new InputSource(new StringReader(xml))); - } catch (IOException e) { - logger.error("Could not parse Entries from String {}",xml); - } - return handler.getMetaData(); - } - - /** - * @param controller - * @param xml - * @return zone group from the given xml - * @throws IOException - * @throws SAXException - */ - public static List getZoneGroupFromXML(String xml) throws SAXException { - XMLReader reader = XMLReaderFactory.createXMLReader(); - ZoneGroupHandler handler = new ZoneGroupHandler(); - reader.setContentHandler(handler); - try { - reader.parse(new InputSource(new StringReader(xml))); - } catch (IOException e) { - // This should never happen - we're not performing I/O! - logger.error("Could not parse ZoneGroup from String {}",xml); - } - - return handler.getGroups(); - - } - - public static List getRadioTimeFromXML(String xml) throws SAXException { - XMLReader reader = XMLReaderFactory.createXMLReader(); - OpmlHandler handler = new OpmlHandler(); - reader.setContentHandler(handler); - try { - reader.parse(new InputSource(new StringReader(xml))); - } catch (IOException e) { - // This should never happen - we're not performing I/O! - logger.error("Could not parse RadioTime from String {}",xml); - } - - return handler.getTextFields(); - - } - - public static Map getRenderingControlFromXML(String xml) throws SAXException { - XMLReader reader = XMLReaderFactory.createXMLReader(); - RenderingControlEventHandler handler = new RenderingControlEventHandler(); - reader.setContentHandler(handler); - try { - reader.parse(new InputSource(new StringReader(xml))); - } catch (IOException e) { - // This should never happen - we're not performing I/O! - logger.debug("Could not parse Rendering Control event: {}", e); - } - return handler.getChanges(); - } - - public static Map getAVTransportFromXML(String xml) throws SAXException { - XMLReader reader = XMLReaderFactory.createXMLReader(); - AVTransportEventHandler handler = new AVTransportEventHandler(); - reader.setContentHandler(handler); - try { - reader.parse(new InputSource(new StringReader(xml))); - } catch (IOException e) { - // This should never happen - we're not performing I/O! - logger.error("Could not parse AV Transport Event: {}", e); - } - return handler.getChanges(); - } - - public static SonosMetaData getMetaDataFromXML(String xml) throws SAXException { - XMLReader reader = XMLReaderFactory.createXMLReader(); - //logger.debug("getTrackFromXML {}",xml); - MetaDataHandler handler = new MetaDataHandler(); - reader.setContentHandler(handler); - try { - reader.parse(new InputSource(new StringReader(xml))); - } catch (IOException e) { - // This should never happen - we're not performing I/O! - logger.error("Could not parse AV Transport Event: {}", e); - } - return handler.getMetaData(); - } - - - static private class EntryHandler extends DefaultHandler { - - // Maintain a set of elements about which it is unuseful to complain about. - // This list will be initialized on the first failure case - private static List ignore = null; - - private String id; - private String parentId; - private StringBuilder upnpClass = new StringBuilder(); - private StringBuilder res = new StringBuilder(); - private StringBuilder title = new StringBuilder(); - private StringBuilder album = new StringBuilder(); - private StringBuilder albumArtUri = new StringBuilder(); - private StringBuilder creator = new StringBuilder(); - private StringBuilder trackNumber = new StringBuilder(); - private StringBuilder desc = new StringBuilder(); - private Element element = null; - - private List artists = new ArrayList(); - - EntryHandler() { - // shouldn't be used outside of this package. - } - - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - if (qName.equals("container") || qName.equals("item")) { - id = attributes.getValue("id"); - parentId = attributes.getValue("parentID"); - } else if (qName.equals("res")) { - element = Element.RES; - } else if (qName.equals("dc:title")) { - element = Element.TITLE; - } else if (qName.equals("upnp:class")) { - element = Element.CLASS; - } else if (qName.equals("dc:creator")) { - element = Element.CREATOR; - } else if (qName.equals("upnp:album")) { - element = Element.ALBUM; - } else if (qName.equals("upnp:albumArtURI")) { - element = Element.ALBUM_ART_URI; - } else if (qName.equals("upnp:originalTrackNumber")) { - element = Element.TRACK_NUMBER; - } else if (qName.equals("r:resMD")) { - element = Element.RESMD; - } else { - if (ignore == null) { - ignore = new ArrayList(); - ignore.add("DIDL-Lite"); - ignore.add("type"); - ignore.add("ordinal"); - ignore.add("description"); - } - - if (!ignore.contains(localName)) { - logger.warn("Did not recognise element named {}",localName); - } - element = null; - } - } - - @Override - public void characters(char[] ch, int start, int length) throws SAXException { - if (element == null) { - return; - } - switch (element) { - case TITLE: - title.append(ch, start, length); - break; - case CLASS: - upnpClass.append(ch, start, length); - break; - case RES: - res.append(ch, start, length); - break; - case ALBUM: - album.append(ch, start, length); - break; - case ALBUM_ART_URI: - albumArtUri.append(ch, start, length); - break; - case CREATOR: - creator.append(ch, start, length); - break; - case TRACK_NUMBER: - trackNumber.append(ch, start, length); - break; - case RESMD: - desc.append(ch, start, length); - break; - // no default - } - } - - @Override - public void endElement(String uri, String localName, String qName) throws SAXException { - if (qName.equals("container") || qName.equals("item")) { - element = null; - - int trackNumberVal = 0; - try { - trackNumberVal = Integer.parseInt(trackNumber.toString()); - } catch (Exception e) { - } - - SonosResourceMetaData md = null; - - //The resource description is needed for playing favorites on pandora - try { - md = getEmbededMetaDataFromResource(desc.toString()); - } catch (SAXException ignore){ - logger.debug("Failed to parse embeded",ignore); - } - - artists.add(new SonosEntry(id, title.toString(), parentId, album.toString(), - albumArtUri.toString(), creator.toString(),upnpClass.toString(), res.toString(),trackNumberVal, md)); - title= new StringBuilder(); - upnpClass = new StringBuilder(); - res = new StringBuilder(); - album = new StringBuilder(); - albumArtUri = new StringBuilder(); - creator = new StringBuilder(); - trackNumber = new StringBuilder(); - desc = new StringBuilder(); - } - } - - public List getArtists() { - return artists; - } - } - - static private class EmbededMetaDataHandler extends DefaultHandler { - - private String id; - private String parentId; - private StringBuilder title = new StringBuilder(); - private StringBuilder upnpClass = new StringBuilder(); - private StringBuilder desc = new StringBuilder(); - private Element element = null; - private SonosResourceMetaData metaData = null; - - EmbededMetaDataHandler() { - // shouldn't be used outside of this package. - } - - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - - if (qName.equals("container") || qName.equals("item")) { - id = attributes.getValue("id"); - parentId = attributes.getValue("parentID"); - } else if (qName.equals("desc")) { - element = Element.DESC; - } else if (qName.equals("upnp:class")) { - element = Element.CLASS; - } else if (qName.equals("dc:title")) { - element = Element.TITLE; - } else { - element = null; - } - } - - @Override - public void characters(char[] ch, int start, int length) throws SAXException { - if (element == null) { - return; - } - switch (element) { - case TITLE: - title.append(ch, start, length); - break; - case CLASS: - upnpClass.append(ch, start, length); - break; - case DESC: - desc.append(ch, start, length);; - break; - default: - break; - } - } - - @Override - public void endElement(String uri, String localName, String qName) throws SAXException { - if (qName.equals("DIDL-Lite") ) { - metaData = new SonosResourceMetaData(id, parentId, title.toString(), upnpClass.toString(),desc.toString()); - element = null; - desc = new StringBuilder(); - upnpClass = new StringBuilder(); - title = new StringBuilder(); - } - } - - public SonosResourceMetaData getMetaData() { - return metaData; - } - } - - static private class AlarmHandler extends DefaultHandler { - - private String id; - private String startTime ; - private String duration; - private String recurrence ; - private String enabled ; - private String roomUUID ; - private String programURI; - private String programMetaData; - private String playMode ; - private String volume; - private String includeLinkedZones ; - - private List alarms = new ArrayList(); - - AlarmHandler() { - // shouldn't be used outside of this package. - } - - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - - if (qName.equals("Alarm")) { - id = attributes.getValue("ID"); - duration = attributes.getValue("Duration"); - recurrence = attributes.getValue("Recurrence"); - startTime = attributes.getValue("StartTime"); - enabled = attributes.getValue("Enabled"); - roomUUID = attributes.getValue("RoomUUID"); - programURI = attributes.getValue("ProgramURI"); - programMetaData = attributes.getValue("ProgramMetaData"); - playMode = attributes.getValue("PlayMode"); - volume = attributes.getValue("Volume"); - includeLinkedZones = attributes.getValue("IncludeLinkedZones"); - } - } - - @Override - public void endElement(String uri, String localName, String qName) throws SAXException { - - if (qName.equals("Alarm")) { - - int finalID = 0; - int finalVolume = 0; - boolean finalEnabled = false; - boolean finalIncludeLinkedZones = false; - DateTime finalStartTime = null; - Period finalDuration = null; - - try { - finalID = Integer.parseInt(id); - finalVolume = Integer.parseInt(volume); - if(enabled.equals("0")) { - finalEnabled = false; - } else { - finalEnabled = true; - } - - if(includeLinkedZones.equals("0")) { - finalIncludeLinkedZones = false; - } else { - finalIncludeLinkedZones = true; - } - } catch (Exception e) { - logger.debug("Error parsing Integer"); - } - - try { - DateTimeFormatter formatter = DateTimeFormat.forPattern("HH:mm:ss"); - finalStartTime = formatter.parseDateTime(startTime); - LocalTime localDateTime = finalStartTime.toLocalTime(); - finalStartTime = localDateTime.toDateTimeToday(); - - PeriodFormatter pFormatter= new PeriodFormatterBuilder() - .appendHours() - .appendSeparator(":") - .appendMinutes() - .appendSeparator(":") - .appendSeconds() - .toFormatter(); - - finalDuration = pFormatter.parsePeriod(duration); - } catch(Exception e) { - logger.error("Error parsing DateTime"); - } - - alarms.add(new SonosAlarm(finalID, finalStartTime, finalDuration, recurrence, - finalEnabled, roomUUID, programURI, - programMetaData, playMode, finalVolume, finalIncludeLinkedZones)); - } - - } - - public List getAlarms() { - return alarms; - } - } - - static private class ZoneGroupHandler extends DefaultHandler { - - private final List groups = new ArrayList(); - private final List currentGroupPlayers = new ArrayList(); - private String coordinator; - private String groupId; - - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - if (qName.equals("ZoneGroup")) { - groupId = attributes.getValue("ID"); - coordinator = attributes.getValue("Coordinator"); - } else if (qName.equals("ZoneGroupMember")) { - currentGroupPlayers.add(attributes.getValue("UUID")); - } - } - - @Override - public void endElement(String uri, String localName, String qName) throws SAXException { - if (qName.equals("ZoneGroup")) { - groups.add(new SonosZoneGroup(groupId, coordinator, currentGroupPlayers)); - currentGroupPlayers.clear(); - } - } - - public List getGroups() { - return groups; - } - } - - static private class OpmlHandler extends DefaultHandler { - -// -// -// 200 -// -// -// -// -// -// -// -// -// -// http://radiotime-logos.s3.amazonaws.com/s87683.png -// -// -// -// -// - - private final List textFields = new ArrayList(); - private String textField; - private String type; - private String logo; - - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - if (qName.equals("outline")) { - type = attributes.getValue("type"); - if(type.equals("text")) { - textField = attributes.getValue("text"); - } else { - textField = null; - } - } else if (qName.equals("logo")) { - //logo = attributes.getValue("UUID"); - } - } - - @Override - public void endElement(String uri, String localName, String qName) throws SAXException { - if (qName.equals("outline")) { - if(textField != null) { - textFields.add(textField); - } - } - } - - public List getTextFields() { - return textFields; - } - - } - - static private class AVTransportEventHandler extends DefaultHandler { - - /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - - private final Map changes = new HashMap(); - - @Override - public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { - /* The events are all of the form so we can get all - * the info we need from here. - */ - try { - if(atts.getValue("val") != null) { - StateVariable stateVariable = new StateVariable(localName, new StateVariableTypeDetails(new StringDatatype())); - StateVariableValue stateVariableValue = new StateVariableValue(stateVariable, atts.getValue("val")); - changes.put(localName, stateVariableValue); - } - } catch (IllegalArgumentException e) { - // this means that localName isn't defined in EventType, which is expected for some elements - logger.info("{} is not defined in EventType. ",localName); - } - } - - public Map getChanges() { - return changes; - } - - } - - - static private class MetaDataHandler extends DefaultHandler { - - private CurrentElement currentElement = null; - - private String id = "-1"; - private String parentId = "-1"; - private StringBuilder resource = new StringBuilder(); - private StringBuilder streamContent = new StringBuilder(); - private StringBuilder albumArtUri = new StringBuilder(); - private StringBuilder title = new StringBuilder(); - private StringBuilder upnpClass = new StringBuilder(); - private StringBuilder creator = new StringBuilder(); - private StringBuilder album = new StringBuilder(); - private StringBuilder albumArtist = new StringBuilder(); - - @Override - public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { - if ("item".equals(localName)) { - currentElement = CurrentElement.item; - id = atts.getValue("id"); - parentId = atts.getValue("parentID"); - } else if ("res".equals(localName)) { - currentElement = CurrentElement.res; - } else if ("streamContent".equals(localName)) { - currentElement = CurrentElement.streamContent; - } else if ("albumArtURI".equals(localName)) { - currentElement = CurrentElement.albumArtURI; - } else if ("title".equals(localName)) { - currentElement = CurrentElement.title; - } else if ("class".equals(localName)) { - currentElement = CurrentElement.upnpClass; - } else if ("creator".equals(localName)) { - currentElement = CurrentElement.creator; - } else if ("album".equals(localName)) { - currentElement = CurrentElement.album; - } else if ("albumArtist".equals(localName)) { - currentElement = CurrentElement.albumArtist; - } else { - // unknown element - currentElement = null; - } - } - - @Override - public void characters(char[] ch, int start, int length) throws SAXException { - if (currentElement != null) { - switch (currentElement) { - case item: - break; - case res: resource.append(ch, start, length); - break; - case streamContent: streamContent.append(ch, start, length); - break; - case albumArtURI: albumArtUri.append(ch, start, length); - break; - case title: title.append(ch, start, length); - break; - case upnpClass: upnpClass.append(ch, start, length); - break; - case creator: creator.append(ch, start, length); - break; - case album: album.append(ch, start, length); - break; - case albumArtist: albumArtist.append(ch, start, length); - break; - } - } - } - - public SonosMetaData getMetaData() { - return new SonosMetaData(id, parentId, resource.toString(), - streamContent.toString(), albumArtUri.toString(), - title.toString(), upnpClass.toString(), creator.toString(), - album.toString(), albumArtist.toString()); - } - - } - - static private class RenderingControlEventHandler extends DefaultHandler { - - private final Map changes = new HashMap(); - - private boolean getPresetName=false; - private String presetName; - - @Override - public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { - StateVariable stateVariable = new StateVariable(localName, new StateVariableTypeDetails(new StringDatatype())); - StateVariableValue stateVariableValue = new StateVariableValue(stateVariable, atts.getValue("val")); - - - if ("Volume".equals(qName)) { - changes.put(qName+atts.getValue("channel"), stateVariableValue); - } else if ("Mute".equals(qName)) { - changes.put(qName+atts.getValue("channel"), stateVariableValue); - } else if ("Bass".equals(qName)) { - changes.put(qName, stateVariableValue); - } else if ("Treble".equals(qName)) { - changes.put(qName, stateVariableValue); - } else if ("Loudness".equals(qName)) { - changes.put(qName+atts.getValue("channel"), stateVariableValue); - } else if ("OutputFixed".equals(qName)) { - changes.put(qName, stateVariableValue); - } else if ("PresetNameList".equals(qName)) { - getPresetName=true; - } - } - - @Override - public void characters(char[] ch, int start, int length) throws SAXException { - if (getPresetName) { - presetName = new String(ch, start, length); - } - } - - @Override - public void endElement(String uri, String localName, String qName) throws SAXException { - if (getPresetName) { - getPresetName = false; - StateVariable stateVariable = new StateVariable(localName, new StateVariableTypeDetails(new StringDatatype())); - StateVariableValue stateVariableValue = new StateVariableValue(stateVariable, presetName); - changes.put(qName, stateVariableValue); - } - } - - public Map getChanges() { - return changes; - } - - - } - - public static String compileMetadataString(SonosEntry entry) { -// if (upnpClass.startsWith("object.container")) { -// upnpClass = "object.container"; -// } - /** - * If the entry contains resource meta data we will override this with - * that data. - */ - String id = entry.getId(); - String parentId = entry.getParentId(); - String title = entry.getTitle(); - String upnpClass = entry.getUpnpClass(); - - /** - * By default 'RINCON_AssociatedZPUDN' is used for most operations, - * however when playing a favorite entry that is associated withh a - * subscription like pandora we need to use the desc string asscoiated - * with that item. - */ - String desc = "RINCON_AssociatedZPUDN"; - - /** - * If resource meta data exists, use it over the parent data - */ - if(entry.getResourceMetaData() != null){ - id = entry.getResourceMetaData().getId(); - parentId = entry.getResourceMetaData().getParentId(); - title = entry.getResourceMetaData().getTitle(); - desc = entry.getResourceMetaData().getDesc(); - upnpClass = entry.getResourceMetaData().getUpnpClass(); - } - - String metadata = METADATA_FORMAT.format(new Object[] {id, parentId, title, upnpClass, desc}); - - return metadata; - } + + static final Logger logger = LoggerFactory.getLogger(SonosXMLParser.class); + + private static MessageFormat METADATA_FORMAT = new MessageFormat( + "" + + "" + "{2}" + + "{3}" + + "" + "{4}" + + ""); + + private enum Element { + TITLE, + CLASS, + ALBUM, + ALBUM_ART_URI, + CREATOR, + RES, + TRACK_NUMBER, + RESMD, + DESC + } + + private enum CurrentElement { + item, + res, + streamContent, + albumArtURI, + title, + upnpClass, + creator, + album, + albumArtist, + desc; + } + + /** + * @param xml + * @return a list of alarms from the given xml string. + * @throws IOException + * @throws SAXException + */ + public static List getAlarmsFromStringResult(String xml) throws SAXException { + XMLReader reader = XMLReaderFactory.createXMLReader(); + AlarmHandler handler = new AlarmHandler(); + reader.setContentHandler(handler); + try { + reader.parse(new InputSource(new StringReader(xml))); + } catch (IOException e) { + logger.error("Could not parse Alarms from String {}", xml); + } + return handler.getAlarms(); + } + + /** + * @param xml + * @return a list of Entrys from the given xml string. + * @throws IOException + * @throws SAXException + */ + public static List getEntriesFromString(String xml) throws SAXException { + XMLReader reader = XMLReaderFactory.createXMLReader(); + EntryHandler handler = new EntryHandler(); + reader.setContentHandler(handler); + try { + reader.parse(new InputSource(new StringReader(xml))); + } catch (IOException e) { + logger.error("Could not parse Entries from String {}", xml); + } + return handler.getArtists(); + } + + /** + * Returns the meta data which is needed to play Pandora + * (and others?) favorites + * + * @param xml + * @return The value of the desc xml tag + * @throws SAXException + */ + public static SonosResourceMetaData getEmbededMetaDataFromResource(String xml) throws SAXException { + XMLReader reader = XMLReaderFactory.createXMLReader(); + EmbededMetaDataHandler handler = new EmbededMetaDataHandler(); + reader.setContentHandler(handler); + try { + reader.parse(new InputSource(new StringReader(xml))); + } catch (IOException e) { + logger.error("Could not parse Entries from String {}", xml); + } + return handler.getMetaData(); + } + + /** + * @param controller + * @param xml + * @return zone group from the given xml + * @throws IOException + * @throws SAXException + */ + public static List getZoneGroupFromXML(String xml) throws SAXException { + XMLReader reader = XMLReaderFactory.createXMLReader(); + ZoneGroupHandler handler = new ZoneGroupHandler(); + reader.setContentHandler(handler); + try { + reader.parse(new InputSource(new StringReader(xml))); + } catch (IOException e) { + // This should never happen - we're not performing I/O! + logger.error("Could not parse ZoneGroup from String {}", xml); + } + + return handler.getGroups(); + + } + + public static List getRadioTimeFromXML(String xml) throws SAXException { + XMLReader reader = XMLReaderFactory.createXMLReader(); + OpmlHandler handler = new OpmlHandler(); + reader.setContentHandler(handler); + try { + reader.parse(new InputSource(new StringReader(xml))); + } catch (IOException e) { + // This should never happen - we're not performing I/O! + logger.error("Could not parse RadioTime from String {}", xml); + } + + return handler.getTextFields(); + + } + + public static Map getRenderingControlFromXML(String xml) throws SAXException { + XMLReader reader = XMLReaderFactory.createXMLReader(); + RenderingControlEventHandler handler = new RenderingControlEventHandler(); + reader.setContentHandler(handler); + try { + reader.parse(new InputSource(new StringReader(xml))); + } catch (IOException e) { + // This should never happen - we're not performing I/O! + logger.debug("Could not parse Rendering Control event: {}", e); + } + return handler.getChanges(); + } + + public static Map getAVTransportFromXML(String xml) throws SAXException { + XMLReader reader = XMLReaderFactory.createXMLReader(); + AVTransportEventHandler handler = new AVTransportEventHandler(); + reader.setContentHandler(handler); + try { + reader.parse(new InputSource(new StringReader(xml))); + } catch (IOException e) { + // This should never happen - we're not performing I/O! + logger.error("Could not parse AV Transport Event: {}", e); + } + return handler.getChanges(); + } + + public static SonosMetaData getMetaDataFromXML(String xml) throws SAXException { + XMLReader reader = XMLReaderFactory.createXMLReader(); + // logger.debug("getTrackFromXML {}",xml); + MetaDataHandler handler = new MetaDataHandler(); + reader.setContentHandler(handler); + try { + reader.parse(new InputSource(new StringReader(xml))); + } catch (IOException e) { + // This should never happen - we're not performing I/O! + logger.error("Could not parse AV Transport Event: {}", e); + } + return handler.getMetaData(); + } + + static private class EntryHandler extends DefaultHandler { + + // Maintain a set of elements about which it is unuseful to complain about. + // This list will be initialized on the first failure case + private static List ignore = null; + + private String id; + private String parentId; + private StringBuilder upnpClass = new StringBuilder(); + private StringBuilder res = new StringBuilder(); + private StringBuilder title = new StringBuilder(); + private StringBuilder album = new StringBuilder(); + private StringBuilder albumArtUri = new StringBuilder(); + private StringBuilder creator = new StringBuilder(); + private StringBuilder trackNumber = new StringBuilder(); + private StringBuilder desc = new StringBuilder(); + private Element element = null; + + private List artists = new ArrayList(); + + EntryHandler() { + // shouldn't be used outside of this package. + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + if (qName.equals("container") || qName.equals("item")) { + id = attributes.getValue("id"); + parentId = attributes.getValue("parentID"); + } else if (qName.equals("res")) { + element = Element.RES; + } else if (qName.equals("dc:title")) { + element = Element.TITLE; + } else if (qName.equals("upnp:class")) { + element = Element.CLASS; + } else if (qName.equals("dc:creator")) { + element = Element.CREATOR; + } else if (qName.equals("upnp:album")) { + element = Element.ALBUM; + } else if (qName.equals("upnp:albumArtURI")) { + element = Element.ALBUM_ART_URI; + } else if (qName.equals("upnp:originalTrackNumber")) { + element = Element.TRACK_NUMBER; + } else if (qName.equals("r:resMD")) { + element = Element.RESMD; + } else { + if (ignore == null) { + ignore = new ArrayList(); + ignore.add("DIDL-Lite"); + ignore.add("type"); + ignore.add("ordinal"); + ignore.add("description"); + } + + if (!ignore.contains(localName)) { + logger.warn("Did not recognise element named {}", localName); + } + element = null; + } + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + if (element == null) { + return; + } + switch (element) { + case TITLE: + title.append(ch, start, length); + break; + case CLASS: + upnpClass.append(ch, start, length); + break; + case RES: + res.append(ch, start, length); + break; + case ALBUM: + album.append(ch, start, length); + break; + case ALBUM_ART_URI: + albumArtUri.append(ch, start, length); + break; + case CREATOR: + creator.append(ch, start, length); + break; + case TRACK_NUMBER: + trackNumber.append(ch, start, length); + break; + case RESMD: + desc.append(ch, start, length); + break; + // no default + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + if (qName.equals("container") || qName.equals("item")) { + element = null; + + int trackNumberVal = 0; + try { + trackNumberVal = Integer.parseInt(trackNumber.toString()); + } catch (Exception e) { + } + + SonosResourceMetaData md = null; + + // The resource description is needed for playing favorites on pandora + try { + md = getEmbededMetaDataFromResource(desc.toString()); + } catch (SAXException ignore) { + logger.debug("Failed to parse embedded", ignore); + } + + artists.add(new SonosEntry(id, title.toString(), parentId, album.toString(), albumArtUri.toString(), + creator.toString(), upnpClass.toString(), res.toString(), trackNumberVal, md)); + title = new StringBuilder(); + upnpClass = new StringBuilder(); + res = new StringBuilder(); + album = new StringBuilder(); + albumArtUri = new StringBuilder(); + creator = new StringBuilder(); + trackNumber = new StringBuilder(); + desc = new StringBuilder(); + } + } + + public List getArtists() { + return artists; + } + } + + static private class EmbededMetaDataHandler extends DefaultHandler { + + private String id; + private String parentId; + private StringBuilder title = new StringBuilder(); + private StringBuilder upnpClass = new StringBuilder(); + private StringBuilder desc = new StringBuilder(); + private Element element = null; + private SonosResourceMetaData metaData = null; + + EmbededMetaDataHandler() { + // shouldn't be used outside of this package. + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + + if (qName.equals("container") || qName.equals("item")) { + id = attributes.getValue("id"); + parentId = attributes.getValue("parentID"); + } else if (qName.equals("desc")) { + element = Element.DESC; + } else if (qName.equals("upnp:class")) { + element = Element.CLASS; + } else if (qName.equals("dc:title")) { + element = Element.TITLE; + } else { + element = null; + } + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + if (element == null) { + return; + } + switch (element) { + case TITLE: + title.append(ch, start, length); + break; + case CLASS: + upnpClass.append(ch, start, length); + break; + case DESC: + desc.append(ch, start, length); + ; + break; + default: + break; + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + if (qName.equals("DIDL-Lite")) { + metaData = new SonosResourceMetaData(id, parentId, title.toString(), upnpClass.toString(), + desc.toString()); + element = null; + desc = new StringBuilder(); + upnpClass = new StringBuilder(); + title = new StringBuilder(); + } + } + + public SonosResourceMetaData getMetaData() { + return metaData; + } + } + + static private class AlarmHandler extends DefaultHandler { + + private String id; + private String startTime; + private String duration; + private String recurrence; + private String enabled; + private String roomUUID; + private String programURI; + private String programMetaData; + private String playMode; + private String volume; + private String includeLinkedZones; + + private List alarms = new ArrayList(); + + AlarmHandler() { + // shouldn't be used outside of this package. + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + + if (qName.equals("Alarm")) { + id = attributes.getValue("ID"); + duration = attributes.getValue("Duration"); + recurrence = attributes.getValue("Recurrence"); + startTime = attributes.getValue("StartTime"); + enabled = attributes.getValue("Enabled"); + roomUUID = attributes.getValue("RoomUUID"); + programURI = attributes.getValue("ProgramURI"); + programMetaData = attributes.getValue("ProgramMetaData"); + playMode = attributes.getValue("PlayMode"); + volume = attributes.getValue("Volume"); + includeLinkedZones = attributes.getValue("IncludeLinkedZones"); + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + + if (qName.equals("Alarm")) { + + int finalID = 0; + int finalVolume = 0; + boolean finalEnabled = false; + boolean finalIncludeLinkedZones = false; + DateTime finalStartTime = null; + Period finalDuration = null; + + try { + finalID = Integer.parseInt(id); + finalVolume = Integer.parseInt(volume); + if (enabled.equals("0")) { + finalEnabled = false; + } else { + finalEnabled = true; + } + + if (includeLinkedZones.equals("0")) { + finalIncludeLinkedZones = false; + } else { + finalIncludeLinkedZones = true; + } + } catch (Exception e) { + logger.debug("Error parsing Integer"); + } + + try { + DateTimeFormatter formatter = DateTimeFormat.forPattern("HH:mm:ss"); + finalStartTime = formatter.parseDateTime(startTime); + LocalTime localDateTime = finalStartTime.toLocalTime(); + finalStartTime = localDateTime.toDateTimeToday(); + + PeriodFormatter pFormatter = new PeriodFormatterBuilder().appendHours().appendSeparator(":") + .appendMinutes().appendSeparator(":").appendSeconds().toFormatter(); + + finalDuration = pFormatter.parsePeriod(duration); + } catch (Exception e) { + logger.error("Error parsing DateTime"); + } + + alarms.add(new SonosAlarm(finalID, finalStartTime, finalDuration, recurrence, finalEnabled, roomUUID, + programURI, programMetaData, playMode, finalVolume, finalIncludeLinkedZones)); + } + + } + + public List getAlarms() { + return alarms; + } + } + + static private class ZoneGroupHandler extends DefaultHandler { + + private final List groups = new ArrayList(); + private final List currentGroupPlayers = new ArrayList(); + private String coordinator; + private String groupId; + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + if (qName.equals("ZoneGroup")) { + groupId = attributes.getValue("ID"); + coordinator = attributes.getValue("Coordinator"); + } else if (qName.equals("ZoneGroupMember")) { + currentGroupPlayers.add(attributes.getValue("UUID")); + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + if (qName.equals("ZoneGroup")) { + groups.add(new SonosZoneGroup(groupId, coordinator, currentGroupPlayers)); + currentGroupPlayers.clear(); + } + } + + public List getGroups() { + return groups; + } + } + + static private class OpmlHandler extends DefaultHandler { + + // + // + // 200 + // + // + // + // + // + // + // + // + // + // http://radiotime-logos.s3.amazonaws.com/s87683.png + // + // + // + // + // + + private final List textFields = new ArrayList(); + private String textField; + private String type; + private String logo; + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + if (qName.equals("outline")) { + type = attributes.getValue("type"); + if (type.equals("text")) { + textField = attributes.getValue("text"); + } else { + textField = null; + } + } else if (qName.equals("logo")) { + // logo = attributes.getValue("UUID"); + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + if (qName.equals("outline")) { + if (textField != null) { + textFields.add(textField); + } + } + } + + public List getTextFields() { + return textFields; + } + + } + + static private class AVTransportEventHandler extends DefaultHandler { + + /* + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ + + private final Map changes = new HashMap(); + + @Override + public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { + /* + * The events are all of the form so we can get all + * the info we need from here. + */ + try { + if (atts.getValue("val") != null) { + StateVariable stateVariable = new StateVariable(localName, + new StateVariableTypeDetails(new StringDatatype())); + StateVariableValue stateVariableValue = new StateVariableValue(stateVariable, atts.getValue("val")); + changes.put(localName, stateVariableValue); + } + } catch (IllegalArgumentException e) { + // this means that localName isn't defined in EventType, which is expected for some elements + logger.info("{} is not defined in EventType. ", localName); + } + } + + public Map getChanges() { + return changes; + } + + } + + static private class MetaDataHandler extends DefaultHandler { + + private CurrentElement currentElement = null; + + private String id = "-1"; + private String parentId = "-1"; + private StringBuilder resource = new StringBuilder(); + private StringBuilder streamContent = new StringBuilder(); + private StringBuilder albumArtUri = new StringBuilder(); + private StringBuilder title = new StringBuilder(); + private StringBuilder upnpClass = new StringBuilder(); + private StringBuilder creator = new StringBuilder(); + private StringBuilder album = new StringBuilder(); + private StringBuilder albumArtist = new StringBuilder(); + + @Override + public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { + if ("item".equals(localName)) { + currentElement = CurrentElement.item; + id = atts.getValue("id"); + parentId = atts.getValue("parentID"); + } else if ("res".equals(localName)) { + currentElement = CurrentElement.res; + } else if ("streamContent".equals(localName)) { + currentElement = CurrentElement.streamContent; + } else if ("albumArtURI".equals(localName)) { + currentElement = CurrentElement.albumArtURI; + } else if ("title".equals(localName)) { + currentElement = CurrentElement.title; + } else if ("class".equals(localName)) { + currentElement = CurrentElement.upnpClass; + } else if ("creator".equals(localName)) { + currentElement = CurrentElement.creator; + } else if ("album".equals(localName)) { + currentElement = CurrentElement.album; + } else if ("albumArtist".equals(localName)) { + currentElement = CurrentElement.albumArtist; + } else { + // unknown element + currentElement = null; + } + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + if (currentElement != null) { + switch (currentElement) { + case item: + break; + case res: + resource.append(ch, start, length); + break; + case streamContent: + streamContent.append(ch, start, length); + break; + case albumArtURI: + albumArtUri.append(ch, start, length); + break; + case title: + title.append(ch, start, length); + break; + case upnpClass: + upnpClass.append(ch, start, length); + break; + case creator: + creator.append(ch, start, length); + break; + case album: + album.append(ch, start, length); + break; + case albumArtist: + albumArtist.append(ch, start, length); + break; + } + } + } + + public SonosMetaData getMetaData() { + return new SonosMetaData(id, parentId, resource.toString(), streamContent.toString(), + albumArtUri.toString(), title.toString(), upnpClass.toString(), creator.toString(), + album.toString(), albumArtist.toString()); + } + + } + + static private class RenderingControlEventHandler extends DefaultHandler { + + private final Map changes = new HashMap(); + + private boolean getPresetName = false; + private String presetName; + + @Override + public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { + StateVariable stateVariable = new StateVariable(localName, + new StateVariableTypeDetails(new StringDatatype())); + StateVariableValue stateVariableValue = new StateVariableValue(stateVariable, atts.getValue("val")); + + if ("Volume".equals(qName)) { + changes.put(qName + atts.getValue("channel"), stateVariableValue); + } else if ("Mute".equals(qName)) { + changes.put(qName + atts.getValue("channel"), stateVariableValue); + } else if ("Bass".equals(qName)) { + changes.put(qName, stateVariableValue); + } else if ("Treble".equals(qName)) { + changes.put(qName, stateVariableValue); + } else if ("Loudness".equals(qName)) { + changes.put(qName + atts.getValue("channel"), stateVariableValue); + } else if ("OutputFixed".equals(qName)) { + changes.put(qName, stateVariableValue); + } else if ("PresetNameList".equals(qName)) { + getPresetName = true; + } + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + if (getPresetName) { + presetName = new String(ch, start, length); + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + if (getPresetName) { + getPresetName = false; + StateVariable stateVariable = new StateVariable(localName, + new StateVariableTypeDetails(new StringDatatype())); + StateVariableValue stateVariableValue = new StateVariableValue(stateVariable, presetName); + changes.put(qName, stateVariableValue); + } + } + + public Map getChanges() { + return changes; + } + + } + + public static String compileMetadataString(SonosEntry entry) { + // if (upnpClass.startsWith("object.container")) { + // upnpClass = "object.container"; + // } + /** + * If the entry contains resource meta data we will override this with + * that data. + */ + String id = entry.getId(); + String parentId = entry.getParentId(); + String title = entry.getTitle(); + String upnpClass = entry.getUpnpClass(); + + /** + * By default 'RINCON_AssociatedZPUDN' is used for most operations, + * however when playing a favorite entry that is associated withh a + * subscription like pandora we need to use the desc string asscoiated + * with that item. + */ + String desc = "RINCON_AssociatedZPUDN"; + + /** + * If resource meta data exists, use it over the parent data + */ + if (entry.getResourceMetaData() != null) { + id = entry.getResourceMetaData().getId(); + parentId = entry.getResourceMetaData().getParentId(); + title = entry.getResourceMetaData().getTitle(); + desc = entry.getResourceMetaData().getDesc(); + upnpClass = entry.getResourceMetaData().getUpnpClass(); + } + + String metadata = METADATA_FORMAT.format(new Object[] { id, parentId, title, upnpClass, desc }); + + return metadata; + } } diff --git a/bundles/binding/org.openhab.binding.stiebelheatpump/src/main/java/org/openhab/binding/stiebelheatpump/internal/CommunicationService.java b/bundles/binding/org.openhab.binding.stiebelheatpump/src/main/java/org/openhab/binding/stiebelheatpump/internal/CommunicationService.java index fffc9fd145f..857d1e2af9f 100644 --- a/bundles/binding/org.openhab.binding.stiebelheatpump/src/main/java/org/openhab/binding/stiebelheatpump/internal/CommunicationService.java +++ b/bundles/binding/org.openhab.binding.stiebelheatpump/src/main/java/org/openhab/binding/stiebelheatpump/internal/CommunicationService.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2015, openHAB.org and others. + * Copyright (c) 2010-2016, openHAB.org and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -26,691 +26,648 @@ public class CommunicationService { - private ProtocolConnector connector; - private static final int MAXRETRIES = 100; - private final int INPUT_BUFFER_LENGTH = 1024; - private byte buffer[] = new byte[INPUT_BUFFER_LENGTH]; - - public static int WAITING_TIME_BETWEEN_REQUESTS = 2000; - - /** heat pump request definition */ - private List heatPumpConfiguration = new ArrayList(); - private List heatPumpSensorConfiguration = new ArrayList(); - private List heatPumpSettingConfiguration = new ArrayList(); - private List heatPumpStatusConfiguration = new ArrayList(); - Request versionRequest; - - DataParser parser = new DataParser(); - - private static final Logger logger = LoggerFactory - .getLogger(CommunicationService.class); - - public CommunicationService(ProtocolConnector connector) { - this.connector = connector; - this.connector.connect(); - } - - public CommunicationService(ProtocolConnector connector, List configuration) { - this(connector); - heatPumpConfiguration = configuration; - categorizeHeatPumpConfiguration(); - } - - public void finalizer() { - connector.disconnect(); - } - - /** - * This method reads the version information from the heat pump - * - * @return version string, e.g: 2.06 - */ - public String getversion() throws StiebelHeatPumpException { - String version = ""; - try { - Map data = readData(versionRequest); - version = data.get("Version"); - Thread.sleep(WAITING_TIME_BETWEEN_REQUESTS); - } catch (InterruptedException e) { - throw new StiebelHeatPumpException(e.toString()); - } - return version; - } - - /** - * This method reads all settings defined in the heat pump configuration - * from the heat pump - * - * @return map of heat pump setting values - */ - public Map getSettings() throws StiebelHeatPumpException { - logger.debug("Loading Settings"); - Map data = new HashMap(); - for (Request request : heatPumpSettingConfiguration) { - logger.debug("Loading data for request {} ...", request.getName()); - try { - Map newData = readData(request); - data.putAll(newData); - Thread.sleep(WAITING_TIME_BETWEEN_REQUESTS); - } catch (InterruptedException e) { - throw new StiebelHeatPumpException(e.toString()); - } - } - return data; - } - - /** - * This method reads all sensor values defined in the heat pump - * configuration from the heat pump - * - * @return map of heat pump sensor values - */ - public Map getSensors() throws StiebelHeatPumpException { - logger.debug("Loading Sensors"); - Map data = new HashMap(); - for (Request request : heatPumpSensorConfiguration) { - logger.debug("Loading data for request {} ...", request.getName()); - try { - Map newData = readData(request); - data.putAll(newData); - Thread.sleep(WAITING_TIME_BETWEEN_REQUESTS); - } catch (InterruptedException e) { - throw new StiebelHeatPumpException(e.toString()); - } - } - return data; - } - - /** - * This method reads all status values defined in the heat pump - * configuration from the heat pump - * - * @return map of heat pump status values - */ - public Map getStatus() throws StiebelHeatPumpException { - logger.debug("Loading Status"); - Map data = new HashMap(); - for (Request request : heatPumpStatusConfiguration) { - logger.debug("Loading data for request {} ...", request.getName()); - try { - Map newData = readData(request); - data.putAll(newData); - Thread.sleep(WAITING_TIME_BETWEEN_REQUESTS); - } catch (InterruptedException e) { - throw new StiebelHeatPumpException(e.toString()); - } - } - return data; - } - - /** - * This method set the time of the heat pump to the current time - * - * @return true if time has been updated - */ - public Map setTime() throws StiebelHeatPumpException { - - startCommunication(); - Map data = new HashMap(); - - Request timeRequest = null; - - for (Request request : heatPumpSettingConfiguration) { - if (request.getName().equals("Time")) { - timeRequest = request; - break; - } - } - - if (timeRequest == null) { - logger.warn("Could not find request definition for time settings! Skip setting time."); - return data; - } - - logger.debug("Loading current time data ..."); - try { - // get time from heat pump - byte[] requestMessage = createRequestMessage(timeRequest); - byte[] response = getData(requestMessage); - - // get current time from local machine - DateTime dt = DateTime.now(); - logger.debug("Current time is : {}", dt.toString()); - String weekday = Integer.toString(dt.getDayOfWeek() - 1); - String day = Integer.toString(dt.getDayOfMonth()); - String month = Integer.toString(dt.getMonthOfYear()); - String year = Integer.toString(dt.getYearOfCentury()); - String seconds = Integer.toString(dt.getSecondOfMinute()); - String hours = Integer.toString(dt.getHourOfDay()); - String minutes = Integer.toString(dt.getMinuteOfHour()); - - data = parser.parseRecords(response, timeRequest); - - boolean updateRequired = false; - for (Map.Entry entry : data.entrySet()) { - String entryName = entry.getKey(); - String entryValue = entry.getValue(); - RecordDefinition currentRecord = null; - - for (RecordDefinition record : timeRequest - .getRecordDefinitions()) { - if (record.getName().equals(entryName)) { - currentRecord = record; - break; - } - } - if (entryName.equals("WeekDay") && !entryValue.equals(weekday)) { - updateRequired = true; - response = parser.composeRecord(weekday, response, - currentRecord); - logger.debug("WeekDay needs update from {} to {}", - entryValue, weekday); - continue; - } - if (entryName.equals("Hours") && !entryValue.equals(hours)) { - updateRequired = true; - response = parser.composeRecord(hours, response, - currentRecord); - logger.debug("Hours needs update from {} to {}", - entryValue, hours); - continue; - } - if (entryName.equals("Minutes") && !entryValue.equals(minutes)) { - updateRequired = true; - response = parser.composeRecord(minutes, response, - currentRecord); - logger.debug("Minutes needs update from {} to {}", - entryValue, minutes); - continue; - } - if (entryName.equals("Seconds") && !entryValue.equals(seconds)) { - updateRequired = true; - response = parser.composeRecord(seconds, response, - currentRecord); - logger.debug("Seconds needs update from {} to {}", - entryValue, seconds); - continue; - } - if (entryName.equals("Year") && !entryValue.equals(year)) { - updateRequired = true; - response = parser.composeRecord(year, response, - currentRecord); - logger.debug("Year needs update from {} to {}", entryValue, - year); - continue; - } - if (entryName.equals("Month") && !entryValue.equals(month)) { - updateRequired = true; - response = parser.composeRecord(month, response, - currentRecord); - logger.debug("Month needs update from {} to {}", - entryValue, month); - continue; - } - if (entryName.equals("Day") && !entryValue.equals(day)) { - updateRequired = true; - response = parser.composeRecord(day, response, - currentRecord); - logger.debug("Day needs update from {} to {}", entryValue, - day); - continue; - } - } - - if (updateRequired) { - Thread.sleep(WAITING_TIME_BETWEEN_REQUESTS); - logger.info("Time need update. Set time to " + dt.toString()); - setData(response); - - Thread.sleep(WAITING_TIME_BETWEEN_REQUESTS); - response = getData(requestMessage); - data = parser.parseRecords(response, timeRequest); - dt = DateTime.now(); - logger.debug("Current time is : {}", dt.toString()); - - } - return data; - - } catch (InterruptedException e) { - throw new StiebelHeatPumpException(e.toString()); - } - } - - /** - * This method looks up all files in resource and List of Request objects - * into xml file - * - * @return true if heat pump configuration for version could be found and - * loaded - */ - public List getHeatPumpConfiguration(String configFile) { - ConfigLocator configLocator = new ConfigLocator(configFile); - heatPumpConfiguration = configLocator.getConfig(); - - if (heatPumpConfiguration != null && !heatPumpConfiguration.isEmpty()) { - logger.info("Loaded heat pump configuration {} .", configFile); - logger.info("Configuration file contains {} requests.", - heatPumpConfiguration.size()); - - if (categorizeHeatPumpConfiguration()) { - return heatPumpConfiguration; - } - } - logger.warn("Could not load heat pump configuration file for {}!", - configFile); - return null; - } - - /** - * This method categorize the heat pump configuration into setting, sensor - * and status - * - * @return true if heat pump configuration for version could be found and - * loaded - */ - private boolean categorizeHeatPumpConfiguration() { - for (Request request : heatPumpConfiguration) { - logger.debug( - "Request : Name -> {}, Description -> {} , RequestByte -> {}", - request.getName(), request.getDescription(), - DatatypeConverter.printHexBinary(new byte[] { request - .getRequestByte() })); - if (request.getName().equalsIgnoreCase("Version")) { - versionRequest = request; - logger.debug("Loaded Request : " - + versionRequest.getDescription()); - continue; - } - - for (RecordDefinition record : request.getRecordDefinitions()) { - if (record.getDataType() == Type.Settings - && !heatPumpSettingConfiguration.contains(request)) { - heatPumpSettingConfiguration.add(request); - } - if (record.getDataType() == Type.Status - && !heatPumpStatusConfiguration.contains(request)) { - heatPumpStatusConfiguration.add(request); - } - if (record.getDataType() == Type.Sensor - && !heatPumpSensorConfiguration.contains(request)) { - heatPumpSensorConfiguration.add(request); - } - } - } - - if (versionRequest == null) { - logger.debug("version request could not be found in configuration"); - return false; - } - return true; - } - - /** - * This method reads all values defined in the request from the heat pump - * - * @param request - * definition to load the values from - * @return map of heat pump values according request definition - */ - public Map readData(Request request) - throws StiebelHeatPumpException { - Map data = new HashMap(); - logger.debug( - "Request : Name -> {}, Description -> {} , RequestByte -> {}", - request.getName(), request.getDescription(), - DatatypeConverter.printHexBinary(new byte[] { request - .getRequestByte() })); - startCommunication(); - byte responseAvailable[] = new byte[0]; - byte requestMessage[] = createRequestMessage(request); - boolean validData = false; - try { - while (!validData) { - responseAvailable = getData(requestMessage); - responseAvailable = parser - .fixDuplicatedBytes(responseAvailable); - validData = parser.headerCheck(responseAvailable); - if (validData) { - data = parser.parseRecords(responseAvailable, request); - continue; - } - Thread.sleep(WAITING_TIME_BETWEEN_REQUESTS); - startCommunication(); - } - } catch (StiebelHeatPumpException e) { - logger.error("Error reading data : {}", e.toString()); - } catch (InterruptedException e) { - } - return data; - } - - /** - * This method updates the parameter item of a heat pump request - * - * @param value - * the new value of the item - * @param parameter - * to be update in the heat pump - */ - public Map setData(String value, String parameter) - throws StiebelHeatPumpException { - Request updateRequest = null; - RecordDefinition updateRecord = null; - Map data = new HashMap(); - - // we lookup the right request definition that contains the parameter to - // be updated - if (parameter != null) { - for (Request request : heatPumpSettingConfiguration) { - for (RecordDefinition record : request.getRecordDefinitions()) { - if (record.getName().equalsIgnoreCase(parameter)) { - updateRecord = record; - updateRequest = request; - - logger.debug( - "Found valid record definition {} in request {}:{}", - record.getName(), request.getName(), - request.getDescription()); - break; - } - } - } - } - - if (updateRecord == null || updateRequest == null) { - // did not find any valid record, do nothing - logger.warn("Could not find valid record definition for {}", - parameter); - return data; - } - - try { - // get actual value for the corresponding request - // as we do no have individual requests for each settings we need to - // decode the new value - // into a current response , the response is available in the - // connector object - byte[] requestMessage = createRequestMessage(updateRequest); - byte[] response = getData(requestMessage); - data = parser.parseRecords(response, updateRequest); - - // lookup parameter value in the data - String currentState = data.get(updateRecord.getName()); - if (currentState.equals(value)) { - // current State is already same as new values! - logger.debug("Current State for {} is already {}.", parameter, - value); - return data; - } - - // create new set request out from the existing read response - byte[] requestUpdateMessage = parser.composeRecord(value, response, - updateRecord); - logger.debug("Setting new value [{}] for parameter [{}]", value, - parameter); - - Thread.sleep(WAITING_TIME_BETWEEN_REQUESTS); - - response = setData(requestUpdateMessage); - - if (parser.setDataCheck(response)) { - logger.debug("Updated parameter {} successfully.", parameter); - } else { - logger.debug("Update for parameter {} failed!", parameter); - } - - } catch (StiebelHeatPumpException e) { - logger.error("Stiebel heat pump communication error during update of value! " - + e.toString()); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } finally { - } - return data; - } - - /** - * dumps response of connected heat pump by request byte - * - * @param requestByte - * request byte to send to heat pump - */ - public void dumpResponse(byte requestByte) { - Request request = new Request(); - request.setRequestByte(requestByte); - - byte requestMessage[] = createRequestMessage(request); - - if (!establishRequest(requestMessage)) { - logger.info("Could not get response for request byte {} ..."); - return; - } - try { - connector.write(DataParser.ESCAPE); - byte[] response = receiveData(); - - logger.info("Received response from heatpump: {}", - DataParser.bytesToHex(response)); - return; - } catch (Exception e) { - logger.error("Could not get data from heat pump! {}", e.toString()); - } - return; - } - - - /** - * Gets data from connected heat pump - * - * @param request - * request bytes to send to heat pump - * @return response bytes from heat pump - * - * General overview of handshake between application and serial - * interface of heat pump 1. Sending request bytes , e.g.: 01 00 FD - * FC 10 03 for version request 01 -> header start 00 -> get request - * FD -> checksum of request FC -> request byte 10 03 -> Footer - * ending the communication - * - * 2. Receive a data available 10 -> ok 02 -> it does have - * data,which wants to send now - * - * 3. acknowledge sending data 10 -> ok - * - * 4. receive data until footer 01 -> header start 00 -> get request - * CC -> checksum of send data FD -> request byte 00 CE -> data , - * e.g. short value as 2 bytes -> 206 -> 2.06 version 10 03 -> - * Footer ending the communication - */ - private byte[] getData(byte request[]) { - if (!establishRequest(request)) { - return new byte[0]; - } - try { - connector.write(DataParser.ESCAPE); - byte[] response = receiveData(); - return response; - } catch (Exception e) { - logger.error("Could not get data from heat pump! {}", e.toString()); - return buffer; - } - } - - /** - * Sets data to connected heat pump - * - * @param request - * request bytes to send to heat pump - * @return response bytes from heat pump - * - * General overview of handshake between application and serial - * interface of heat pump - * - * 1. Sending request bytes, e.g update time in heat pump 01 -> - * header start 80 -> set request F1 -> checksum of request FC -> - * request byte 00 02 0a 22 1b 0e 00 03 1a -> new values according - * record definition for time 10 03 -> Footer ending the - * communication - * - * 2. Receive response message the confirmation message is ready for - * sending 10 -> ok 02 -> it does have data ,which wants to send now - * - * 3. acknowledge sending data 10 -> ok - * - * 4. receive confirmation message until footer 01 -> header start - * 80 -> set request 7D -> checksum of send data FC -> request byte - * 10 03 -> Footer ending the communication - */ - private byte[] setData(byte[] request) throws StiebelHeatPumpException { - try { - startCommunication(); - establishRequest(request); - // Acknowledge sending data - connector.write(DataParser.ESCAPE); - - } catch (Exception e) { - logger.error("Could not set data to heat pump! {}", e.toString()); - return new byte[0]; - } - - // finally receive data - return receiveData(); - } - - /** - * This method start the communication for the request It send the initial - * handshake and expects a response - */ - private void startCommunication() throws StiebelHeatPumpException { - logger.debug("Sending start communication"); - byte response; - try { - connector.write(DataParser.STARTCOMMUNICATION); - response = connector.get(); - } catch (Exception e) { - logger.error("heat pump communication could not be established !"); - throw new StiebelHeatPumpException( - "heat pump communication could not be established !"); - } - if (response != DataParser.ESCAPE) { - logger.warn("heat pump is communicating, but did not received Escape message in inital handshake!"); - throw new StiebelHeatPumpException( - "heat pump is communicating, but did not received Escape message in inital handshake!"); - } - } - - /** - * This method establish the connection for the request It send the request - * and expects a data available response - * - * @param request - * to be send to heat pump - * @return true if data are available from heatpump - */ - private boolean establishRequest(byte[] request) { - int numBytesReadTotal = 0; - boolean dataAvailable = false; - int requestRetry = 0; - int retry = 0; - try { - while (requestRetry < MAXRETRIES) { - connector.write(request); - retry = 0; - byte singleByte; - while ((!dataAvailable) & (retry < MAXRETRIES)) { - try { - singleByte = connector.get(); - } catch (Exception e) { - retry++; - continue; - } - buffer[numBytesReadTotal] = singleByte; - numBytesReadTotal++; - if (buffer[0] != DataParser.DATAAVAILABLE[0] - || buffer[1] != DataParser.DATAAVAILABLE[1]) { - continue; - } - dataAvailable = true; - return true; - } - logger.debug("retry request!"); - startCommunication(); - } - if (!dataAvailable) { - logger.warn("heat pump has no data available for request!"); - return false; - } - } catch (Exception e1) { - logger.error("Could not get data from heat pump! {}", e1.toString()); - return false; - } - return true; - } - - /** - * This method receive the response from the heat pump It receive single - * bytes until the end of message s detected - * - * @return bytes representing the data send from heat pump - */ - private byte[] receiveData() { - byte singleByte; - int numBytesReadTotal; - int retry; - buffer = new byte[INPUT_BUFFER_LENGTH]; - retry = 0; - numBytesReadTotal = 0; - boolean endOfMessage = false; - - while (!endOfMessage & retry < MAXRETRIES) { - try { - singleByte = connector.get(); - } catch (Exception e) { - // reconnect and try again to send request - retry++; - continue; - } - - buffer[numBytesReadTotal] = singleByte; - numBytesReadTotal++; - - if (numBytesReadTotal > 4 - && buffer[numBytesReadTotal - 2] == DataParser.ESCAPE - && buffer[numBytesReadTotal - 1] == DataParser.END) { - // we have reached the end of the response - endOfMessage = true; - logger.debug("reached end of response message."); - break; - } - } - - byte[] responseBuffer = new byte[numBytesReadTotal]; - System.arraycopy(buffer, 0, responseBuffer, 0, numBytesReadTotal); - return responseBuffer; - } - - /** - * This creates the request message ready to be send to heat pump - * - * @param request - * object containing necessary information to build request - * message - * @return request message byte[] - */ - private byte[] createRequestMessage(Request request) { - short checkSum; - byte[] requestMessage = new byte[] { DataParser.HEADERSTART, - DataParser.GET, (byte) 0x00, request.getRequestByte(), - DataParser.ESCAPE, DataParser.END }; - try { - // prepare request message - checkSum = parser.calculateChecksum(requestMessage); - requestMessage[2] = parser.shortToByte(checkSum)[0]; - requestMessage = parser.addDuplicatedBytes(requestMessage); - } catch (StiebelHeatPumpException e) { - } - return requestMessage; - } + private ProtocolConnector connector; + private static final int MAXRETRIES = 100; + private final int INPUT_BUFFER_LENGTH = 1024; + private byte buffer[] = new byte[INPUT_BUFFER_LENGTH]; + + public static int WAITING_TIME_BETWEEN_REQUESTS = 2000; + + /** heat pump request definition */ + private List heatPumpConfiguration = new ArrayList(); + private List heatPumpSensorConfiguration = new ArrayList(); + private List heatPumpSettingConfiguration = new ArrayList(); + private List heatPumpStatusConfiguration = new ArrayList(); + Request versionRequest; + + DataParser parser = new DataParser(); + + private static final Logger logger = LoggerFactory.getLogger(CommunicationService.class); + + public CommunicationService(ProtocolConnector connector) { + this.connector = connector; + this.connector.connect(); + } + + public CommunicationService(ProtocolConnector connector, List configuration) { + this(connector); + heatPumpConfiguration = configuration; + categorizeHeatPumpConfiguration(); + } + + public void finalizer() { + connector.disconnect(); + } + + /** + * This method reads the version information from the heat pump + * + * @return version string, e.g: 2.06 + */ + public String getversion() throws StiebelHeatPumpException { + String version = ""; + try { + Map data = readData(versionRequest); + version = data.get("Version"); + Thread.sleep(WAITING_TIME_BETWEEN_REQUESTS); + } catch (InterruptedException e) { + throw new StiebelHeatPumpException(e.toString()); + } + return version; + } + + /** + * This method reads all settings defined in the heat pump configuration + * from the heat pump + * + * @return map of heat pump setting values + */ + public Map getSettings() throws StiebelHeatPumpException { + logger.debug("Loading Settings"); + Map data = new HashMap(); + for (Request request : heatPumpSettingConfiguration) { + logger.debug("Loading data for request {} ...", request.getName()); + try { + Map newData = readData(request); + data.putAll(newData); + Thread.sleep(WAITING_TIME_BETWEEN_REQUESTS); + } catch (InterruptedException e) { + throw new StiebelHeatPumpException(e.toString()); + } + } + return data; + } + + /** + * This method reads all sensor values defined in the heat pump + * configuration from the heat pump + * + * @return map of heat pump sensor values + */ + public Map getSensors() throws StiebelHeatPumpException { + logger.debug("Loading Sensors"); + Map data = new HashMap(); + for (Request request : heatPumpSensorConfiguration) { + logger.debug("Loading data for request {} ...", request.getName()); + try { + Map newData = readData(request); + data.putAll(newData); + Thread.sleep(WAITING_TIME_BETWEEN_REQUESTS); + } catch (InterruptedException e) { + throw new StiebelHeatPumpException(e.toString()); + } + } + return data; + } + + /** + * This method reads all status values defined in the heat pump + * configuration from the heat pump + * + * @return map of heat pump status values + */ + public Map getStatus() throws StiebelHeatPumpException { + logger.debug("Loading Status"); + Map data = new HashMap(); + for (Request request : heatPumpStatusConfiguration) { + logger.debug("Loading data for request {} ...", request.getName()); + try { + Map newData = readData(request); + data.putAll(newData); + Thread.sleep(WAITING_TIME_BETWEEN_REQUESTS); + } catch (InterruptedException e) { + throw new StiebelHeatPumpException(e.toString()); + } + } + return data; + } + + /** + * This method set the time of the heat pump to the current time + * + * @return true if time has been updated + */ + public Map setTime() throws StiebelHeatPumpException { + + startCommunication(); + Map data = new HashMap(); + + Request timeRequest = null; + + for (Request request : heatPumpSettingConfiguration) { + if (request.getName().equals("Time")) { + timeRequest = request; + break; + } + } + + if (timeRequest == null) { + logger.warn("Could not find request definition for time settings! Skip setting time."); + return data; + } + + logger.debug("Loading current time data ..."); + try { + // get time from heat pump + byte[] requestMessage = createRequestMessage(timeRequest); + byte[] response = getData(requestMessage); + + // get current time from local machine + DateTime dt = DateTime.now(); + logger.debug("Current time is : {}", dt.toString()); + String weekday = Integer.toString(dt.getDayOfWeek() - 1); + String day = Integer.toString(dt.getDayOfMonth()); + String month = Integer.toString(dt.getMonthOfYear()); + String year = Integer.toString(dt.getYearOfCentury()); + String seconds = Integer.toString(dt.getSecondOfMinute()); + String hours = Integer.toString(dt.getHourOfDay()); + String minutes = Integer.toString(dt.getMinuteOfHour()); + + data = parser.parseRecords(response, timeRequest); + + boolean updateRequired = false; + for (Map.Entry entry : data.entrySet()) { + String entryName = entry.getKey(); + String entryValue = entry.getValue(); + RecordDefinition currentRecord = null; + + for (RecordDefinition record : timeRequest.getRecordDefinitions()) { + if (record.getName().equals(entryName)) { + currentRecord = record; + break; + } + } + if (entryName.equals("WeekDay") && !entryValue.equals(weekday)) { + updateRequired = true; + response = parser.composeRecord(weekday, response, currentRecord); + logger.debug("WeekDay needs update from {} to {}", entryValue, weekday); + continue; + } + if (entryName.equals("Hours") && !entryValue.equals(hours)) { + updateRequired = true; + response = parser.composeRecord(hours, response, currentRecord); + logger.debug("Hours needs update from {} to {}", entryValue, hours); + continue; + } + if (entryName.equals("Minutes") && !entryValue.equals(minutes)) { + updateRequired = true; + response = parser.composeRecord(minutes, response, currentRecord); + logger.debug("Minutes needs update from {} to {}", entryValue, minutes); + continue; + } + if (entryName.equals("Seconds") && !entryValue.equals(seconds)) { + updateRequired = true; + response = parser.composeRecord(seconds, response, currentRecord); + logger.debug("Seconds needs update from {} to {}", entryValue, seconds); + continue; + } + if (entryName.equals("Year") && !entryValue.equals(year)) { + updateRequired = true; + response = parser.composeRecord(year, response, currentRecord); + logger.debug("Year needs update from {} to {}", entryValue, year); + continue; + } + if (entryName.equals("Month") && !entryValue.equals(month)) { + updateRequired = true; + response = parser.composeRecord(month, response, currentRecord); + logger.debug("Month needs update from {} to {}", entryValue, month); + continue; + } + if (entryName.equals("Day") && !entryValue.equals(day)) { + updateRequired = true; + response = parser.composeRecord(day, response, currentRecord); + logger.debug("Day needs update from {} to {}", entryValue, day); + continue; + } + } + + if (updateRequired) { + Thread.sleep(WAITING_TIME_BETWEEN_REQUESTS); + logger.info("Time need update. Set time to " + dt.toString()); + setData(response); + + Thread.sleep(WAITING_TIME_BETWEEN_REQUESTS); + response = getData(requestMessage); + data = parser.parseRecords(response, timeRequest); + dt = DateTime.now(); + logger.debug("Current time is : {}", dt.toString()); + + } + return data; + + } catch (InterruptedException e) { + throw new StiebelHeatPumpException(e.toString()); + } + } + + /** + * This method looks up all files in resource and List of Request objects + * into xml file + * + * @return true if heat pump configuration for version could be found and + * loaded + */ + public List getHeatPumpConfiguration(String configFile) { + ConfigLocator configLocator = new ConfigLocator(configFile); + heatPumpConfiguration = configLocator.getConfig(); + + if (heatPumpConfiguration != null && !heatPumpConfiguration.isEmpty()) { + logger.info("Loaded heat pump configuration {} .", configFile); + logger.info("Configuration file contains {} requests.", heatPumpConfiguration.size()); + + if (categorizeHeatPumpConfiguration()) { + return heatPumpConfiguration; + } + } + logger.warn("Could not load heat pump configuration file for {}!", configFile); + return null; + } + + /** + * This method categorize the heat pump configuration into setting, sensor + * and status + * + * @return true if heat pump configuration for version could be found and + * loaded + */ + private boolean categorizeHeatPumpConfiguration() { + for (Request request : heatPumpConfiguration) { + logger.debug("Request : Name -> {}, Description -> {} , RequestByte -> {}", request.getName(), + request.getDescription(), + DatatypeConverter.printHexBinary(new byte[] { request.getRequestByte() })); + if (request.getName().equalsIgnoreCase("Version")) { + versionRequest = request; + logger.debug("Loaded Request : " + versionRequest.getDescription()); + continue; + } + + for (RecordDefinition record : request.getRecordDefinitions()) { + if (record.getDataType() == Type.Settings && !heatPumpSettingConfiguration.contains(request)) { + heatPumpSettingConfiguration.add(request); + } + if (record.getDataType() == Type.Status && !heatPumpStatusConfiguration.contains(request)) { + heatPumpStatusConfiguration.add(request); + } + if (record.getDataType() == Type.Sensor && !heatPumpSensorConfiguration.contains(request)) { + heatPumpSensorConfiguration.add(request); + } + } + } + + if (versionRequest == null) { + logger.debug("version request could not be found in configuration"); + return false; + } + return true; + } + + /** + * This method reads all values defined in the request from the heat pump + * + * @param request + * definition to load the values from + * @return map of heat pump values according request definition + */ + public Map readData(Request request) throws StiebelHeatPumpException { + Map data = new HashMap(); + logger.debug("Request : Name -> {}, Description -> {} , RequestByte -> {}", request.getName(), + request.getDescription(), DatatypeConverter.printHexBinary(new byte[] { request.getRequestByte() })); + startCommunication(); + byte responseAvailable[] = new byte[0]; + byte requestMessage[] = createRequestMessage(request); + boolean validData = false; + try { + while (!validData) { + responseAvailable = getData(requestMessage); + responseAvailable = parser.fixDuplicatedBytes(responseAvailable); + validData = parser.headerCheck(responseAvailable); + if (validData) { + data = parser.parseRecords(responseAvailable, request); + continue; + } + Thread.sleep(WAITING_TIME_BETWEEN_REQUESTS); + startCommunication(); + } + } catch (StiebelHeatPumpException e) { + logger.error("Error reading data : {}", e.toString()); + } catch (InterruptedException e) { + } + return data; + } + + /** + * This method updates the parameter item of a heat pump request + * + * @param value + * the new value of the item + * @param parameter + * to be update in the heat pump + */ + public Map setData(String value, String parameter) throws StiebelHeatPumpException { + Request updateRequest = null; + RecordDefinition updateRecord = null; + Map data = new HashMap(); + + // we lookup the right request definition that contains the parameter to + // be updated + if (parameter != null) { + for (Request request : heatPumpSettingConfiguration) { + for (RecordDefinition record : request.getRecordDefinitions()) { + if (record.getName().equalsIgnoreCase(parameter)) { + updateRecord = record; + updateRequest = request; + + logger.debug("Found valid record definition {} in request {}:{}", record.getName(), + request.getName(), request.getDescription()); + break; + } + } + } + } + + if (updateRecord == null || updateRequest == null) { + // did not find any valid record, do nothing + logger.warn("Could not find valid record definition for {}", parameter); + return data; + } + + try { + // get actual value for the corresponding request + // as we do no have individual requests for each settings we need to + // decode the new value + // into a current response , the response is available in the + // connector object + byte[] requestMessage = createRequestMessage(updateRequest); + byte[] response = getData(requestMessage); + data = parser.parseRecords(response, updateRequest); + + // lookup parameter value in the data + String currentState = data.get(updateRecord.getName()); + if (currentState.equals(value)) { + // current State is already same as new values! + logger.debug("Current State for {} is already {}.", parameter, value); + return data; + } + + // create new set request out from the existing read response + byte[] requestUpdateMessage = parser.composeRecord(value, response, updateRecord); + logger.debug("Setting new value [{}] for parameter [{}]", value, parameter); + + Thread.sleep(WAITING_TIME_BETWEEN_REQUESTS); + + response = setData(requestUpdateMessage); + + if (parser.setDataCheck(response)) { + logger.debug("Updated parameter {} successfully.", parameter); + } else { + logger.debug("Update for parameter {} failed!", parameter); + } + + } catch (StiebelHeatPumpException e) { + logger.error("Stiebel heat pump communication error during update of value! " + e.toString()); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } finally { + } + return data; + } + + /** + * dumps response of connected heat pump by request byte + * + * @param requestByte + * request byte to send to heat pump + */ + public void dumpResponse(byte requestByte) { + Request request = new Request(); + request.setRequestByte(requestByte); + + byte requestMessage[] = createRequestMessage(request); + + if (!establishRequest(requestMessage)) { + logger.info("Could not get response for request byte {} ..."); + return; + } + try { + connector.write(DataParser.ESCAPE); + byte[] response = receiveData(); + + logger.info("Received response from heatpump: {}", DataParser.bytesToHex(response)); + return; + } catch (Exception e) { + logger.error("Could not get data from heat pump! {}", e.toString()); + } + return; + } + + /** + * Gets data from connected heat pump + * + * @param request + * request bytes to send to heat pump + * @return response bytes from heat pump + * + * General overview of handshake between application and serial + * interface of heat pump 1. Sending request bytes , e.g.: 01 00 FD + * FC 10 03 for version request 01 -> header start 00 -> get request + * FD -> checksum of request FC -> request byte 10 03 -> Footer + * ending the communication + * + * 2. Receive a data available 10 -> ok 02 -> it does have + * data,which wants to send now + * + * 3. acknowledge sending data 10 -> ok + * + * 4. receive data until footer 01 -> header start 00 -> get request + * CC -> checksum of send data FD -> request byte 00 CE -> data , + * e.g. short value as 2 bytes -> 206 -> 2.06 version 10 03 -> + * Footer ending the communication + */ + private byte[] getData(byte request[]) { + if (!establishRequest(request)) { + return new byte[0]; + } + try { + connector.write(DataParser.ESCAPE); + byte[] response = receiveData(); + return response; + } catch (Exception e) { + logger.error("Could not get data from heat pump! {}", e.toString()); + return buffer; + } + } + + /** + * Sets data to connected heat pump + * + * @param request + * request bytes to send to heat pump + * @return response bytes from heat pump + * + * General overview of handshake between application and serial + * interface of heat pump + * + * 1. Sending request bytes, e.g update time in heat pump 01 -> + * header start 80 -> set request F1 -> checksum of request FC -> + * request byte 00 02 0a 22 1b 0e 00 03 1a -> new values according + * record definition for time 10 03 -> Footer ending the + * communication + * + * 2. Receive response message the confirmation message is ready for + * sending 10 -> ok 02 -> it does have data ,which wants to send now + * + * 3. acknowledge sending data 10 -> ok + * + * 4. receive confirmation message until footer 01 -> header start + * 80 -> set request 7D -> checksum of send data FC -> request byte + * 10 03 -> Footer ending the communication + */ + private byte[] setData(byte[] request) throws StiebelHeatPumpException { + try { + startCommunication(); + establishRequest(request); + // Acknowledge sending data + connector.write(DataParser.ESCAPE); + + } catch (Exception e) { + logger.error("Could not set data to heat pump! {}", e.toString()); + return new byte[0]; + } + + // finally receive data + return receiveData(); + } + + /** + * This method start the communication for the request It send the initial + * handshake and expects a response + */ + private void startCommunication() throws StiebelHeatPumpException { + logger.debug("Sending start communication"); + byte response; + try { + connector.write(DataParser.STARTCOMMUNICATION); + response = connector.get(); + } catch (Exception e) { + logger.error("heat pump communication could not be established !"); + throw new StiebelHeatPumpException("heat pump communication could not be established !"); + } + if (response != DataParser.ESCAPE) { + logger.warn("heat pump is communicating, but did not receive Escape message in initial handshake!"); + throw new StiebelHeatPumpException( + "heat pump is communicating, but did not receive Escape message in initial handshake!"); + } + } + + /** + * This method establish the connection for the request It send the request + * and expects a data available response + * + * @param request + * to be send to heat pump + * @return true if data are available from heatpump + */ + private boolean establishRequest(byte[] request) { + int numBytesReadTotal = 0; + boolean dataAvailable = false; + int requestRetry = 0; + int retry = 0; + try { + while (requestRetry < MAXRETRIES) { + connector.write(request); + retry = 0; + byte singleByte; + while ((!dataAvailable) & (retry < MAXRETRIES)) { + try { + singleByte = connector.get(); + } catch (Exception e) { + retry++; + continue; + } + buffer[numBytesReadTotal] = singleByte; + numBytesReadTotal++; + if (buffer[0] != DataParser.DATAAVAILABLE[0] || buffer[1] != DataParser.DATAAVAILABLE[1]) { + continue; + } + dataAvailable = true; + return true; + } + logger.debug("retry request!"); + startCommunication(); + } + if (!dataAvailable) { + logger.warn("heat pump has no data available for request!"); + return false; + } + } catch (Exception e1) { + logger.error("Could not get data from heat pump! {}", e1.toString()); + return false; + } + return true; + } + + /** + * This method receive the response from the heat pump It receive single + * bytes until the end of message s detected + * + * @return bytes representing the data send from heat pump + */ + private byte[] receiveData() { + byte singleByte; + int numBytesReadTotal; + int retry; + buffer = new byte[INPUT_BUFFER_LENGTH]; + retry = 0; + numBytesReadTotal = 0; + boolean endOfMessage = false; + + while (!endOfMessage & retry < MAXRETRIES) { + try { + singleByte = connector.get(); + } catch (Exception e) { + // reconnect and try again to send request + retry++; + continue; + } + + buffer[numBytesReadTotal] = singleByte; + numBytesReadTotal++; + + if (numBytesReadTotal > 4 && buffer[numBytesReadTotal - 2] == DataParser.ESCAPE + && buffer[numBytesReadTotal - 1] == DataParser.END) { + // we have reached the end of the response + endOfMessage = true; + logger.debug("reached end of response message."); + break; + } + } + + byte[] responseBuffer = new byte[numBytesReadTotal]; + System.arraycopy(buffer, 0, responseBuffer, 0, numBytesReadTotal); + return responseBuffer; + } + + /** + * This creates the request message ready to be send to heat pump + * + * @param request + * object containing necessary information to build request + * message + * @return request message byte[] + */ + private byte[] createRequestMessage(Request request) { + short checkSum; + byte[] requestMessage = new byte[] { DataParser.HEADERSTART, DataParser.GET, (byte) 0x00, + request.getRequestByte(), DataParser.ESCAPE, DataParser.END }; + try { + // prepare request message + checkSum = parser.calculateChecksum(requestMessage); + requestMessage[2] = parser.shortToByte(checkSum)[0]; + requestMessage = parser.addDuplicatedBytes(requestMessage); + } catch (StiebelHeatPumpException e) { + } + return requestMessage; + } } diff --git a/bundles/binding/org.openhab.binding.stiebelheatpump/src/main/java/org/openhab/binding/stiebelheatpump/internal/StiebelHeatPumpBinding.java b/bundles/binding/org.openhab.binding.stiebelheatpump/src/main/java/org/openhab/binding/stiebelheatpump/internal/StiebelHeatPumpBinding.java index c57b24fd42d..2ab7f593624 100644 --- a/bundles/binding/org.openhab.binding.stiebelheatpump/src/main/java/org/openhab/binding/stiebelheatpump/internal/StiebelHeatPumpBinding.java +++ b/bundles/binding/org.openhab.binding.stiebelheatpump/src/main/java/org/openhab/binding/stiebelheatpump/internal/StiebelHeatPumpBinding.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2015, openHAB.org and others. + * Copyright (c) 2010-2016, openHAB.org and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -35,389 +35,376 @@ /** * Stiebel heat pump binding implementation. - * + * * @author Peter Kreutzer * @since 1.5.0 */ -public class StiebelHeatPumpBinding extends - AbstractActiveBinding implements - ManagedService { - - private static final Logger logger = LoggerFactory - .getLogger(StiebelHeatPumpBinding.class); - - // configuration defaults for optional properties - private static int DEFAULT_BAUD_RATE = 9600; - private static int DEFAULT_SERIAL_TIMEOUT = 5; - - /** - * the refresh interval which is used to poll values from the heat pump - * server (optional, defaults to 1 Minute) - */ - private long refreshInterval = 60000; // in ms - - /** timeout for the serial port */ - private int serialTimeout = DEFAULT_SERIAL_TIMEOUT; - - /** version number of heat pump */ - private String version = ""; - - /** heat pump request definition */ - private List heatPumpConfiguration = new ArrayList(); - - // ** request to get the version of the heat pump - Request versionRequest; - - // ** indicates if the communication is currently in use by a call - boolean communicationInUse = false; - - private ProtocolConnector connector; - - public StiebelHeatPumpBinding() { - } - - @Override - public void activate() { - - } - - @Override - public void deactivate() { - - } - - /** - * @{inheritDoc - */ - @Override - protected long getRefreshInterval() { - return refreshInterval; - } - - /** - * @{inheritDoc - */ - @Override - protected String getName() { - return "stiebelheatpump Refresh Service"; - } - - /** - * @{inheritDoc - */ - @Override - protected void execute() { - - if (communicationInUse) - return; - - logger.debug("Refresh heat pump sensor and status values ..."); - communicationInUse = true; - CommunicationService communicationService = null; - - try { - communicationService = new CommunicationService(connector, heatPumpConfiguration); - Map data = new HashMap(); - Map allData = new HashMap(); - - data = communicationService.getStatus(); - allData.putAll(data); - for (Map.Entry entry : data.entrySet()) { - logger.debug("Data {} has value {}", entry.getKey(), - entry.getValue()); - } - data = communicationService.getSensors(); - allData.putAll(data); - for (Map.Entry entry : data.entrySet()) { - logger.debug("Data {} has value {}", entry.getKey(), - entry.getValue()); - } - - publishValues(allData); - } catch (StiebelHeatPumpException e) { - logger.error("Could not read data from heat pump! " + e.toString()); - } finally { - communicationService.finalizer(); - communicationInUse = false; - } - } - - /** - * @{inheritDoc - */ - @Override - protected void internalReceiveCommand(String itemName, Command command) { - logger.debug("Received command {} for item {}", command, itemName); - - int retry = 0; - while (communicationInUse & (retry < DEFAULT_SERIAL_TIMEOUT)) { - try { - Thread.sleep(CommunicationService.WAITING_TIME_BETWEEN_REQUESTS); - } catch (InterruptedException e) { - logger.debug("Could not get access to heatpump ! : {}", - e.toString()); - } - retry++; - } - if (communicationInUse) - return; - - for (StiebelHeatPumpBindingProvider provider : providers) { - for (String name : provider.getItemNames()) { - if (!name.equals(itemName)) { - continue; - } - String parameter = provider.getParameter(name); - logger.debug( - "Found item {} with heat pump parameter {} in providers", - itemName, parameter); - try { - Map data = new HashMap(); - communicationInUse = true; - CommunicationService communicationService = new CommunicationService(connector, heatPumpConfiguration); - data = communicationService.setData(command.toString(), - parameter); - - communicationService.finalizer(); - - publishValues(data); - } catch (StiebelHeatPumpException e) { - logger.error("Could not set new value!"); - } finally { - communicationInUse = false; - } - } - } - } - - /** - * @{inheritDoc - */ - @Override - protected void internalReceiveUpdate(String itemName, State newState) { - // the code being executed when a state was sent on the openHAB - // event bus goes here. This method is only called if one of the - // BindingProviders provide a binding for the given 'itemName'. - // logger.debug("internalReceiveUpdate() is called!"); - // logger.debug("Received update {} for item {}", newState, itemName); - - } - - /** - * @{inheritDoc - */ - @Override - public void updated(Dictionary config) - throws ConfigurationException { - - String serialPort = null; - int baudRate = DEFAULT_BAUD_RATE; - serialTimeout = DEFAULT_SERIAL_TIMEOUT; - String host = null; - int port = 0; - - logger.debug("Loading stiebelheatpump binding configuration."); - - if (config == null || config.isEmpty()) { - logger.warn("Empty or null configuration. Ignoring."); - return; - } - - if (config != null) { - // to override the default refresh interval one has to add a - // parameter to openhab.cfg like - // :refresh= - if (StringUtils.isNotBlank((String) config.get("refresh"))) { - refreshInterval = Long - .parseLong((String) config.get("refresh")); - } - if (StringUtils.isNotBlank((String) config.get("serialPort"))) { - serialPort = (String) config.get("serialPort"); - } - if (StringUtils.isNotBlank((String) config.get("baudRate"))) { - baudRate = Integer.parseInt((String) config.get("baudRate")); - } - if (StringUtils.isNotBlank((String) config.get("host"))) { - host = (String) config.get("host"); - } - if (StringUtils.isNotBlank((String) config.get("port"))) { - port = Integer.parseInt((String) config.get("port")); - } - if (StringUtils.isNotBlank((String) config.get("serialTimeout"))) { - serialTimeout = Integer.parseInt((String) config - .get("serialTimeout")); - } - if (StringUtils.isNotBlank((String) config.get("version"))) { - version = (String) config.get("version"); - } - try { - if (host != null) { - this.connector = new TcpConnector(host, port); - } else { - this.connector = new SerialPortConnector(serialPort, baudRate); - } - boolean isInitialized = getInitialHeatPumpSettings(); - setTime(); - if (host != null) { - logger.info( - "Created heatpump configuration with tcp {}:{}, version:{} ", - host, port, version); - } else { - logger.info( - "Created heatpump configuration with serialport:{}, baudrate:{}, version:{} ", - serialPort, baudRate, version); - } - setProperlyConfigured(isInitialized); - } catch (RuntimeException e) { - logger.warn(e.getMessage(), e); - throw e; - } - - } - } - - /** - * This method reads initially all information from the heat pump It read - * the configuration file and loads all defined record definitions of sensor - * data, status information , actual time settings and setting parameter - * values. - * - * @return true if heat pump information could be successfully read - */ - public boolean getInitialHeatPumpSettings() { - CommunicationService communicationService = null; - try { - - int retry = 0; - while (communicationInUse) { - try { - Thread.sleep(CommunicationService.WAITING_TIME_BETWEEN_REQUESTS); - retry++; - if (retry>DEFAULT_SERIAL_TIMEOUT) - { - return false; - } - } catch (InterruptedException e) { - logger.error("could not access Heat pump for has version {}", version); - } - } - - communicationInUse = true; - - communicationService = new CommunicationService(connector); - Map data = new HashMap(); - Map allData = new HashMap(); - - heatPumpConfiguration = communicationService - .getHeatPumpConfiguration(version + ".xml"); - String version = communicationService.getversion(); - logger.info("Heat pump has version {}", version); - allData.put("Version", version); - - data = communicationService.getSettings(); - allData.putAll(data); - - data = communicationService.getStatus(); - allData.putAll(data); - - data = communicationService.getSensors(); - allData.putAll(data); - - for (Map.Entry entry : allData.entrySet()) { - logger.debug("Data {} has value {}", entry.getKey(), - entry.getValue()); - } - - - - publishValues(allData); - - return true; - } catch (StiebelHeatPumpException e) { - logger.error("Stiebel heatpump version could not be read from heat pump! " - + e.toString()); - } finally{ - communicationInUse = false; - if (communicationService != null) { - communicationService.finalizer(); - } - } - - return false; - } - - /** - * This method sets the time in the heat pump. - * I case of the time the time is initially verified and set to - * actual time. - * - * @return true if heat pump time could be successfully set - */ - public boolean setTime() { - CommunicationService communicationService = null; - try { - - int retry = 0; - while (communicationInUse) { - try { - Thread.sleep(CommunicationService.WAITING_TIME_BETWEEN_REQUESTS); - retry++; - if (retry>DEFAULT_SERIAL_TIMEOUT) - { - return false; - } - } catch (InterruptedException e) { - logger.error("could not access Heat pump for has version {}", version); - } - } - communicationInUse = true; - communicationService = new CommunicationService(connector, heatPumpConfiguration); - communicationService.setTime(); - - return true; - } catch (StiebelHeatPumpException e) { - logger.error("Stiebel heatpump time could not be set on heat pump! " - + e.toString()); - } finally{ - communicationInUse = false; - if (communicationService != null) { - communicationService.finalizer(); - } - } - - return false; - } - - /** - * This method publishes all values on the event bus - * - * @param heatPumpData - * as map of provider parameter and value - */ - private void publishValues(Map heatPumpData) { - for (StiebelHeatPumpBindingProvider provider : providers) { - publishForProvider(heatPumpData, provider); - } - } - - private void publishForProvider(Map heatPumpData, - StiebelHeatPumpBindingProvider provider) { - for (String itemName : provider.getItemNames()) { - String parameter = provider.getParameter(itemName); - if (parameter != null && heatPumpData.containsKey(parameter)) { - publishItem(itemName, heatPumpData.get(parameter), provider.getItemType(itemName)); - } - } - } - - private void publishItem(String itemName, String heatpumpValue, - Class itemType) { - if (itemType.isAssignableFrom(NumberItem.class)) { - eventPublisher.postUpdate(itemName, new DecimalType(heatpumpValue)); - } - if (itemType.isAssignableFrom(StringItem.class)) { - eventPublisher.postUpdate(itemName, new StringType(heatpumpValue)); - } - } +public class StiebelHeatPumpBinding extends AbstractActiveBinding + implements ManagedService { + + private static final Logger logger = LoggerFactory.getLogger(StiebelHeatPumpBinding.class); + + // configuration defaults for optional properties + private static int DEFAULT_BAUD_RATE = 9600; + private static int DEFAULT_SERIAL_TIMEOUT = 5; + + /** + * the refresh interval which is used to poll values from the heat pump + * server (optional, defaults to 1 Minute) + */ + private long refreshInterval = 60000; // in ms + + /** timeout for the serial port */ + private int serialTimeout = DEFAULT_SERIAL_TIMEOUT; + + /** version number of heat pump */ + private String version = ""; + + /** heat pump request definition */ + private List heatPumpConfiguration = new ArrayList(); + + // ** request to get the version of the heat pump + Request versionRequest; + + // ** indicates if the communication is currently in use by a call + boolean communicationInUse = false; + + private ProtocolConnector connector; + + public StiebelHeatPumpBinding() { + } + + @Override + public void activate() { + + } + + @Override + public void deactivate() { + + } + + /** + * @{inheritDoc + */ + @Override + protected long getRefreshInterval() { + return refreshInterval; + } + + /** + * @{inheritDoc + */ + @Override + protected String getName() { + return "stiebelheatpump Refresh Service"; + } + + /** + * @{inheritDoc + */ + @Override + protected void execute() { + + if (communicationInUse) { + return; + } + + logger.debug("Refresh heat pump sensor and status values ..."); + communicationInUse = true; + CommunicationService communicationService = null; + + try { + communicationService = new CommunicationService(connector, heatPumpConfiguration); + Map data = new HashMap(); + Map allData = new HashMap(); + + data = communicationService.getStatus(); + allData.putAll(data); + for (Map.Entry entry : data.entrySet()) { + logger.debug("Data {} has value {}", entry.getKey(), entry.getValue()); + } + data = communicationService.getSensors(); + allData.putAll(data); + for (Map.Entry entry : data.entrySet()) { + logger.debug("Data {} has value {}", entry.getKey(), entry.getValue()); + } + + publishValues(allData); + } catch (StiebelHeatPumpException e) { + logger.error("Could not read data from heat pump! " + e.toString()); + } finally { + communicationService.finalizer(); + communicationInUse = false; + } + } + + /** + * @{inheritDoc + */ + @Override + protected void internalReceiveCommand(String itemName, Command command) { + logger.debug("Received command {} for item {}", command, itemName); + + int retry = 0; + while (communicationInUse & (retry < DEFAULT_SERIAL_TIMEOUT)) { + try { + Thread.sleep(CommunicationService.WAITING_TIME_BETWEEN_REQUESTS); + } catch (InterruptedException e) { + logger.debug("Could not get access to heatpump ! : {}", e.toString()); + } + retry++; + } + if (communicationInUse) { + return; + } + + for (StiebelHeatPumpBindingProvider provider : providers) { + for (String name : provider.getItemNames()) { + if (!name.equals(itemName)) { + continue; + } + String parameter = provider.getParameter(name); + logger.debug("Found item {} with heat pump parameter {} in providers", itemName, parameter); + try { + Map data = new HashMap(); + communicationInUse = true; + CommunicationService communicationService = new CommunicationService(connector, + heatPumpConfiguration); + data = communicationService.setData(command.toString(), parameter); + + communicationService.finalizer(); + + publishValues(data); + } catch (StiebelHeatPumpException e) { + logger.error("Could not set new value!"); + } finally { + communicationInUse = false; + } + } + } + } + + /** + * @{inheritDoc + */ + @Override + protected void internalReceiveUpdate(String itemName, State newState) { + // the code being executed when a state was sent on the openHAB + // event bus goes here. This method is only called if one of the + // BindingProviders provide a binding for the given 'itemName'. + // logger.debug("internalReceiveUpdate() is called!"); + // logger.debug("Received update {} for item {}", newState, itemName); + + } + + protected void addBindingProvider(StiebelHeatPumpBindingProvider bindingProvider) { + super.addBindingProvider(bindingProvider); + } + + protected void removeBindingProvider(StiebelHeatPumpBindingProvider bindingProvider) { + super.removeBindingProvider(bindingProvider); + } + + /** + * {@inheritDoc} + */ + @Override + public void updated(Dictionary config) throws ConfigurationException { + + String serialPort = null; + int baudRate = DEFAULT_BAUD_RATE; + serialTimeout = DEFAULT_SERIAL_TIMEOUT; + String host = null; + int port = 0; + + logger.debug("Loading stiebelheatpump binding configuration."); + + if (config == null || config.isEmpty()) { + logger.warn("Empty or null configuration. Ignoring."); + return; + } + + if (config != null) { + // to override the default refresh interval one has to add a + // parameter to openhab.cfg like + // :refresh= + if (StringUtils.isNotBlank((String) config.get("refresh"))) { + refreshInterval = Long.parseLong((String) config.get("refresh")); + } + if (StringUtils.isNotBlank((String) config.get("serialPort"))) { + serialPort = (String) config.get("serialPort"); + } + if (StringUtils.isNotBlank((String) config.get("baudRate"))) { + baudRate = Integer.parseInt((String) config.get("baudRate")); + } + if (StringUtils.isNotBlank((String) config.get("host"))) { + host = (String) config.get("host"); + } + if (StringUtils.isNotBlank((String) config.get("port"))) { + port = Integer.parseInt((String) config.get("port")); + } + if (StringUtils.isNotBlank((String) config.get("serialTimeout"))) { + serialTimeout = Integer.parseInt((String) config.get("serialTimeout")); + } + if (StringUtils.isNotBlank((String) config.get("version"))) { + version = (String) config.get("version"); + } + try { + if (host != null) { + this.connector = new TcpConnector(host, port); + } else { + this.connector = new SerialPortConnector(serialPort, baudRate); + } + boolean isInitialized = getInitialHeatPumpSettings(); + setTime(); + if (host != null) { + logger.info("Created heatpump configuration with tcp {}:{}, version:{} ", host, port, version); + } else { + logger.info("Created heatpump configuration with serialport:{}, baudrate:{}, version:{} ", + serialPort, baudRate, version); + } + setProperlyConfigured(isInitialized); + } catch (RuntimeException e) { + logger.warn(e.getMessage(), e); + throw e; + } + + } + } + + /** + * This method reads initially all information from the heat pump It read + * the configuration file and loads all defined record definitions of sensor + * data, status information , actual time settings and setting parameter + * values. + * + * @return true if heat pump information could be successfully read + */ + public boolean getInitialHeatPumpSettings() { + CommunicationService communicationService = null; + try { + + int retry = 0; + while (communicationInUse) { + try { + Thread.sleep(CommunicationService.WAITING_TIME_BETWEEN_REQUESTS); + retry++; + if (retry > DEFAULT_SERIAL_TIMEOUT) { + return false; + } + } catch (InterruptedException e) { + logger.error("could not access Heat pump for version {}", version); + } + } + + communicationInUse = true; + + communicationService = new CommunicationService(connector); + Map data = new HashMap(); + Map allData = new HashMap(); + + heatPumpConfiguration = communicationService.getHeatPumpConfiguration(version + ".xml"); + String version = communicationService.getversion(); + logger.info("Heat pump has version {}", version); + allData.put("Version", version); + + data = communicationService.getSettings(); + allData.putAll(data); + + data = communicationService.getStatus(); + allData.putAll(data); + + data = communicationService.getSensors(); + allData.putAll(data); + + for (Map.Entry entry : allData.entrySet()) { + logger.debug("Data {} has value {}", entry.getKey(), entry.getValue()); + } + + publishValues(allData); + + return true; + } catch (StiebelHeatPumpException e) { + logger.error("Stiebel heatpump version could not be read from heat pump! " + e.toString()); + } finally { + communicationInUse = false; + if (communicationService != null) { + communicationService.finalizer(); + } + } + + return false; + } + + /** + * This method sets the time in the heat pump. + * I case of the time the time is initially verified and set to + * actual time. + * + * @return true if heat pump time could be successfully set + */ + public boolean setTime() { + CommunicationService communicationService = null; + try { + + int retry = 0; + while (communicationInUse) { + try { + Thread.sleep(CommunicationService.WAITING_TIME_BETWEEN_REQUESTS); + retry++; + if (retry > DEFAULT_SERIAL_TIMEOUT) { + return false; + } + } catch (InterruptedException e) { + logger.error("could not access Heat pump for has version {}", version); + } + } + communicationInUse = true; + communicationService = new CommunicationService(connector, heatPumpConfiguration); + communicationService.setTime(); + + return true; + } catch (StiebelHeatPumpException e) { + logger.error("Stiebel heatpump time could not be set on heat pump! " + e.toString()); + } finally { + communicationInUse = false; + if (communicationService != null) { + communicationService.finalizer(); + } + } + + return false; + } + + /** + * This method publishes all values on the event bus + * + * @param heatPumpData + * as map of provider parameter and value + */ + private void publishValues(Map heatPumpData) { + for (StiebelHeatPumpBindingProvider provider : providers) { + publishForProvider(heatPumpData, provider); + } + } + + private void publishForProvider(Map heatPumpData, StiebelHeatPumpBindingProvider provider) { + for (String itemName : provider.getItemNames()) { + String parameter = provider.getParameter(itemName); + if (parameter != null && heatPumpData.containsKey(parameter)) { + publishItem(itemName, heatPumpData.get(parameter), provider.getItemType(itemName)); + } + } + } + + private void publishItem(String itemName, String heatpumpValue, Class itemType) { + if (itemType.isAssignableFrom(NumberItem.class)) { + eventPublisher.postUpdate(itemName, new DecimalType(heatpumpValue)); + } + if (itemType.isAssignableFrom(StringItem.class)) { + eventPublisher.postUpdate(itemName, new StringType(heatpumpValue)); + } + } } diff --git a/bundles/binding/org.openhab.binding.stiebelheatpump/src/main/java/org/openhab/binding/stiebelheatpump/protocol/DataParser.java b/bundles/binding/org.openhab.binding.stiebelheatpump/src/main/java/org/openhab/binding/stiebelheatpump/protocol/DataParser.java index 26f0036b8e8..e601029b2f2 100644 --- a/bundles/binding/org.openhab.binding.stiebelheatpump/src/main/java/org/openhab/binding/stiebelheatpump/protocol/DataParser.java +++ b/bundles/binding/org.openhab.binding.stiebelheatpump/src/main/java/org/openhab/binding/stiebelheatpump/protocol/DataParser.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2015, openHAB.org and others. + * Copyright (c) 2010-2016, openHAB.org and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -22,15 +22,15 @@ /** * Class for parse data packets from Stiebel heat pumps - * + * * @author Peter Kreutzer * @param * original protocol parser was written by Robert Penz in python * @since 1.5.0 - * + * * Each response has the same structure as request header (four bytes), * optional data and footer: - * + * * Header: 01 Read/Write: 00 for Read (get) response, 80 for Write (set) * response; in case of error during command exchange device stores error * code here; know error code : 03 = unknown command Checksum: ? 1 byte - @@ -40,580 +40,545 @@ */ public class DataParser { - private static final Logger logger = LoggerFactory - .getLogger(DataParser.class); - - public static byte ESCAPE = (byte) 0x10; - public static byte HEADERSTART = (byte) 0x01; - public static byte END = (byte) 0x03; - public static byte GET = (byte) 0x00; - public static byte SET = (byte) 0x80; - public static byte STARTCOMMUNICATION = (byte) 0x02; - public static byte[] FOOTER = { ESCAPE, END }; - public static byte[] DATAAVAILABLE = { ESCAPE, STARTCOMMUNICATION }; - - final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); - - public List parserConfiguration = new ArrayList(); - - public DataParser() { - } - - /** - * verifies response on availability of data - * - * @param response - * of heat pump - * @param request - * request defined for heat pump response - * @return Map of Strings with name and values - */ - public Map parseRecords(final byte[] response, - Request request) throws StiebelHeatPumpException { - - Map map = new HashMap(); - - logger.debug("Parse bytes: {}", DataParser.bytesToHex(response)); - - if (response.length < 2) { - logger.error("response does not have a valid length ogf bytes: {}", - DataParser.bytesToHex(response)); - return map; - } - - // parse response and fill map - for (RecordDefinition recordDefinition : request.getRecordDefinitions()) { - try { - String value = parseRecord(response, recordDefinition); - logger.debug("Parsed value {} -> {} with pos: {} , len: {}", - recordDefinition.getName(), value, - recordDefinition.getPosition(), - recordDefinition.getLength()); - map.put(recordDefinition.getName(), value); - } catch (StiebelHeatPumpException e) { - continue; - } - } - return map; - } - - /** - * parses a single record - * - * @param response - * of heat pump - * @param RecordDefinition - * that shall be used for parsing the heat pump response - * @return string value of the parse response - * @throws StiebelHeatPumpException - */ - public String parseRecord(byte[] response, RecordDefinition recordDefinition) - throws StiebelHeatPumpException { - try { - if (response.length < 2) { - logger.error( - "response does not have a valid length of bytes: {}", - DataParser.bytesToHex(response)); - throw new StiebelHeatPumpException(); - } - ByteBuffer buffer = ByteBuffer.wrap(response); - short number = 0; - byte[] bytes = null; - - switch (recordDefinition.getLength()) { - case 1: - bytes = new byte[1]; - System.arraycopy(response, recordDefinition.getPosition(), - bytes, 0, 1); - number = Byte - .valueOf(buffer.get(recordDefinition.getPosition())); - break; - case 2: - bytes = new byte[2]; - System.arraycopy(response, recordDefinition.getPosition(), - bytes, 0, 2); - number = buffer - .getShort(recordDefinition.getPosition()); - break; - } - - if (recordDefinition.getBitPosition() > 0) { - - int returnValue = getBit(bytes, - recordDefinition.getBitPosition()); - return String.valueOf(returnValue); - } - - if (recordDefinition.getScale() != 1.0) { - double myDoubleNumber = number * recordDefinition.getScale(); - myDoubleNumber = Math.round(myDoubleNumber * 100.0) / 100.0; - String returnString = String.format("%s", myDoubleNumber); - return returnString; - } - - return String.valueOf(number); - } catch (Exception e) { - logger.error( - "response {} could not be parsed for record definition {} ", - DataParser.bytesToHex(response), recordDefinition.getName()); - throw new StiebelHeatPumpException(); - } - } - - /** - * composes the new value of a record definition into a updated set command - * that can be send back to heat pump - * - * @param response - * of heat pump that should be updated with new value - * @param RecordDefinition - * that shall be used for compose the new value into the heat - * pump set command - * @param string - * value to be compose - * @return byte[] ready to send to heat pump - * @throws StiebelHeatPumpException - */ - public byte[] composeRecord(String value, byte[] response, - RecordDefinition recordDefinition) throws StiebelHeatPumpException { - short newValue = 0; - - if (recordDefinition.getDataType() != Type.Settings) { - logger.warn( - "The record {} can not be set as it is not a setable value!", - recordDefinition.getName()); - throw new StiebelHeatPumpException("record is not a setting!"); - } - - double number = Double.parseDouble(value); - - if (number > recordDefinition.getMax() - || number < recordDefinition.getMin()) { - logger.warn( - "The record {} can not be set to value {} as allowed range is {}<-->{} !", - recordDefinition.getName(), value, - recordDefinition.getMax(), recordDefinition.getMin()); - throw new StiebelHeatPumpException("invalid value !"); - } - - // change response byte to setting command - response[1] = SET; - - // reverse the scale - if (recordDefinition.getScale() != 1.0) { - number = number / recordDefinition.getScale(); - newValue = (short) number; - } - - // set new bit values in a byte - if (recordDefinition.getBitPosition() > 0) { - - byte[] abyte = new byte[] { response[recordDefinition.getPosition()] }; - abyte = setBit(abyte, recordDefinition.getBitPosition(), newValue); - response[recordDefinition.getPosition()] = abyte[0]; - return response; - } - - // create byte values for single and double byte values - // and update response - switch (recordDefinition.getLength()) { - case 1: - byte newByteValue = (byte) number; - response[recordDefinition.getPosition()] = newByteValue; - break; - case 2: - byte[] newByteValues = shortToByte(newValue); - int position = recordDefinition.getPosition(); - response[position] = newByteValues[1]; - response[position + 1] = newByteValues[0]; - break; - } - - response[2] = this.calculateChecksum(response); - response = this.addDuplicatedBytes(response); - logger.debug("Updated record {} at position {} to value {}.", - recordDefinition.getName(), recordDefinition.getPosition(), - value); - return response; - } - - /** - * verifies response on availability of data - * - * @param response - * of heat pump - * @return true if the response of the heat pump indicates availability of - * data - */ - public boolean dataAvailable(byte[] response) - throws StiebelHeatPumpException { - - if (response.length == 0 || response.length > 2) { - throw new StiebelHeatPumpException( - "invalid response length on request of data " - + new String(response)); - } - - if (response[0] != ESCAPE) { - throw new StiebelHeatPumpException( - "invalid response on request of data " - + new String(response)); - } - if (response.length == 2 && response[1] == DATAAVAILABLE[1]) { - return true; - } - - return false; - } - - /** - * verifies the header of the heat pump response - * - * @param response - * of heat pump - */ - public void verifyHeader(byte[] response) throws StiebelHeatPumpException { - - if (response.length < 4) { - throw new StiebelHeatPumpException( - "invalide response length on request of data " - + new String(response)); - } - - if (response[0] != HEADERSTART) { - throw new StiebelHeatPumpException( - "invalid response on request of data, found no header start: " - + new String(response)); - } - - if (response[1] != GET & response[1] != SET) { - throw new StiebelHeatPumpException( - "invalid response on request of data, response is neither get nor set: " - + new String(response)); - } - - if (response[2] != calculateChecksum(response)) { - throw new StiebelHeatPumpException( - "invalid checksum on request of data " - + new String(response)); - } - } - - /** - * verifies the header of the heat pump response - * - * @param response - * of heat pump - * @return true if header is valid - */ - public boolean headerCheck(byte[] response) { - try { - verifyHeader(response); - } catch (StiebelHeatPumpException e) { - logger.debug("verification of response failed " + e.toString()); - return false; - } - - return true; - } - - /** - * verifies the heat pump response after data has been updated - * - * @param response - * of heat pump - * @return true if data set has been confirmed - */ - public boolean setDataCheck(byte[] response) { - try { - verifyHeader(response); - } catch (StiebelHeatPumpException e) { - return false; - } - - return true; - } - - /** - * calculates the checksum of a byte data array - * - * @param data - * to calculate the checksum for - * @param withReplace - * to set if the byte array shall be corrected by special replace - * method - * @return calculated checksum as short - */ - public byte calculateChecksum(byte[] data) throws StiebelHeatPumpException { - - if (data.length < 5) { - throw new StiebelHeatPumpException( - "no valid byte[] for calulation of checksum!"); - } - - int checkSum = 0, i = 0; - for (i = 0; i < data.length - 2; i++) { - if (i == 2) { - continue; - } - checkSum += (short) (data[i] & 0xFF); - } - - return shortToByte((short) checkSum)[0]; - } - - /** - * converts short to byte[] - * - * @return array of bytes - */ - public byte[] shortToByte(short value) { - byte[] returnByteArray = new byte[2]; - returnByteArray[0] = (byte) (value & 0xff); - returnByteArray[1] = (byte) ((value >> 8) & 0xff); - - return returnByteArray; - } - - /** - * converts integer to byte[] - * - * @return array of bytes - */ - public byte[] intToByte(int checkSum) { - byte[] returnByteArray = ByteBuffer.allocate(4).putInt(checkSum) - .array(); - return returnByteArray; - } - - /** - * converts byte to short - * - * @return short - */ - private short byteToShort(byte[] bytes) throws StiebelHeatPumpException { - return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getShort(); - } - - /** - * Search the data byte array for the first occurrence of the byte array - * pattern. raw data received from device have to be de-escaped before - * header evaluation and data use: - each sequence 2B 18 must be replaced - * with single byte 2B - each sequence 10 10 must be replaced with single - * byte 10 - * - * @param data - * as byte array representing response from heat pump which shall - * be fixed - * @return byte array with fixed byte entries - */ - public byte[] fixDuplicatedBytes(byte[] data) { - - // first copy the data except the last 2 bytes , the footer - byte[] bytesToBeAnalyzed = new byte[data.length-2]; - System.arraycopy(data, 0, bytesToBeAnalyzed, 0, data.length-2); - - byte[] fixedData = findReplace(bytesToBeAnalyzed, new byte[] { (byte) 0x10, - (byte) 0x10 }, new byte[] { (byte) 0x10 }); - fixedData = findReplace(fixedData, new byte[] { (byte) 0x2b, (byte) 0x18 }, - new byte[] { (byte) 0x2b }); - - byte[] result = new byte[fixedData.length + FOOTER.length]; - // copy fixedData to result - System.arraycopy(fixedData, 0, result, 0, fixedData.length); - // copy footer to result - System.arraycopy(FOOTER, 0, result, fixedData.length, FOOTER.length); - - return result; - } - - /** - * Search the data byte array for the first occurrence of the byte array - * pattern. raw data received from device have to be de-escaped before - * header evaluation and data use: - each sequence 2B must be replaced with - * single byte 2B 18 - each sequence 10 must be replaced with single byte 10 - * 10 - * - * @param data - * as byte array representing response from heat pump which shall - * be fixed - * @return byte array with fixed byte entries - */ - public byte[] addDuplicatedBytes(byte[] data) { - ByteBuffer byteBuffer = ByteBuffer.allocate(data.length * 2); - - // add header without changes - for (int i = 0; i < 2; i++) { - byteBuffer.put(data[i]); - } - - // add now duplicates - for (int i = 2; i < data.length - 2; i++) { - byteBuffer.put(data[i]); - if (data[i] == (byte) 0x10) { - byteBuffer.put(data[i]); - } - if (data[i] == (byte) 0x2b) { - byteBuffer.put((byte) 0x18); - } - } - - // add footer without changes - for (int i = data.length - 2; i < data.length; i++) { - byteBuffer.put(data[i]); - } - - byte[] newdata = new byte[byteBuffer.position()]; - byteBuffer.rewind(); - byteBuffer.get(newdata); - - return newdata; - } - - /** - * Search the data byte array for the first occurrence of the byte array - * pattern. - * - * @param data - * as byte array to search into and to replace the pattern bytes - * with replace bytes - * @param pattern - * as byte array to search for - * @param replace - * as byte array to replace with - * @return byte array which has pattern bytes been replaced with replace - * bytes - */ - public byte[] findReplace(byte[] data, byte[] pattern, byte[] replace) { - - int position = indexOf(data, pattern); - while (position >= 0) { - byte[] newData = new byte[data.length - pattern.length - + replace.length]; - System.arraycopy(data, 0, newData, 0, position); - System.arraycopy(replace, 0, newData, position, replace.length); - System.arraycopy(data, position + pattern.length, newData, position - + replace.length, data.length - position - pattern.length); - position = indexOf(newData, pattern); - data = new byte[newData.length]; - System.arraycopy(newData, 0, data, 0, newData.length); - } - return data; - } - - /** - * Search the data byte array for the first occurrence of the byte array - * pattern. - * - * @param data - * to find pattern in - * @param pattern - * to be searched - * @return byte number were pattern was found in data - */ - private int indexOf(byte[] data, byte[] pattern) { - int[] failure = computeFailure(pattern); - int j = 0; - for (int i = 0; i < data.length; i++) { - while (j > 0 && pattern[j] != data[i]) { - j = failure[j - 1]; - } - if (pattern[j] == data[i]) { - j++; - } - if (j == pattern.length) { - return i - pattern.length + 1; - } - } - return -1; - } - - /** - * Computes the failure function using a boot-strapping process, where the - * pattern is matched against itself. - */ - private int[] computeFailure(byte[] pattern) { - int[] failure = new int[pattern.length]; - int j = 0; - for (int i = 1; i < pattern.length; i++) { - while (j > 0 && pattern[j] != pattern[i]) { - j = failure[j - 1]; - } - if (pattern[j] == pattern[i]) { - j++; - } - failure[i] = j; - } - return failure; - } - - /** - * Gets one bit back from a bit string stored in a byte array at the - * specified position. - * - * @param data - * , byte array to pick short value from - * @param position - * to get the bit value - * @return integer value 1 or 0 that represents the bit - */ - private int getBit(byte[] data, int pos) { - int posByte = pos / 8; - int posBit = pos % 8; - byte valByte = data[posByte]; - int valInt = valByte >> (8 - (posBit + 1)) & 0x0001; - return valInt; - } - - /** - * Sets one bit to a bit string at the specified position with the specified - * bit value. - * - * @param data - * , byte array to pick short value from - * @param position - * to set the bit - * @param value - * to set the bit to (0 or 1) - */ - private byte[] setBit(byte[] data, int position, int value) { - int posByte = position / 8; - int posBit = position % 8; - byte oldByte = data[posByte]; - oldByte = (byte) (((0xFF7F >> posBit) & oldByte) & 0x00FF); - byte newByte = (byte) ((value << (8 - (posBit + 1))) | oldByte); - data[posByte] = newByte; - return data; - } - - /** - * Converts a byte array to good readable string. - * - * @param bytes - * to be converted - * @return string representing the bytes - */ - public static String bytesToHex(byte[] bytes) { - int dwords = bytes.length / 4 + 1; - char[] hexChars = new char[bytes.length * 3 + dwords * 4]; - int position = 0; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - if (j % 4 == 0) { - String str = "(" + String.format("%02d", j) + ")"; - char[] charArray = str.toCharArray(); - for (char character : charArray) { - hexChars[position] = character; - position++; - } - } - hexChars[position] = hexArray[v >>> 4]; - position++; - hexChars[position] = hexArray[v & 0x0F]; - position++; - hexChars[position] = ' '; - position++; - } - return new String(hexChars); - } + private static final Logger logger = LoggerFactory.getLogger(DataParser.class); + + public static byte ESCAPE = (byte) 0x10; + public static byte HEADERSTART = (byte) 0x01; + public static byte END = (byte) 0x03; + public static byte GET = (byte) 0x00; + public static byte SET = (byte) 0x80; + public static byte STARTCOMMUNICATION = (byte) 0x02; + public static byte[] FOOTER = { ESCAPE, END }; + public static byte[] DATAAVAILABLE = { ESCAPE, STARTCOMMUNICATION }; + + final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); + + public List parserConfiguration = new ArrayList(); + + public DataParser() { + } + + /** + * verifies response on availability of data + * + * @param response + * of heat pump + * @param request + * request defined for heat pump response + * @return Map of Strings with name and values + */ + public Map parseRecords(final byte[] response, Request request) throws StiebelHeatPumpException { + + Map map = new HashMap(); + + logger.debug("Parse bytes: {}", DataParser.bytesToHex(response)); + + if (response.length < 2) { + logger.error("response does not have a valid length of bytes: {}", DataParser.bytesToHex(response)); + return map; + } + + // parse response and fill map + for (RecordDefinition recordDefinition : request.getRecordDefinitions()) { + try { + String value = parseRecord(response, recordDefinition); + logger.debug("Parsed value {} -> {} with pos: {} , len: {}", recordDefinition.getName(), value, + recordDefinition.getPosition(), recordDefinition.getLength()); + map.put(recordDefinition.getName(), value); + } catch (StiebelHeatPumpException e) { + continue; + } + } + return map; + } + + /** + * parses a single record + * + * @param response + * of heat pump + * @param RecordDefinition + * that shall be used for parsing the heat pump response + * @return string value of the parse response + * @throws StiebelHeatPumpException + */ + public String parseRecord(byte[] response, RecordDefinition recordDefinition) throws StiebelHeatPumpException { + try { + if (response.length < 2) { + logger.error("response does not have a valid length of bytes: {}", DataParser.bytesToHex(response)); + throw new StiebelHeatPumpException(); + } + ByteBuffer buffer = ByteBuffer.wrap(response); + short number = 0; + byte[] bytes = null; + + switch (recordDefinition.getLength()) { + case 1: + bytes = new byte[1]; + System.arraycopy(response, recordDefinition.getPosition(), bytes, 0, 1); + number = Byte.valueOf(buffer.get(recordDefinition.getPosition())); + break; + case 2: + bytes = new byte[2]; + System.arraycopy(response, recordDefinition.getPosition(), bytes, 0, 2); + number = buffer.getShort(recordDefinition.getPosition()); + break; + } + + if (recordDefinition.getBitPosition() > 0) { + + int returnValue = getBit(bytes, recordDefinition.getBitPosition()); + return String.valueOf(returnValue); + } + + if (recordDefinition.getScale() != 1.0) { + double myDoubleNumber = number * recordDefinition.getScale(); + myDoubleNumber = Math.round(myDoubleNumber * 100.0) / 100.0; + String returnString = String.format("%s", myDoubleNumber); + return returnString; + } + + return String.valueOf(number); + } catch (Exception e) { + logger.error("response {} could not be parsed for record definition {} ", DataParser.bytesToHex(response), + recordDefinition.getName()); + throw new StiebelHeatPumpException(); + } + } + + /** + * composes the new value of a record definition into a updated set command + * that can be send back to heat pump + * + * @param response + * of heat pump that should be updated with new value + * @param RecordDefinition + * that shall be used for compose the new value into the heat + * pump set command + * @param string + * value to be compose + * @return byte[] ready to send to heat pump + * @throws StiebelHeatPumpException + */ + public byte[] composeRecord(String value, byte[] response, RecordDefinition recordDefinition) + throws StiebelHeatPumpException { + short newValue = 0; + + if (recordDefinition.getDataType() != Type.Settings) { + logger.warn("The record {} can not be set as it is not a setable value!", recordDefinition.getName()); + throw new StiebelHeatPumpException("record is not a setting!"); + } + + double number = Double.parseDouble(value); + + if (number > recordDefinition.getMax() || number < recordDefinition.getMin()) { + logger.warn("The record {} can not be set to value {} as allowed range is {}<-->{} !", + recordDefinition.getName(), value, recordDefinition.getMax(), recordDefinition.getMin()); + throw new StiebelHeatPumpException("invalid value !"); + } + + // change response byte to setting command + response[1] = SET; + + // reverse the scale + if (recordDefinition.getScale() != 1.0) { + number = number / recordDefinition.getScale(); + newValue = (short) number; + } + + // set new bit values in a byte + if (recordDefinition.getBitPosition() > 0) { + + byte[] abyte = new byte[] { response[recordDefinition.getPosition()] }; + abyte = setBit(abyte, recordDefinition.getBitPosition(), newValue); + response[recordDefinition.getPosition()] = abyte[0]; + return response; + } + + // create byte values for single and double byte values + // and update response + switch (recordDefinition.getLength()) { + case 1: + byte newByteValue = (byte) number; + response[recordDefinition.getPosition()] = newByteValue; + break; + case 2: + byte[] newByteValues = shortToByte(newValue); + int position = recordDefinition.getPosition(); + response[position] = newByteValues[1]; + response[position + 1] = newByteValues[0]; + break; + } + + response[2] = this.calculateChecksum(response); + response = this.addDuplicatedBytes(response); + logger.debug("Updated record {} at position {} to value {}.", recordDefinition.getName(), + recordDefinition.getPosition(), value); + return response; + } + + /** + * verifies response on availability of data + * + * @param response + * of heat pump + * @return true if the response of the heat pump indicates availability of + * data + */ + public boolean dataAvailable(byte[] response) throws StiebelHeatPumpException { + + if (response.length == 0 || response.length > 2) { + throw new StiebelHeatPumpException("invalid response length on request of data " + new String(response)); + } + + if (response[0] != ESCAPE) { + throw new StiebelHeatPumpException("invalid response on request of data " + new String(response)); + } + if (response.length == 2 && response[1] == DATAAVAILABLE[1]) { + return true; + } + + return false; + } + + /** + * verifies the header of the heat pump response + * + * @param response + * of heat pump + */ + public void verifyHeader(byte[] response) throws StiebelHeatPumpException { + + if (response.length < 4) { + throw new StiebelHeatPumpException("invalide response length on request of data " + new String(response)); + } + + if (response[0] != HEADERSTART) { + throw new StiebelHeatPumpException( + "invalid response on request of data, found no header start: " + new String(response)); + } + + if (response[1] != GET & response[1] != SET) { + throw new StiebelHeatPumpException( + "invalid response on request of data, response is neither get nor set: " + new String(response)); + } + + if (response[2] != calculateChecksum(response)) { + throw new StiebelHeatPumpException("invalid checksum on request of data " + new String(response)); + } + } + + /** + * verifies the header of the heat pump response + * + * @param response + * of heat pump + * @return true if header is valid + */ + public boolean headerCheck(byte[] response) { + try { + verifyHeader(response); + } catch (StiebelHeatPumpException e) { + logger.debug("verification of response failed " + e.toString()); + return false; + } + + return true; + } + + /** + * verifies the heat pump response after data has been updated + * + * @param response + * of heat pump + * @return true if data set has been confirmed + */ + public boolean setDataCheck(byte[] response) { + try { + verifyHeader(response); + } catch (StiebelHeatPumpException e) { + return false; + } + + return true; + } + + /** + * calculates the checksum of a byte data array + * + * @param data + * to calculate the checksum for + * @param withReplace + * to set if the byte array shall be corrected by special replace + * method + * @return calculated checksum as short + */ + public byte calculateChecksum(byte[] data) throws StiebelHeatPumpException { + + if (data.length < 5) { + throw new StiebelHeatPumpException("no valid byte[] for calulation of checksum!"); + } + + int checkSum = 0, i = 0; + for (i = 0; i < data.length - 2; i++) { + if (i == 2) { + continue; + } + checkSum += (short) (data[i] & 0xFF); + } + + return shortToByte((short) checkSum)[0]; + } + + /** + * converts short to byte[] + * + * @return array of bytes + */ + public byte[] shortToByte(short value) { + byte[] returnByteArray = new byte[2]; + returnByteArray[0] = (byte) (value & 0xff); + returnByteArray[1] = (byte) ((value >> 8) & 0xff); + + return returnByteArray; + } + + /** + * converts integer to byte[] + * + * @return array of bytes + */ + public byte[] intToByte(int checkSum) { + byte[] returnByteArray = ByteBuffer.allocate(4).putInt(checkSum).array(); + return returnByteArray; + } + + /** + * converts byte to short + * + * @return short + */ + private short byteToShort(byte[] bytes) throws StiebelHeatPumpException { + return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getShort(); + } + + /** + * Search the data byte array for the first occurrence of the byte array + * pattern. raw data received from device have to be de-escaped before + * header evaluation and data use: - each sequence 2B 18 must be replaced + * with single byte 2B - each sequence 10 10 must be replaced with single + * byte 10 + * + * @param data + * as byte array representing response from heat pump which shall + * be fixed + * @return byte array with fixed byte entries + */ + public byte[] fixDuplicatedBytes(byte[] data) { + + // first copy the data except the last 2 bytes , the footer + byte[] bytesToBeAnalyzed = new byte[data.length - 2]; + System.arraycopy(data, 0, bytesToBeAnalyzed, 0, data.length - 2); + + byte[] fixedData = findReplace(bytesToBeAnalyzed, new byte[] { (byte) 0x10, (byte) 0x10 }, + new byte[] { (byte) 0x10 }); + fixedData = findReplace(fixedData, new byte[] { (byte) 0x2b, (byte) 0x18 }, new byte[] { (byte) 0x2b }); + + byte[] result = new byte[fixedData.length + FOOTER.length]; + // copy fixedData to result + System.arraycopy(fixedData, 0, result, 0, fixedData.length); + // copy footer to result + System.arraycopy(FOOTER, 0, result, fixedData.length, FOOTER.length); + + return result; + } + + /** + * Search the data byte array for the first occurrence of the byte array + * pattern. raw data received from device have to be de-escaped before + * header evaluation and data use: - each sequence 2B must be replaced with + * single byte 2B 18 - each sequence 10 must be replaced with single byte 10 + * 10 + * + * @param data + * as byte array representing response from heat pump which shall + * be fixed + * @return byte array with fixed byte entries + */ + public byte[] addDuplicatedBytes(byte[] data) { + ByteBuffer byteBuffer = ByteBuffer.allocate(data.length * 2); + + // add header without changes + for (int i = 0; i < 2; i++) { + byteBuffer.put(data[i]); + } + + // add now duplicates + for (int i = 2; i < data.length - 2; i++) { + byteBuffer.put(data[i]); + if (data[i] == (byte) 0x10) { + byteBuffer.put(data[i]); + } + if (data[i] == (byte) 0x2b) { + byteBuffer.put((byte) 0x18); + } + } + + // add footer without changes + for (int i = data.length - 2; i < data.length; i++) { + byteBuffer.put(data[i]); + } + + byte[] newdata = new byte[byteBuffer.position()]; + byteBuffer.rewind(); + byteBuffer.get(newdata); + + return newdata; + } + + /** + * Search the data byte array for the first occurrence of the byte array + * pattern. + * + * @param data + * as byte array to search into and to replace the pattern bytes + * with replace bytes + * @param pattern + * as byte array to search for + * @param replace + * as byte array to replace with + * @return byte array which has pattern bytes been replaced with replace + * bytes + */ + public byte[] findReplace(byte[] data, byte[] pattern, byte[] replace) { + + int position = indexOf(data, pattern); + while (position >= 0) { + byte[] newData = new byte[data.length - pattern.length + replace.length]; + System.arraycopy(data, 0, newData, 0, position); + System.arraycopy(replace, 0, newData, position, replace.length); + System.arraycopy(data, position + pattern.length, newData, position + replace.length, + data.length - position - pattern.length); + position = indexOf(newData, pattern); + data = new byte[newData.length]; + System.arraycopy(newData, 0, data, 0, newData.length); + } + return data; + } + + /** + * Search the data byte array for the first occurrence of the byte array + * pattern. + * + * @param data + * to find pattern in + * @param pattern + * to be searched + * @return byte number were pattern was found in data + */ + private int indexOf(byte[] data, byte[] pattern) { + int[] failure = computeFailure(pattern); + int j = 0; + for (int i = 0; i < data.length; i++) { + while (j > 0 && pattern[j] != data[i]) { + j = failure[j - 1]; + } + if (pattern[j] == data[i]) { + j++; + } + if (j == pattern.length) { + return i - pattern.length + 1; + } + } + return -1; + } + + /** + * Computes the failure function using a boot-strapping process, where the + * pattern is matched against itself. + */ + private int[] computeFailure(byte[] pattern) { + int[] failure = new int[pattern.length]; + int j = 0; + for (int i = 1; i < pattern.length; i++) { + while (j > 0 && pattern[j] != pattern[i]) { + j = failure[j - 1]; + } + if (pattern[j] == pattern[i]) { + j++; + } + failure[i] = j; + } + return failure; + } + + /** + * Gets one bit back from a bit string stored in a byte array at the + * specified position. + * + * @param data + * , byte array to pick short value from + * @param position + * to get the bit value + * @return integer value 1 or 0 that represents the bit + */ + private int getBit(byte[] data, int pos) { + int posByte = pos / 8; + int posBit = pos % 8; + byte valByte = data[posByte]; + int valInt = valByte >> (8 - (posBit + 1)) & 0x0001; + return valInt; + } + + /** + * Sets one bit to a bit string at the specified position with the specified + * bit value. + * + * @param data + * , byte array to pick short value from + * @param position + * to set the bit + * @param value + * to set the bit to (0 or 1) + */ + private byte[] setBit(byte[] data, int position, int value) { + int posByte = position / 8; + int posBit = position % 8; + byte oldByte = data[posByte]; + oldByte = (byte) (((0xFF7F >> posBit) & oldByte) & 0x00FF); + byte newByte = (byte) ((value << (8 - (posBit + 1))) | oldByte); + data[posByte] = newByte; + return data; + } + + /** + * Converts a byte array to good readable string. + * + * @param bytes + * to be converted + * @return string representing the bytes + */ + public static String bytesToHex(byte[] bytes) { + int dwords = bytes.length / 4 + 1; + char[] hexChars = new char[bytes.length * 3 + dwords * 4]; + int position = 0; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + if (j % 4 == 0) { + String str = "(" + String.format("%02d", j) + ")"; + char[] charArray = str.toCharArray(); + for (char character : charArray) { + hexChars[position] = character; + position++; + } + } + hexChars[position] = hexArray[v >>> 4]; + position++; + hexChars[position] = hexArray[v & 0x0F]; + position++; + hexChars[position] = ' '; + position++; + } + return new String(hexChars); + } } diff --git a/bundles/binding/org.openhab.binding.swegonventilation/src/main/java/org/openhab/binding/swegonventilation/internal/SwegonVentilationBinding.java b/bundles/binding/org.openhab.binding.swegonventilation/src/main/java/org/openhab/binding/swegonventilation/internal/SwegonVentilationBinding.java index 8d6c98140e4..688c23cb522 100644 --- a/bundles/binding/org.openhab.binding.swegonventilation/src/main/java/org/openhab/binding/swegonventilation/internal/SwegonVentilationBinding.java +++ b/bundles/binding/org.openhab.binding.swegonventilation/src/main/java/org/openhab/binding/swegonventilation/internal/SwegonVentilationBinding.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2015, openHAB.org and others. + * Copyright (c) 2010-2016, openHAB.org and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -38,229 +38,228 @@ import org.slf4j.LoggerFactory; /** - * + * * Binding to receive data from Swegon ventilation system. - * + * * @author Pauli Anttila * @since 1.4.0 */ -public class SwegonVentilationBinding extends - AbstractBinding implements - ManagedService { - - private static final Logger logger = LoggerFactory - .getLogger(SwegonVentilationBinding.class); - - /* configuration variables for communication */ - private int udpPort = 9998; - private String serialPort = null; - private boolean simulator = false; - - /** Thread to handle messages from heat pump */ - private MessageListener messageListener = null; - - public SwegonVentilationBinding() { - } - - public void activate() { - logger.debug("Activate"); - } - - public void deactivate() { - logger.debug("Deactivate"); - - if (messageListener != null) { - messageListener.setInterrupted(true); - } - } - - public void setEventPublisher(EventPublisher eventPublisher) { - this.eventPublisher = eventPublisher; - } - - public void unsetEventPublisher(EventPublisher eventPublisher) { - this.eventPublisher = null; - } - - /** - * @{inheritDoc - */ - @Override - public void updated(Dictionary config) - throws ConfigurationException { - - logger.debug("Configuration updated, config {}", config != null ? true - : false); - - if (config != null) { - - String portString = (String) config.get("udpPort"); - if (StringUtils.isNotBlank(portString)) { - udpPort = Integer.parseInt(portString); - } - - serialPort = (String) config.get("serialPort"); - - String simulateString = (String) config.get("simulate"); - if (StringUtils.isNotBlank(simulateString)) { - simulator = Boolean.parseBoolean(simulateString); - } - } - - if (messageListener != null) { - - logger.debug("Close previous message listener"); - - messageListener.setInterrupted(true); - try { - messageListener.join(); - } catch (InterruptedException e) { - logger.info("Previous message listener closing interrupted", e); - } - } - - messageListener = new MessageListener(); - messageListener.start(); - - } - - /** - * Convert device value to OpenHAB state. - * - * @param itemType - * @param value - * - * @return a {@link State} - */ - private State convertDeviceValueToOpenHabState(Class itemType, Integer value) { - State state = UnDefType.UNDEF; - - try { - if (itemType == SwitchItem.class) { - state = value == 0 ? OnOffType.OFF : OnOffType.ON; - - } else if (itemType == NumberItem.class) { - state = new DecimalType(value); - - } - } catch (Exception e) { - logger.debug("Cannot convert value '{}' to data type {}", value, itemType); - } - - return state; - } - - /** - * The MessageListener runs as a separate thread. - * - * Thread listening message from Swegon ventilation system and send updates - * to openHAB bus. - * - */ - private class MessageListener extends Thread { - - private boolean interrupted = false; - - MessageListener() { - } - - public void setInterrupted(boolean interrupted) { - this.interrupted = interrupted; - this.interrupt(); - } - - @Override - public void run() { - - logger.debug("Swegon ventilation system message listener started"); - - SwegonVentilationConnector connector; - - if (simulator == true) - connector = new SwegonVentilationSimulator(); - else if (serialPort != null) - connector = new SwegonVentilationSerialConnector(serialPort); - else - connector = new SwegonVentilationUDPConnector(udpPort); - - try { - connector.connect(); - } catch (SwegonVentilationException e) { - logger.error( - "Error occured when connecting to Swegon ventilation system", - e); - - logger.warn("Closing Swegon ventilation system message listener"); - - // exit - interrupted = true; - } - - // as long as no interrupt is requested, continue running - while (!interrupted) { - - try { - // Wait a packet (blocking) - byte[] data = connector.receiveDatagram(); - - logger.trace("Received data (len={}): {}", data.length, - DatatypeConverter.printHexBinary(data)); - - HashMap regValues = SwegonVentilationDataParser - .parseData(data); - - if (regValues != null) { - - logger.debug("regValues (len={}): {}", - regValues.size(), regValues); - - Set> set = regValues - .entrySet(); - - for (Entry val : set) { - - SwegonVentilationCommandType cmdType = val.getKey(); - Integer value = val.getValue(); - - for (SwegonVentilationBindingProvider provider : providers) { - for (String itemName : provider.getItemNames()) { - - SwegonVentilationCommandType commandType = provider - .getCommandType(itemName); - - if (commandType.equals(cmdType)) { - Class itemType = provider.getItemType(itemName); - - org.openhab.core.types.State state = convertDeviceValueToOpenHabState(itemType, value); +public class SwegonVentilationBinding extends AbstractBinding + implements ManagedService { + + private static final Logger logger = LoggerFactory.getLogger(SwegonVentilationBinding.class); + + /* configuration variables for communication */ + private int udpPort = 9998; + private String serialPort = null; + private boolean simulator = false; + + /** Thread to handle messages from heat pump */ + private MessageListener messageListener = null; + + public SwegonVentilationBinding() { + } + + @Override + public void activate() { + logger.debug("Activate"); + } + + @Override + public void deactivate() { + logger.debug("Deactivate"); + + if (messageListener != null) { + messageListener.setInterrupted(true); + } + } + + @Override + public void setEventPublisher(EventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + @Override + public void unsetEventPublisher(EventPublisher eventPublisher) { + this.eventPublisher = null; + } + + protected void addBindingProvider(SwegonVentilationBindingProvider bindingProvider) { + super.addBindingProvider(bindingProvider); + } + + protected void removeBindingProvider(SwegonVentilationBindingProvider bindingProvider) { + super.removeBindingProvider(bindingProvider); + } + + /** + * {@inheritDoc} + */ + @Override + public void updated(Dictionary config) throws ConfigurationException { + + logger.debug("Configuration updated, config {}", config != null ? true : false); + + if (config != null) { + + String portString = (String) config.get("udpPort"); + if (StringUtils.isNotBlank(portString)) { + udpPort = Integer.parseInt(portString); + } + + serialPort = (String) config.get("serialPort"); + + String simulateString = (String) config.get("simulate"); + if (StringUtils.isNotBlank(simulateString)) { + simulator = Boolean.parseBoolean(simulateString); + } + } + + if (messageListener != null) { + + logger.debug("Close previous message listener"); + + messageListener.setInterrupted(true); + try { + messageListener.join(); + } catch (InterruptedException e) { + logger.info("Previous message listener closing interrupted", e); + } + } + + messageListener = new MessageListener(); + messageListener.start(); + + } + + /** + * Convert device value to OpenHAB state. + * + * @param itemType + * @param value + * + * @return a {@link State} + */ + private State convertDeviceValueToOpenHabState(Class itemType, Integer value) { + State state = UnDefType.UNDEF; + + try { + if (itemType == SwitchItem.class) { + state = value == 0 ? OnOffType.OFF : OnOffType.ON; + + } else if (itemType == NumberItem.class) { + state = new DecimalType(value); + + } + } catch (Exception e) { + logger.debug("Cannot convert value '{}' to data type {}", value, itemType); + } + + return state; + } + + /** + * The MessageListener runs as a separate thread. + * + * Thread listening message from Swegon ventilation system and send updates + * to openHAB bus. + * + */ + private class MessageListener extends Thread { + + private boolean interrupted = false; + + MessageListener() { + } + + public void setInterrupted(boolean interrupted) { + this.interrupted = interrupted; + this.interrupt(); + } + + @Override + public void run() { + + logger.debug("Swegon ventilation system message listener started"); + + SwegonVentilationConnector connector; + + if (simulator == true) { + connector = new SwegonVentilationSimulator(); + } else if (serialPort != null) { + connector = new SwegonVentilationSerialConnector(serialPort); + } else { + connector = new SwegonVentilationUDPConnector(udpPort); + } + + try { + connector.connect(); + } catch (SwegonVentilationException e) { + logger.error("Error occured when connecting to Swegon ventilation system", e); + + logger.warn("Closing Swegon ventilation system message listener"); + + // exit + interrupted = true; + } + + // as long as no interrupt is requested, continue running + while (!interrupted) { + + try { + // Wait a packet (blocking) + byte[] data = connector.receiveDatagram(); + + logger.trace("Received data (len={}): {}", data.length, DatatypeConverter.printHexBinary(data)); + + HashMap regValues = SwegonVentilationDataParser + .parseData(data); + + if (regValues != null) { + + logger.debug("regValues (len={}): {}", regValues.size(), regValues); + + Set> set = regValues.entrySet(); + + for (Entry val : set) { + + SwegonVentilationCommandType cmdType = val.getKey(); + Integer value = val.getValue(); + + for (SwegonVentilationBindingProvider provider : providers) { + for (String itemName : provider.getItemNames()) { + + SwegonVentilationCommandType commandType = provider.getCommandType(itemName); + + if (commandType.equals(cmdType)) { + Class itemType = provider.getItemType(itemName); + + org.openhab.core.types.State state = convertDeviceValueToOpenHabState(itemType, + value); - eventPublisher.postUpdate(itemName, - state); - } - } - } + eventPublisher.postUpdate(itemName, state); + } + } + } - } + } - } + } - } catch (SwegonVentilationException e) { + } catch (SwegonVentilationException e) { - logger.error( - "Error occured when received data from Swegon ventilation system", - e); - } - } + logger.error("Error occured when received data from Swegon ventilation system", e); + } + } - try { - connector.disconnect(); - } catch (SwegonVentilationException e) { - logger.error( - "Error occured when disconnecting form Swegon ventilation system", - e); - } + try { + connector.disconnect(); + } catch (SwegonVentilationException e) { + logger.error("Error occured when disconnecting from Swegon ventilation system", e); + } - } + } - } + } }