/**
* 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.homematic.internal.communicator;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.TimerTask;
import org.openhab.binding.homematic.internal.common.HomematicContext;
import org.openhab.binding.homematic.internal.communicator.ProviderItemIterator.ProviderItemIteratorCallback;
import org.openhab.binding.homematic.internal.communicator.client.BinRpcClient;
import org.openhab.binding.homematic.internal.communicator.client.CcuClient;
import org.openhab.binding.homematic.internal.communicator.client.HomegearClient;
import org.openhab.binding.homematic.internal.communicator.client.HomematicClientException;
import org.openhab.binding.homematic.internal.communicator.client.interfaces.HomematicClient;
import org.openhab.binding.homematic.internal.communicator.client.interfaces.RpcClient;
import org.openhab.binding.homematic.internal.communicator.server.BinRpcCallbackServer;
import org.openhab.binding.homematic.internal.config.BindingAction;
import org.openhab.binding.homematic.internal.config.binding.ActionConfig;
import org.openhab.binding.homematic.internal.config.binding.DatapointConfig;
import org.openhab.binding.homematic.internal.config.binding.HomematicBindingConfig;
import org.openhab.binding.homematic.internal.config.binding.ProgramConfig;
import org.openhab.binding.homematic.internal.config.binding.VariableConfig;
import org.openhab.binding.homematic.internal.converter.state.Converter;
import org.openhab.binding.homematic.internal.model.HmDatapoint;
import org.openhab.binding.homematic.internal.model.HmInterface;
import org.openhab.binding.homematic.internal.model.HmValueItem;
import org.openhab.binding.homematic.internal.util.DelayedExecutor;
import org.openhab.core.binding.BindingConfig;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The heart of the Homematic binding, this class handles the complete
* communication between a Homematic server and openHAB.
*
* @author Gerhard Riegler
* @since 1.5.0
*/
public class HomematicCommunicator implements HomematicCallbackReceiver {
private static final Logger logger = LoggerFactory.getLogger(HomematicCommunicator.class);
private HomematicContext context = HomematicContext.getInstance();
private DelayedExecutor delayedExecutor = new DelayedExecutor();
private int newDevicesCounter;
private HomematicCallbackServer homematicCallbackServer;
private HomematicClient homematicClient;
private ItemDisabler itemDisabler;
private long lastEventTime = System.currentTimeMillis();
private long lastReconnectTime = System.currentTimeMillis();
private HomematicPublisher publisher = new HomematicPublisher();
private Set<BindingConfig> sentPressEvents = Collections.synchronizedSet(new HashSet<BindingConfig>());
/**
* Starts the communicator and initializes everything.
*/
public void start() {
if (homematicCallbackServer == null) {
logger.info("Starting Homematic communicator");
try {
homematicCallbackServer = new BinRpcCallbackServer(this);
itemDisabler = new ItemDisabler();
itemDisabler.start();
newDevicesCounter = 0;
RpcClient rpcClient = new BinRpcClient();
context.setServerId(rpcClient.getServerId(HmInterface.RF));
logger.info("Homematic {}", context.getServerId());
homematicClient = context.getServerId().isHomegear() ? new HomegearClient(rpcClient)
: new CcuClient(rpcClient);
context.setHomematicClient(homematicClient);
homematicClient.start();
context.getStateHolder().init();
context.getStateHolder().loadDatapoints();
context.getStateHolder().loadVariables();
homematicCallbackServer.start();
homematicClient.registerCallback();
scheduleFirstRefresh();
lastReconnectTime = System.currentTimeMillis();
} catch (Exception e) {
logger.error("Could not start Homematic communicator: " + e.getMessage(), e);
stop();
}
}
}
/**
* Schedule refresh after one minute in case there is a value change between
* initial load and server startup
*/
private void scheduleFirstRefresh() {
logger.info("Scheduling one datapoint reload job in one minute");
delayedExecutor.schedule(new TimerTask() {
@Override
public void run() {
logger.debug("Initial Homematic datapoints reload");
context.getStateHolder().reloadDatapoints();
}
}, 61000); // 61 seconds to prevent reload at a reconnect
}
/**
* Stops the communicator.
*/
public void stop() {
if (homematicCallbackServer != null) {
logger.info("Shutting down Homematic communicator");
try {
delayedExecutor.cancel();
homematicCallbackServer.shutdown();
if (homematicClient != null) {
try {
homematicClient.releaseCallback();
} catch (HomematicClientException e) {
// ignore
}
try {
homematicClient.shutdown();
} catch (HomematicClientException e) {
// ignore
}
}
if (itemDisabler != null) {
itemDisabler.stop();
}
if (context.getStateHolder() != null) {
context.getStateHolder().destroy();
}
} finally {
homematicCallbackServer = null;
}
}
}
/**
* Receives a message from the Homematic server and publishes the state to
* openHAB.
*/
@Override
public void event(String interfaceId, String addressWithChannel, String parameter, Object value) {
boolean isVariable = "".equals(addressWithChannel);
HomematicBindingConfig bindingConfig = null;
if (isVariable) {
bindingConfig = new VariableConfig(parameter);
} else {
bindingConfig = new DatapointConfig(HmInterface.parse(interfaceId), addressWithChannel, parameter);
}
String className = value == null ? "Unknown" : value.getClass().getSimpleName();
logger.debug("Received new ({}) value '{}' for {}", className, value, bindingConfig);
lastEventTime = System.currentTimeMillis();
final Event event = new Event(bindingConfig, value);
if (sentPressEvents.remove(event.getBindingConfig())) {
logger.debug("Echo PRESS_* event detected, ignoring: {}", event.getBindingConfig());
} else {
if (context.getStateHolder().isDatapointReloadInProgress() && !isVariable) {
context.getStateHolder().addToRefreshCache(event.getBindingConfig(), event.getNewValue());
}
event.setHmValueItem(context.getStateHolder().getState(event.getBindingConfig()));
if (event.getHmValueItem() != null) {
event.getHmValueItem().setValue(event.getNewValue());
new ProviderItemIterator().iterate(event.getBindingConfig(), new ProviderItemIteratorCallback() {
@Override
public void next(HomematicBindingConfig providerBindingConfig, Item item, Converter<?> converter) {
State state = converter.convertFromBinding(event.getHmValueItem());
context.getEventPublisher().postUpdate(item.getName(), state);
if (state == OnOffType.ON) {
executeBindingAction(providerBindingConfig);
if (event.isPressValueItem()) {
itemDisabler.add(providerBindingConfig);
}
}
}
});
} else {
logger.warn("Can't find {}, value is not published to openHAB!", event.getBindingConfig());
}
}
}
/**
* Called on startup or when some binding has changed, for example if a item
* file is reloaded. Publishes the current States to openHAB.
*/
public void publishChangedItemToOpenhab(Item item, HomematicBindingConfig bindingConfig) {
HmValueItem hmValueItem = context.getStateHolder().getState(bindingConfig);
if (hmValueItem != null) {
Converter<?> converter = context.getConverterFactory().createConverter(item, bindingConfig);
if (converter != null) {
State state = converter.convertFromBinding(hmValueItem);
context.getEventPublisher().postUpdate(item.getName(), state);
}
} else if (bindingConfig instanceof ProgramConfig || bindingConfig instanceof ActionConfig) {
context.getEventPublisher().postUpdate(item.getName(), OnOffType.OFF);
} else {
logger.warn("Can't find {}, value is not published to openHAB!", bindingConfig);
}
}
/**
* Receives a update from openHAB and sends it to the Homematic server.
*/
public void receiveUpdate(Item item, State newState, HomematicBindingConfig bindingConfig) {
logger.debug("Received update {} for item {}", newState, item.getName());
Event event = new Event(item, newState, bindingConfig);
receiveType(event);
}
/**
* Receives a command from openHAB and sends it to the Homematic server.
*/
public void receiveCommand(Item item, Command command, HomematicBindingConfig bindingConfig) {
logger.debug("Received command {} for item {}", command, item.getName());
Event event = new Event(item, command, bindingConfig);
receiveType(event);
}
/**
* Receives updates/commands from openHAB and sends messages to the
* Homematic server.
*/
public void receiveType(Event event) {
if (event.isProgram()) {
if (event.isOnType()) {
executeProgram(event);
}
} else if (event.isAction()) {
if (event.isOnType()) {
executeBindingAction(event.getBindingConfig());
itemDisabler.add(event.getBindingConfig());
}
} else {
event.setHmValueItem(context.getStateHolder().getState(event.getBindingConfig()));
if (event.getHmValueItem() == null) {
logger.warn("Can't find {}, value is not published to Homematic server!", event.getBindingConfig());
} else {
try {
if (event.isStopLevelDatapoint()) {
homematicClient.setDatapointValue((HmDatapoint) event.getHmValueItem(), "STOP", true);
} else {
Converter<?> converter = context.getConverterFactory().createConverter(event.getItem(),
event.getBindingConfig());
if (converter != null) {
if (!event.isStopLevelDatapoint()) {
event.setNewValue(converter.convertToBinding(event.getType(), event.getHmValueItem()));
}
publishToHomematicServer(event);
publishToAllBindings(event);
if (event.isOnType()) {
executeBindingAction(event.getBindingConfig());
if (event.isPressValueItem()) {
itemDisabler.add(event.getBindingConfig());
}
}
}
}
} catch (Exception ex) {
logger.error(ex.getMessage(), ex);
context.getStateHolder().reloadDatapoints();
context.getStateHolder().reloadVariables();
}
}
}
}
/**
* Sends the event to the Homematic server.
*/
private void publishToHomematicServer(Event event) throws HomematicClientException {
if (event.isPressValueItem()) {
sentPressEvents.add(event.getBindingConfig());
}
if (!event.getHmValueItem().isWriteable()) {
logger.warn("Datapoint/Variable is not writeable, item is not published to the Homematic server: {}",
event.getBindingConfig());
}
else if (event.isNewValueEqual()) {
logger.debug("Value '{}' equals cached Homematic server value '{}' and forceUpdate is false, ignoring {}",
event.getHmValueItem().getValue(), event.getNewValue(), event.getBindingConfig());
}
else {
publisher.execute(event);
}
}
/**
* Publishes the event to all items bound to the same Homematic item.
*/
private void publishToAllBindings(final Event event) {
new ProviderItemIterator().iterate(event.getBindingConfig(), new ProviderItemIteratorCallback() {
@Override
public void next(HomematicBindingConfig providerBindingConfig, Item item, Converter<?> converter) {
if (!item.getName().equals(event.getItem().getName())) {
if (event.isCommand()) {
context.getEventPublisher().postCommand(item.getName(), (Command) event.getType());
} else {
State state = converter.convertFromBinding(event.getHmValueItem());
context.getEventPublisher().postUpdate(item.getName(), state);
}
}
}
});
}
/**
* Executes a program on the Homematic server.
*/
private void executeProgram(Event event) {
try {
homematicClient.executeProgram(((ProgramConfig) event.getBindingConfig()).getName());
} catch (HomematicClientException ex) {
logger.error(ex.getMessage(), ex);
} finally {
itemDisabler.add(event.getBindingConfig());
}
}
/**
* If a binding action is configured, execute it.
*/
private void executeBindingAction(HomematicBindingConfig bindingConfig) {
if (bindingConfig.getAction() != null) {
if (bindingConfig.getAction() == BindingAction.RELOAD_VARIABLES) {
context.getStateHolder().reloadVariables();
} else if (bindingConfig.getAction() == BindingAction.RELOAD_DATAPOINTS) {
context.getStateHolder().reloadDatapoints();
} else if (bindingConfig.getAction() == BindingAction.RELOAD_RSSI) {
context.getStateHolder().reloadRssi();
} else {
logger.warn("Unknown action {}", bindingConfig.getAction());
}
}
}
/**
* Called when the Homematic server detects a new device, datapoints are
* refreshed to have the new device in the cache.
*/
@Override
public void newDevices(String interfaceId, Object[] deviceDescriptions) {
if (newDevicesCounter < 3) {
newDevicesCounter++;
}
// prevent from duplicate loading at startup
if (newDevicesCounter > 2) {
logger.info("New device(s) detected, refreshing datapoints");
context.getStateHolder().reloadDatapoints();
}
}
/**
* Returns the timestamp from the last Homematic server event.
*/
public long getLastEventTime() {
return lastEventTime;
}
/**
* Returns the timestamp from the last Homematic server reconnect.
*/
public long getLastReconnectTime() {
return lastReconnectTime;
}
}