/** * Copyright (c) 2010-2016 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.maxcube.internal; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.Socket; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Dictionary; import java.util.List; import org.apache.commons.lang.StringUtils; import org.openhab.binding.maxcube.MaxCubeBindingProvider; import org.openhab.binding.maxcube.internal.exceptions.IncompleteMessageException; import org.openhab.binding.maxcube.internal.exceptions.IncorrectMultilineIndexException; import org.openhab.binding.maxcube.internal.exceptions.MessageIsWaitingException; import org.openhab.binding.maxcube.internal.exceptions.NoMessageAvailableException; import org.openhab.binding.maxcube.internal.exceptions.UnprocessableMessageException; import org.openhab.binding.maxcube.internal.exceptions.UnsupportedMessageTypeException; import org.openhab.binding.maxcube.internal.message.C_Message; import org.openhab.binding.maxcube.internal.message.Configuration; import org.openhab.binding.maxcube.internal.message.Device; import org.openhab.binding.maxcube.internal.message.DeviceInformation; import org.openhab.binding.maxcube.internal.message.HeatingThermostat; import org.openhab.binding.maxcube.internal.message.L_Message; import org.openhab.binding.maxcube.internal.message.M_Message; import org.openhab.binding.maxcube.internal.message.Message; import org.openhab.binding.maxcube.internal.message.MessageProcessor; import org.openhab.binding.maxcube.internal.message.MessageType; import org.openhab.binding.maxcube.internal.message.S_Command; import org.openhab.binding.maxcube.internal.message.S_Message; import org.openhab.binding.maxcube.internal.message.ShutterContact; import org.openhab.binding.maxcube.internal.message.ThermostatModeType; import org.openhab.core.binding.AbstractActiveBinding; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.StringType; import org.openhab.core.types.Command; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The RefreshService polls the MAX!Cube frequently and updates the list of * configurations and devices. The refresh interval can be changed via * openhab.cfg. * * Note that the MAX Cube has a lock out that only allows a maximum of 36s of * transmissions (1%) in total in 1 hour. This means that if too many S messages * are sent then the cube no longer sends the data out. * * @author Andreas Heil (info@aheil.de) * @author Bernd Michael Helm (bernd.helm at helmundwalter.de) * @since 1.4.0 */ public class MaxCubeBinding extends AbstractActiveBinding<MaxCubeBindingProvider>implements ManagedService { private static final Logger logger = LoggerFactory.getLogger(MaxCubeBinding.class); /** The IP address of the MAX!Cube LAN gateway */ private static String ip; /** * The port of the MAX!Cube LAN gateway as provided at * http://www.elv.de/controller.aspx?cid=824&detail=10&detail2=3484 */ private static int port = 62910; /** * Duty cycle of the cube */ private int dutyCycle = 0; /** * The available memory slots of the cube */ private int freeMemorySlots; /** The refresh interval which is used to poll given MAX!Cube */ private static long refreshInterval = 10000; /** * If set to true, the binding will leave the connection to the cube * open and just request new informations. * This allows much higher poll rates and causes less load than the * non-exclusive polling but has the drawback that no other apps * (i.E. original software) can use the cube while this binding is * running. */ private static boolean exclusive = false; /** * in exclusive mode, how many requests are allowed until connection is closed and reopened */ private static int maxRequestsPerConnection = 1000; private int requestCount = 0; /** MaxCube's default off temperature */ private static final DecimalType DEFAULT_OFF_TEMPERATURE = new DecimalType(4.5); /** MaxCubes default on temperature */ private static final DecimalType DEFAULT_ON_TEMPERATURE = new DecimalType(30.5); /** * Configuration and device lists, kept during the overall lifetime of the * binding */ private List<Configuration> configurations = new ArrayList<Configuration>(); private List<Device> devices = new ArrayList<Device>(); /** * connection socket and reader/writer for execute method */ private Socket socket = null; private BufferedReader reader = null; private OutputStreamWriter writer = null; /** * Processor that handles lines received from MAX!Cube */ MessageProcessor messageProcessor = new MessageProcessor(); /** * {@inheritDoc} */ @Override protected String getName() { return "MAX!Cube Refresh Service"; } /** * {@inheritDoc} */ @Override protected long getRefreshInterval() { return refreshInterval; } /** * {@inheritDoc} */ @Override public void activate() { super.activate(); setProperlyConfigured(false); } @Override public void deactivate() { socketClose(); } /** * {@inheritDoc} */ @Override public synchronized void execute() { if (ip == null) { logger.debug("Update prior to completion of interface IP configuration"); return; } try { String raw = null; if (maxRequestsPerConnection > 0 && requestCount >= maxRequestsPerConnection) { logger.debug("maxRequestsPerConnection reached, reconnecting."); socket.close(); this.socketConnect(); } if (socket == null) { this.socketConnect(); } else { /* * if the connection is already open (this happens in exclusive mode), just send a "l:\r\n" to get the * latest live informations * note that "L:\r\n" or "l:\n" would not work. */ logger.debug("Sending state request #" + this.requestCount + " to Maxcube"); writer.write("l:" + '\r' + '\n'); writer.flush(); requestCount++; } boolean cont = true; while (cont) { raw = reader.readLine(); if (raw == null) { cont = false; continue; } Message message = null; try { this.messageProcessor.addReceivedLine(raw); if (this.messageProcessor.isMessageAvailable()) { message = this.messageProcessor.pull(); } else { continue; } message.debug(logger); if (message != null) { message.debug(logger); if (message.getType() == MessageType.M) { M_Message msg = (M_Message) message; for (DeviceInformation di : msg.devices) { Configuration c = null; for (Configuration conf : configurations) { if (conf.getSerialNumber().equalsIgnoreCase(di.getSerialNumber())) { c = conf; break; } } if (c != null) { configurations.remove(c); } c = Configuration.create(di); configurations.add(c); c.setRoomId(di.getRoomId()); } } else if (message.getType() == MessageType.C) { Configuration c = null; for (Configuration conf : configurations) { if (conf.getSerialNumber().equalsIgnoreCase(((C_Message) message).getSerialNumber())) { c = conf; break; } } if (c == null) { configurations.add(Configuration.create(message)); } else { c.setValues((C_Message) message); } } else if (message.getType() == MessageType.S) { sMessageProcessing((S_Message) message); cont = false; } else if (message.getType() == MessageType.L) { ((L_Message) message).updateDevices(devices, configurations); logger.debug("{} devices found.", devices.size()); // the L message is the last one, while the reader // would hang trying to read a new line and // eventually the // cube will fail to establish // new connections for some time cont = false; } } } catch (IncorrectMultilineIndexException ex) { logger.info( "Incorrect MAX!Cube multiline message detected. Stopping processing and continue with next Line."); this.messageProcessor.reset(); } catch (NoMessageAvailableException ex) { logger.info("Could not process MAX!Cube message. Stopping processing and continue with next Line."); this.messageProcessor.reset(); } catch (IncompleteMessageException ex) { logger.info( "Error while parsing MAX!Cube multiline message. Stopping processing, and continue with next Line."); this.messageProcessor.reset(); } catch (UnprocessableMessageException ex) { logger.info( "Error while parsing MAX!Cube message. Stopping processing, and continue with next Line."); this.messageProcessor.reset(); } catch (UnsupportedMessageTypeException ex) { logger.info("Unsupported MAX!Cube message detected. Ignoring and continue with next Line."); this.messageProcessor.reset(); } catch (MessageIsWaitingException ex) { logger.info("There was an unhandled message waiting. Ignoring and continue with next Line."); this.messageProcessor.reset(); } catch (Exception e) { logger.info("Failed to process message received by MAX! protocol."); logger.debug(Utils.getStackTrace(e)); this.messageProcessor.reset(); } } if (!exclusive) { socketClose(); } for (MaxCubeBindingProvider provider : providers) { for (String itemName : provider.getItemNames()) { String serialNumber = provider.getSerialNumber(itemName); Device device = findDevice(serialNumber, devices); if (device == null) { logger.info("Cannot find MAX!cube device with serial number '{}'", serialNumber); logAvailableMaxDevices(); continue; } // all devices have a battery state, so this is type-independent if (provider.getBindingType(itemName) == BindingType.BATTERY) { if (device.battery().isChargeUpdated()) { eventPublisher.postUpdate(itemName, device.battery().getCharge()); } } else if (provider.getBindingType(itemName) == BindingType.CONNECTION_ERROR) { if (device.isErrorUpdated()) { OnOffType connectionError = device.isError() ? OnOffType.ON : OnOffType.OFF; eventPublisher.postUpdate(itemName, connectionError); } } else { switch (device.getType()) { case HeatingThermostatPlus: case HeatingThermostat: if (provider.getBindingType(itemName) == BindingType.VALVE && ((HeatingThermostat) device).isValvePositionUpdated()) { eventPublisher.postUpdate(itemName, ((HeatingThermostat) device).getValvePosition()); break; } // omitted break, fall through case WallMountedThermostat: // and also HeatingThermostat if (provider.getBindingType(itemName) == BindingType.MODE && ((HeatingThermostat) device).isModeUpdated()) { eventPublisher.postUpdate(itemName, ((HeatingThermostat) device).getModeString()); } else if (provider.getBindingType(itemName) == BindingType.ACTUAL && ((HeatingThermostat) device).isTemperatureActualUpdated()) { eventPublisher.postUpdate(itemName, ((HeatingThermostat) device).getTemperatureActual()); } else if (((HeatingThermostat) device).isTemperatureSetpointUpdated() && provider.getBindingType(itemName) == null) { eventPublisher.postUpdate(itemName, ((HeatingThermostat) device).getTemperatureSetpoint()); } break; case ShutterContact: if (((ShutterContact) device).isShutterStateUpdated()) { eventPublisher.postUpdate(itemName, ((ShutterContact) device).getShutterState()); } break; default: // no further devices supported yet } } } } } catch (UnknownHostException e) { logger.info("Host error occurred while connecting to MAX! Cube lan gateway '{}': {}", ip, e.getMessage()); socketClose(); } catch (IOException e) { logger.info("IO error occurred while connecting to MAX! Cube lan gateway '{}': {}", ip, e.getMessage()); socketClose(); // reconnect on next execution } catch (Exception e) { logger.info("Error occurred while connecting to MAX! Cube lan gateway '{}': {}", ip, e.getMessage()); logger.info(Utils.getStackTrace(e)); socketClose(); // reconnect on next execution } } private void logAvailableMaxDevices() { if (logger.isDebugEnabled()) { StringBuilder sb = new StringBuilder(); sb.append("Available MAX! devices are:"); for (Device d : devices) { sb.append("\n\t"); sb.append(d.getSerialNumber()); } logger.debug(sb.toString()); } } /** * {@inheritDoc} */ @Override public void internalReceiveCommand(String itemName, Command command) { logger.debug("Received command from {}", itemName); // resolve serial number for item String serialNumber = null; for (MaxCubeBindingProvider provider : providers) { serialNumber = provider.getSerialNumber(itemName); if (StringUtils.isBlank(serialNumber)) { continue; } // send command to MAX!Cube LAN Gateway Device device = findDevice(serialNumber, devices); if (device == null) { logger.debug("Cannot send command to device with serial number {}, device not listed.", serialNumber); continue; } String rfAddress = device.getRFAddress(); String commandString = null; if (command instanceof DecimalType || command instanceof OnOffType) { DecimalType decimalType = DEFAULT_OFF_TEMPERATURE; if (command instanceof DecimalType) { decimalType = (DecimalType) command; } else if (command instanceof OnOffType) { decimalType = OnOffType.ON.equals(command) ? DEFAULT_ON_TEMPERATURE : DEFAULT_OFF_TEMPERATURE; } S_Command cmd = new S_Command(rfAddress, device.getRoomId(), ((HeatingThermostat) device).getMode(), decimalType.doubleValue()); commandString = cmd.getCommandString(); } else if (command instanceof StringType) { String commandContent = command.toString().trim().toUpperCase(); S_Command cmd = null; ThermostatModeType commandThermoType = null; if (commandContent.contentEquals(ThermostatModeType.AUTOMATIC.toString())) { commandThermoType = ThermostatModeType.AUTOMATIC; cmd = new S_Command(rfAddress, device.getRoomId(), commandThermoType); } else if (commandContent.contentEquals(ThermostatModeType.BOOST.toString())) { commandThermoType = ThermostatModeType.BOOST; Double setTemp = Double .parseDouble(((HeatingThermostat) device).getTemperatureSetpoint().toString()); cmd = new S_Command(rfAddress, device.getRoomId(), commandThermoType, setTemp); } else if (commandContent.contentEquals(ThermostatModeType.MANUAL.toString())) { commandThermoType = ThermostatModeType.MANUAL; Double setTemp = Double .parseDouble(((HeatingThermostat) device).getTemperatureSetpoint().toString()); cmd = new S_Command(rfAddress, device.getRoomId(), commandThermoType, setTemp); logger.debug("updates to MANUAL mode with temperature '{}'", setTemp); } else { logger.debug("Only updates to AUTOMATIC, MANUAL & BOOST supported, received value ;'{}'", commandContent); continue; } commandString = cmd.getCommandString(); } if (commandString != null) { try { if (socket == null) { this.socketConnect(); } writer.write(commandString); logger.debug(commandString); writer.flush(); Message message = null; String raw = reader.readLine(); try { while (!this.messageProcessor.isMessageAvailable()) { this.messageProcessor.addReceivedLine(raw); raw = reader.readLine(); } message = this.messageProcessor.pull(); } catch (Exception e) { logger.info("Error while handling response from MAX! Cube lan gateway!"); logger.debug(Utils.getStackTrace(e)); this.messageProcessor.reset(); } if (message != null) { if (message.getType() == MessageType.S) { sMessageProcessing((S_Message) message); } } if (!exclusive) { socket.close(); socket = null; } } catch (UnknownHostException e) { logger.info("Host error occurred while connecting to MAX! Cube lan gateway '{}': {}", ip, e.getMessage()); socketClose(); } catch (IOException e) { logger.info("IO error occurred while writing to MAX! Cube lan gateway '{}': {}", ip, e.getMessage()); socketClose(); // reconnect on next execution } catch (Exception e) { logger.info("Error occurred while writing to MAX! Cube lan gateway '{}': {}", ip, e.getMessage()); logger.info(Utils.getStackTrace(e)); socketClose(); // reconnect on next execution } logger.debug("Command Sent to {}", ip); } else { logger.debug("Null Command not sent to {}", ip); } } } /** * Processes the S message and updates Duty Cycle & Free Memory Slots * * @param S_Message message */ private void sMessageProcessing(S_Message message) { dutyCycle = message.getDutyCycle(); freeMemorySlots = message.getFreeMemorySlots(); if (message.isCommandDiscarded()) { logger.info("Last Send Command discarded. Duty Cycle: {}, Free Memory Slots: {}", dutyCycle, freeMemorySlots); } else { logger.debug("S message. Duty Cycle: {}, Free Memory Slots: {}", dutyCycle, freeMemorySlots); } } private boolean socketConnect() throws UnknownHostException, IOException { socket = new Socket(ip, port); socket.setSoTimeout(2000); logger.debug("open new connection... to " + ip + " port " + port); reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); writer = new OutputStreamWriter(socket.getOutputStream()); requestCount = 0; return true; } private void socketClose() { if (socket != null) { try { socket.close(); } catch (IOException e) { // Ignore } socket = null; } } private Device findDevice(String serialNumber, List<Device> devices) { for (Device device : devices) { if (device.getSerialNumber().toUpperCase().equals(serialNumber)) { return device; } } return null; } protected void addBindingProvider(MaxCubeBindingProvider bindingProvider) { super.addBindingProvider(bindingProvider); } protected void removeBindingProvider(MaxCubeBindingProvider bindingProvider) { super.removeBindingProvider(bindingProvider); } /** * {@inheritDoc} */ @Override @SuppressWarnings("rawtypes") public void updated(Dictionary config) throws ConfigurationException { if (config != null) { ip = (String) config.get("ip"); if (StringUtils.isBlank(ip)) { ip = discoveryGatewayIp(); } String portString = (String) config.get("port"); if (portString != null && !portString.isEmpty()) { if (port > 0 && port <= 65535) { port = Integer.parseInt(portString); } } String refreshIntervalString = (String) config.get("refreshInterval"); if (refreshIntervalString != null && !refreshIntervalString.isEmpty()) { refreshInterval = Long.parseLong(refreshIntervalString); } String exclusiveString = (String) config.get("exclusive"); if (StringUtils.isNotBlank(exclusiveString)) { exclusive = Boolean.parseBoolean(exclusiveString); } String maxRequestsPerConnectionString = (String) config.get("maxRequestsPerConnection"); if (maxRequestsPerConnectionString != null && !maxRequestsPerConnectionString.isEmpty()) { maxRequestsPerConnection = Integer.parseInt(maxRequestsPerConnectionString); } } else { ip = discoveryGatewayIp(); } setProperlyConfigured(ip != null); } /** * Discovers the MAX!CUbe LAN Gateway IP address. * * @return the cube IP if available, a blank string otherwise. * @throws ConfigurationException */ private String discoveryGatewayIp() throws ConfigurationException { String ip = MaxCubeDiscover.discoverIp(); if (ip == null) { throw new ConfigurationException("maxcube:ip", "IP address for MAX!Cube must be set"); } else { logger.info("Discovered MAX!Cube lan gateway at '{}'", ip); } return ip; } }