/**
* 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.internal;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.EventObject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.ihc.IhcBindingProvider;
import org.openhab.binding.ihc.ws.IhcClient;
import org.openhab.binding.ihc.ws.IhcClient.ConnectionState;
import org.openhab.binding.ihc.ws.IhcEnumValue;
import org.openhab.binding.ihc.ws.IhcEventListener;
import org.openhab.binding.ihc.ws.IhcExecption;
import org.openhab.binding.ihc.ws.datatypes.WSControllerState;
import org.openhab.binding.ihc.ws.datatypes.WSEnumValue;
import org.openhab.binding.ihc.ws.datatypes.WSResourceValue;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.binding.BindingChangeListener;
import org.openhab.core.binding.BindingProvider;
import org.openhab.core.items.Item;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* IhcBinding order runtime value notifications from IHC / ELKO LS controller
* and post values to the openHAB event bus when notification is received.
*
* Binding also polls resources from controller where interval is configured.
*
* @author Pauli Anttila
* @author Simon Merschjohann
* @since 1.1.0
*/
public class IhcBinding extends AbstractActiveBinding<IhcBindingProvider>
implements ManagedService, IhcEventListener, BindingChangeListener {
private static final Logger logger = LoggerFactory.getLogger(IhcBinding.class);
private long refreshInterval = 1000;
/** Holds runtime notification reorder timeout in milliseconds */
private final int NOTIFICATIONS_REORDER_WAIT_TIME = 2000;
/** Holds time stamps in seconds when binding items states are refreshed */
private Map<String, Long> lastUpdateMap = new HashMap<String, Long>();
/** IHC / ELKO LS Controller client */
private static IhcClient ihc = null;
/** IP address of IHC / ELKO LS Controller */
private static String ip = null;
/** User name for controller authentication */
private static String username = null;
/** Password for controller authentication */
private static String password = null;
/**
* Path for IHC / ELKO lS project file, if it's empty/null project is
* download from controller
*/
private static String projectFile = null;
/** Path for resource's dump. Dump is useful to find out resource id's. */
private static String dumpResourceFile = null;
/** Timeout for controller communication */
private static int timeout = 5000;
/**
* Store current state of the controller, use to recognize when controller
* state is changed
*/
private WSControllerState controllerState = null;
/**
* Reminder to slow down resource value notification ordering from
* controller.
*/
private NotificationsRequestReminder reminder = null;
private boolean reconnectRequest = false;
private boolean valueNotificationRequest = false;
@Override
protected String getName() {
return "IHC / ELKO LS refresh and notification listener service";
}
@Override
protected long getRefreshInterval() {
return refreshInterval;
}
public void activate(ComponentContext componentContext) {
}
public void deactivate(ComponentContext componentContext) {
disconnect();
}
protected boolean isReconnectRequestActivated() {
synchronized (IhcBinding.class) {
return reconnectRequest;
}
}
protected void setReconnectRequest(boolean reconnect) {
synchronized (IhcBinding.class) {
this.reconnectRequest = reconnect;
}
}
protected boolean isValueNotificationRequestActivated() {
synchronized (IhcBinding.class) {
return valueNotificationRequest;
}
}
protected void setValueNotificationRequest(boolean valueNotificationRequest) {
synchronized (IhcBinding.class) {
this.valueNotificationRequest = valueNotificationRequest;
}
}
/**
* Initialize IHC client and open connection to IHC / ELKO LS controller.
*
*/
public void connect() throws IhcExecption {
if (StringUtils.isNotBlank(ip) && StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) {
logger.info("Connecting to IHC / ELKO LS controller [IP='{}' Username='{}' Password='{}'].",
new Object[] { ip, username, "******" });
ihc = new IhcClient(ip, username, password, timeout);
ihc.setProjectFile(projectFile);
ihc.setDumpResourceInformationToFile(dumpResourceFile);
ihc.openConnection();
controllerState = ihc.getControllerState();
ihc.addEventListener(this);
} else {
logger.warn(
"Couldn't connect to IHC controller because of missing connection parameters [IP='{}' Username='{}' Password='{}'].",
new Object[] { ip, username, "******" });
}
}
/**
* Disconnect connection to IHC / ELKO LS controller.
*
*/
public void disconnect() {
if (ihc != null) {
try {
ihc.removeEventListener(this);
ihc.closeConnection();
} catch (IhcExecption e) {
logger.error("Couldn't close connection to IHC controller", e);
}
}
}
/**
* @{inheritDoc
*/
@Override
public void execute() {
if (ihc == null || isReconnectRequestActivated()) {
try {
if (ihc != null) {
disconnect();
}
connect();
setReconnectRequest(false);
enableResourceValueNotifications();
} catch (IhcExecption e) {
logger.warn("Can't open connection to controller", e);
return;
}
}
if (ihc != null) {
if (isValueNotificationRequestActivated()) {
try {
enableResourceValueNotifications();
} catch (IhcExecption e) {
logger.warn("Can't enable resource value notifications from controller", e);
}
}
// Poll all requested resources from controller
for (IhcBindingProvider provider : providers) {
for (String itemName : provider.getItemNames()) {
int resourceId = provider.getResourceIdForInBinding(itemName);
int itemRefreshInterval = provider.getRefreshInterval(itemName) * 1000;
if (resourceId > 0 && itemRefreshInterval > 0) {
Long lastUpdateTimeStamp = lastUpdateMap.get(itemName);
if (lastUpdateTimeStamp == null) {
lastUpdateTimeStamp = 0L;
}
long age = System.currentTimeMillis() - lastUpdateTimeStamp;
boolean needsUpdate = age >= itemRefreshInterval;
if (needsUpdate) {
logger.debug("Item '{}' is about to be refreshed now", itemName);
try {
WSResourceValue resourceValue = null;
try {
resourceValue = ihc.resourceQuery(resourceId);
} catch (IhcExecption e) {
logger.warn("Value could not be read from controller - retrying one time.", e);
try {
resourceValue = ihc.resourceQuery(resourceId);
} catch (IhcExecption ex) {
logger.error("Communication error", ex);
logger.debug("Reconnection request");
setReconnectRequest(true);
}
}
if (resourceValue != null) {
Class<? extends Item> itemType = provider.getItemType(itemName);
State value = IhcDataConverter.convertResourceValueToState(itemType, resourceValue);
eventPublisher.postUpdate(itemName, value);
}
} catch (Exception e) {
logger.error("Error occured during resource query", e);
}
lastUpdateMap.put(itemName, System.currentTimeMillis());
}
}
}
}
} else {
logger.warn("Controller is not initialized => refresh cycle aborted!");
}
}
protected void addBindingProvider(IhcBindingProvider bindingProvider) {
super.addBindingProvider(bindingProvider);
}
protected void removeBindingProvider(IhcBindingProvider bindingProvider) {
super.removeBindingProvider(bindingProvider);
}
/**
* {@inheritDoc}
*/
@Override
public void allBindingsChanged(BindingProvider provider) {
logger.debug("allBindingsChanged");
setValueNotificationRequest(true);
}
/**
* {@inheritDoc}
*
*/
@Override
public void bindingChanged(BindingProvider provider, String itemName) {
logger.trace("bindingChanged {}", itemName);
if (reminder != null) {
reminder.cancel();
reminder = null;
}
reminder = new NotificationsRequestReminder(NOTIFICATIONS_REORDER_WAIT_TIME);
}
/**
* Used to slow down resource value notification ordering process. All
* resource values need to be ordered by one request from the controller,
* therefore wait that all binding items are loaded.
*/
private class NotificationsRequestReminder {
Timer timer;
public NotificationsRequestReminder(int milliseconds) {
timer = new Timer();
timer.schedule(new RemindTask(), milliseconds);
}
public void cancel() {
timer.cancel();
}
class RemindTask extends TimerTask {
@Override
public void run() {
logger.debug("Timer: enableResourceValueNotifications");
setValueNotificationRequest(true);
timer.cancel();
}
}
}
@Override
public void updated(Dictionary<String, ?> config) throws ConfigurationException {
logger.debug("Configuration updated, config {}", config != null ? true : false);
if (config != null) {
ip = (String) config.get("ip");
username = (String) config.get("username");
password = (String) config.get("password");
timeout = Integer.parseInt((String) config.get("timeout"));
projectFile = (String) config.get("projectFile");
dumpResourceFile = (String) config.get("dumpResourceFile");
setProperlyConfigured(true);
setReconnectRequest(true);
}
}
@Override
protected void internalReceiveCommand(String itemName, Command command) {
updateResource(itemName, command, false);
}
@Override
public void internalReceiveUpdate(String itemName, State newState) {
updateResource(itemName, newState, true);
}
/**
* Update resource value to IHC controller.
*/
private void updateResource(String itemName, Type type, boolean updateOnlyExclusiveOutBinding) {
if (itemName != null) {
Command cmd = null;
try {
cmd = (Command) type;
} catch (Exception e) {
}
IhcBindingProvider provider = findFirstMatchingBindingProvider(itemName, cmd);
if (provider == null) {
// command not configured, skip
return;
}
if (updateOnlyExclusiveOutBinding && provider.hasInBinding(itemName)) {
logger.trace("Ignore in binding update for item '{}'", itemName);
return;
}
logger.debug("Received update/command (item='{}', state='{}', class='{}')",
new Object[] { itemName, type.toString(), type.getClass().toString() });
if (ihc == null) {
logger.warn("Controller is not initialized, abort resource value update for item '{}'!", itemName);
return;
}
if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
logger.warn("Connection to controller is not ok, abort resource value update for item '{}'!", itemName);
return;
}
try {
int resourceId = provider.getResourceId(itemName, (Command) type);
logger.trace("found resourceId {} (item='{}', state='{}', class='{}')", new Object[] {
new Integer(resourceId).toString(), itemName, type.toString(), type.getClass().toString() });
if (resourceId > 0) {
WSResourceValue value = ihc.getResourceValueInformation(resourceId);
ArrayList<IhcEnumValue> enumValues = null;
if (value instanceof WSEnumValue) {
enumValues = ihc.getEnumValues(((WSEnumValue) value).getDefinitionTypeID());
}
// check if configuration has a custom value defined
// (0->OFF, 1->ON, >1->trigger)
// if that is the case, the type will be overridden with a
// new type
Integer val = provider.getValue(itemName, (Command) type);
boolean trigger = false;
if (val != null) {
if (val == 0) {
type = OnOffType.OFF;
} else if (val == 1) {
type = OnOffType.ON;
} else {
trigger = true;
}
} else {
// the original type is kept
}
if (!trigger) {
value = IhcDataConverter.convertCommandToResourceValue(type, value, enumValues);
boolean result = updateResource(value);
if (result == true) {
logger.debug("Item updated '{}' succesfully sent", itemName);
} else {
logger.error("Item '{}' update failed", itemName);
}
} else {
value = IhcDataConverter.convertCommandToResourceValue(OnOffType.ON, value, enumValues);
boolean result = updateResource(value);
if (result == true) {
logger.debug("Item '{}' trigger started", itemName);
Thread.sleep(val);
value = IhcDataConverter.convertCommandToResourceValue(OnOffType.OFF, value, enumValues);
result = updateResource(value);
if (result == true) {
logger.debug("Item '{}' trigger completed", itemName);
} else {
logger.error("Item '{}' trigger stop failed", itemName);
}
} else {
logger.error("Item '{}' update failed", itemName);
}
}
} else {
logger.error("resourceId invalid");
}
} catch (IhcExecption e) {
logger.error("Can't update Item '{}' value ", itemName, e);
} catch (Exception e) {
logger.error("Error occured during item update", e);
}
}
}
/**
* Update resource value to IHC controller.
*/
private boolean updateResource(WSResourceValue value) throws IhcExecption {
boolean result = false;
try {
result = ihc.resourceUpdate(value);
} catch (IhcExecption e) {
logger.warn("Value could not be set - retrying one time: {}", e.getMessage());
result = ihc.resourceUpdate(value);
}
return result;
}
/**
* Find the first matching {@link IhcBindingProvider} according to
* <code>itemName</code> and <code>command</code>.
*
* @param itemName
*
* @return the matching binding provider or <code>null</code> if no binding
* provider could be found
*/
private IhcBindingProvider findFirstMatchingBindingProvider(String itemName, Command type) {
IhcBindingProvider firstMatchingProvider = null;
for (IhcBindingProvider provider : this.providers) {
if (provider.getResourceId(itemName, type) > 0 || provider.getResourceId(itemName, null) > 0) {
firstMatchingProvider = provider;
break;
}
}
return firstMatchingProvider;
}
/**
* Order resource value notifications from IHC controller.
*/
private void enableResourceValueNotifications() throws IhcExecption {
logger.debug("Subscribe resource runtime value notifications");
if (ihc != null) {
if (ihc.getConnectionState() != ConnectionState.CONNECTED) {
logger.debug("Controller is connecting, abort subscribe");
return;
}
List<Integer> resourceIdList = new ArrayList<Integer>();
for (IhcBindingProvider provider : providers) {
for (String itemName : provider.getItemNames()) {
resourceIdList.add(provider.getResourceIdForInBinding(itemName));
}
}
if (resourceIdList.size() > 0) {
logger.debug("Enable runtime notfications for {} resources", resourceIdList.size());
try {
ihc.enableRuntimeValueNotifications(resourceIdList);
} catch (IhcExecption e) {
logger.debug("Reconnection request");
setReconnectRequest(true);
}
}
} else {
logger.warn("Controller is not initialized!");
logger.debug("Reconnection request");
setReconnectRequest(true);
}
setValueNotificationRequest(false);
}
@Override
public void statusUpdateReceived(EventObject event, WSControllerState state) {
logger.trace("Controller state {}", state.getState());
if (controllerState.getState().equals(state.getState()) == false) {
logger.info("Controller state change detected ({} -> {})", controllerState.getState(), state.getState());
if (controllerState.getState().equals(IhcClient.CONTROLLER_STATE_INITIALIZE)
|| state.getState().equals(IhcClient.CONTROLLER_STATE_READY)) {
logger.debug("Reconnection request");
setReconnectRequest(true);
}
controllerState.setState(state.getState());
}
}
@Override
public void resourceValueUpdateReceived(EventObject event, WSResourceValue value) {
for (IhcBindingProvider provider : providers) {
for (String itemName : provider.getItemNames()) {
int resourceId = provider.getResourceIdForInBinding(itemName);
if (value.getResourceID() == resourceId) {
if (!provider.hasInBinding(itemName)) {
logger.trace("{} has no inbinding...skip update to OpenHAB bus", itemName);
} else {
Class<? extends Item> itemType = provider.getItemType(itemName);
State state = IhcDataConverter.convertResourceValueToState(itemType, value);
logger.trace("Received resource value update (item='{}', state='{}')",
new Object[] { itemName, state });
eventPublisher.postUpdate(itemName, state);
}
}
}
}
}
@Override
public void errorOccured(EventObject event, IhcExecption e) {
logger.warn("Error occured on communication to IHC controller: {}", e.getMessage());
logger.debug("Reconnection request");
setReconnectRequest(true);
}
}