/** * 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.ihc.ws; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStreamReader; import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.zip.GZIPInputStream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.apache.commons.lang.StringUtils; import org.openhab.binding.ihc.ws.datatypes.WSControllerState; import org.openhab.binding.ihc.ws.datatypes.WSFile; import org.openhab.binding.ihc.ws.datatypes.WSLoginResult; import org.openhab.binding.ihc.ws.datatypes.WSProjectInfo; import org.openhab.binding.ihc.ws.datatypes.WSResourceValue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.xml.sax.InputSource; /** * IhcClient provides interface to communicate IHC / ELKO LS Controller. * * Controller interface is SOAP web service based via HTTPS link. * * @author Pauli Anttila * @since 1.1.0 */ public class IhcClient { /* * If you wonder, why e.g. Axis(2) or JAX-WS is not used to handle SOAP * interface... * * WSDL files are included, so feel free to try ;) */ /** Current state of the connection */ public enum ConnectionState { DISCONNECTED, CONNECTING, CONNECTED } public final static String CONTROLLER_STATE_READY = "text.ctrl.state.ready"; public final static String CONTROLLER_STATE_INITIALIZE = "text.ctrl.state.initialize"; private static final Logger logger = LoggerFactory.getLogger(IhcClient.class); private static ConnectionState connState = ConnectionState.DISCONNECTED; /** Controller services */ private static IhcAuthenticationService authenticationService = null; private static IhcResourceInteractionService resourceInteractionService = null; private static IhcControllerService controllerService = null; /** Thread to handle resource value notifications from the controller */ private IhcResourceValueNotificationListener resourceValueNotificationListener = null; /** Thread to handle controller's state change notifications */ private IhcControllerStateListener controllerStateListener = null; private String username = ""; private String password = ""; private String ip = ""; private int timeout = 5000; // milliseconds private String projectFile = null; private String dumpResourcesToFile = null; private Map<Integer, WSResourceValue> resourceValues = new HashMap<Integer, WSResourceValue>(); private HashMap<Integer, ArrayList<IhcEnumValue>> enumDictionary = new HashMap<Integer, ArrayList<IhcEnumValue>>(); private List<IhcEventListener> eventListeners = new ArrayList<IhcEventListener>(); private WSControllerState controllerState = null; public IhcClient(String ip, String username, String password) { this.ip = ip; this.username = username; this.password = password; } public IhcClient(String ip, String username, String password, int timeout) { this(ip, username, password); setTimeoutInMillisecods(timeout); } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } public int getTimeoutInMillisecods() { return timeout; } public void setTimeoutInMillisecods(int timeout) { this.timeout = timeout; } public String getProjectFile() { return projectFile; } public void setProjectFile(String path) { this.projectFile = path; } public String getDumpResourceInformationToFile() { return dumpResourcesToFile; } public void setDumpResourceInformationToFile(String value) { this.dumpResourcesToFile = value; } public synchronized ConnectionState getConnectionState() { return connState; } private synchronized void setConnectionState(ConnectionState newState) { IhcClient.connState = newState; } public void addEventListener(IhcEventListener listener) { eventListeners.add(listener); } public void removeEventListener(IhcEventListener listener) { eventListeners.remove(listener); } /** * Open connection and authenticate session to IHC / ELKO LS controller. * * @return */ public void closeConnection() throws IhcExecption { logger.debug("Close connection"); if (resourceValueNotificationListener != null) { resourceValueNotificationListener.setInterrupted(true); } if (controllerStateListener != null) { controllerStateListener.setInterrupted(true); } setConnectionState(ConnectionState.DISCONNECTED); } /** * Open connection and authenticate session to IHC / ELKO LS controller. * * @throws IhcExecption */ public void openConnection() throws IhcExecption { logger.debug("Open connection"); setConnectionState(ConnectionState.CONNECTING); authenticationService = new IhcAuthenticationService(ip, timeout); WSLoginResult loginResult = authenticationService.authenticate(username, password, "treeview"); if (!loginResult.isLoginWasSuccessful()) { // Login failed setConnectionState(ConnectionState.DISCONNECTED); if (loginResult.isLoginFailedDueToAccountInvalid()) { throw new IhcExecption("login failed because of invalid account"); } if (loginResult.isLoginFailedDueToConnectionRestrictions()) { throw new IhcExecption("login failed because of connection restrictions"); } if (loginResult.isLoginFailedDueToInsufficientUserRights()) { throw new IhcExecption("login failed because of insufficient user rights"); } throw new IhcExecption("login failed because of unknown reason"); } logger.debug("Connection successfully opened"); resourceInteractionService = new IhcResourceInteractionService(ip, timeout); controllerService = new IhcControllerService(ip, timeout); controllerState = controllerService.getControllerState(); loadProject(); startIhcListeners(); setConnectionState(ConnectionState.CONNECTED); } private void startIhcListeners() { logger.debug("startIhcListeners"); resourceValueNotificationListener = new IhcResourceValueNotificationListener(); resourceValueNotificationListener.start(); controllerStateListener = new IhcControllerStateListener(); controllerStateListener.start(); } /** * Query project information from the controller. * * @return project information. * @throws IhcExecption */ public synchronized WSProjectInfo getProjectInfo() throws IhcExecption { return controllerService.getProjectInfo(); } /** * Query controller current state. * * @return controller's current state. */ public WSControllerState getControllerState() { return controllerState; } /** * Load IHC / ELKO LS project file. * */ private synchronized void loadProject() throws IhcExecption { if (StringUtils.isNotBlank(projectFile)) { logger.debug("Loading IHC /ELKO LS project file from path {}...", projectFile); try { enumDictionary = IhcProjectFile.parseProject(projectFile, dumpResourcesToFile); } catch (IhcExecption e) { logger.error("Project file loading error", e); } } else { logger.debug("Loading IHC /ELKO LS project file from controller..."); try { Document doc = LoadProjectFileFromController(); IhcProjectFile.parseProject(doc, dumpResourcesToFile); } catch (IhcExecption e) { throw new IhcExecption("Project file loading error", e); } } } private Document LoadProjectFileFromController() throws IhcExecption { try { WSProjectInfo projectInfo = getProjectInfo(); int numberOfSegments = controllerService.getProjectNumberOfSegments(); int segmentationSize = controllerService.getProjectSegmentationSize(); logger.debug("Number of segments: {}", numberOfSegments); logger.debug("Segmentation size: {}", segmentationSize); ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); for (int i = 0; i < numberOfSegments; i++) { logger.debug("Downloading segment {}", i); WSFile data = controllerService.getProjectSegment(i, projectInfo.getProjectMajorRevision(), projectInfo.getProjectMinorRevision()); byteStream.write(data.getData()); } logger.debug("File size before base64 encoding: {} bytes", byteStream.size()); byte[] decodedBytes = javax.xml.bind.DatatypeConverter.parseBase64Binary(byteStream.toString()); logger.debug("File size after base64 encoding: {} bytes", decodedBytes.length); GZIPInputStream gzis = new GZIPInputStream(new ByteArrayInputStream(decodedBytes)); InputStreamReader in = new InputStreamReader(gzis, "ISO-8859-1"); InputSource reader = new InputSource(in); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); return db.parse(reader); } catch (Exception e) { throw new IhcExecption(e); } } /** * Wait controller state change notification. * * @param previousState * Previous controller state. * @param timeoutInSeconds * How many seconds to wait notifications. * @return current controller state. */ private WSControllerState waitStateChangeNotifications(WSControllerState previousState, int timeoutInSeconds) throws IhcExecption { IhcControllerService service = new IhcControllerService(ip, timeout); return service.waitStateChangeNotifications(previousState, timeoutInSeconds); } /** * Returns all possible enumerated values for corresponding enum type. * * @param typedefId * Enum type definition identifier. * @return list of enum values. */ public ArrayList<IhcEnumValue> getEnumValues(int typedefId) { return enumDictionary.get(typedefId); } /** * Enable resources runtime value notifications. * * @param resourceIdList * List of resource Identifiers. * @return True is connection successfully opened. */ public synchronized void enableRuntimeValueNotifications(List<? extends Integer> resourceIdList) throws IhcExecption { resourceInteractionService.enableRuntimeValueNotifications(resourceIdList); } /** * Wait runtime value notifications. * * Runtime value notification should firstly be activated by * enableRuntimeValueNotifications function. * * @param timeoutInSeconds * How many seconds to wait notifications. * @return List of received runtime value notifications. * @throws SocketTimeoutException */ private List<? extends WSResourceValue> waitResourceValueNotifications(int timeoutInSeconds) throws IhcExecption, SocketTimeoutException { IhcResourceInteractionService service = new IhcResourceInteractionService(ip, timeout); List<? extends WSResourceValue> list = service.waitResourceValueNotifications(timeoutInSeconds); for (WSResourceValue val : list) { resourceValues.put(val.getResourceID(), val); } return list; } /** * Query resource value from controller. * * * @param resoureId * Resource Identifier. * @return Resource value. */ public WSResourceValue resourceQuery(int resoureId) throws IhcExecption { return resourceInteractionService.resourceQuery(resoureId); } /** * Get resource value information. * * Function return resource value from internal memory, if data is not * available information is read from the controller. * * Resource value's value field (e.g. floatingPointValue) could be old * information. * * @param resoureId * Resource Identifier. * @return Resource value. */ public WSResourceValue getResourceValueInformation(int resourceId) throws IhcExecption { WSResourceValue data = resourceValues.get(resourceId); if (data == null) { // data is not available, read it from the controller data = resourceInteractionService.resourceQuery(resourceId); } return data; } /** * Update resource value to controller. * * * @param value * Resource value. * @return True if value is successfully updated. */ public boolean resourceUpdate(WSResourceValue value) throws IhcExecption { return resourceInteractionService.resourceUpdate(value); } /** * The IhcReader runs as a separate thread. * * Thread listen resource value notifications from IHC / ELKO LS controller * and post updates to openHAB bus when notifications are received. * */ private class IhcResourceValueNotificationListener extends Thread { private boolean interrupted = false; public void setInterrupted(boolean interrupted) { this.interrupted = interrupted; } @Override public void run() { logger.debug("IHC resource value listener started"); // as long as no interrupt is requested, continue running while (!interrupted) { waitResourceNotifications(); } logger.debug("IHC Listener stopped"); } private void waitResourceNotifications() { try { logger.trace("Wait new resource value notifications from controller"); List<? extends WSResourceValue> resourceValueList = waitResourceValueNotifications(10); logger.debug("{} new notifications received from controller", resourceValueList.size()); IhcStatusUpdateEvent event = new IhcStatusUpdateEvent(this); for (int i = 0; i < resourceValueList.size(); i++) { try { Iterator<IhcEventListener> iterator = eventListeners.iterator(); while (iterator.hasNext()) { iterator.next().resourceValueUpdateReceived(event, resourceValueList.get(i)); } } catch (Exception e) { logger.error("Event listener invoking error", e); } } } catch (SocketTimeoutException e) { logger.trace("Notifications timeout - no new notifications"); } catch (IhcExecption e) { logger.error("New notifications wait failed...", e); sendErrorEvent(e); mysleep(1000L); } } private void mysleep(long milli) { try { sleep(milli); } catch (InterruptedException e) { interrupted = true; } } } /** * The IhcReader runs as a separate thread. * * Thread listen controller state change notifications from IHC / ELKO LS * controller and . * */ private class IhcControllerStateListener extends Thread { private boolean interrupted = false; public void setInterrupted(boolean interrupted) { this.interrupted = interrupted; } @Override public void run() { logger.debug("IHC controller state listener started"); WSControllerState oldState = null; // as long as no interrupt is requested, continue running while (!interrupted) { try { if (oldState == null) { oldState = getControllerState(); logger.debug("Controller initial state {}", oldState.getState()); } logger.trace("Wait new state change notification from controller"); WSControllerState currentState = waitStateChangeNotifications(oldState, 10); logger.trace("Controller state {}", currentState.getState()); if (oldState.getState().equals(currentState.getState()) == false) { logger.info("Controller state change detected ({} -> {})", oldState.getState(), currentState.getState()); // send message to event listeners try { Iterator<IhcEventListener> iterator = eventListeners.iterator(); IhcStatusUpdateEvent event = new IhcStatusUpdateEvent(this); while (iterator.hasNext()) { iterator.next().statusUpdateReceived(event, currentState); } } catch (Exception e) { logger.error("Event listener invoking error", e); } oldState.setState(currentState.getState()); } } catch (IhcExecption e) { logger.error("New controller state change notification wait failed...", e); sendErrorEvent(e); mysleep(1000L); } } } private void mysleep(long milli) { try { sleep(milli); } catch (InterruptedException e) { interrupted = true; } } } private void sendErrorEvent(IhcExecption err) { // send error to event listeners try { Iterator<IhcEventListener> iterator = eventListeners.iterator(); IhcErrorEvent event = new IhcErrorEvent(this); while (iterator.hasNext()) { iterator.next().errorOccured(event, err); } } catch (Exception e) { logger.error("Event listener invoking error", e); } } }