/**
* 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);
}
}
}