package be.xhibit.teletask.client;
import be.xhibit.teletask.TeletaskReceiver;
import be.xhibit.teletask.client.builder.composer.MessageHandler;
import be.xhibit.teletask.client.builder.composer.MessageHandlerFactory;
import be.xhibit.teletask.client.builder.message.MessageUtilities;
import be.xhibit.teletask.client.builder.message.executor.MessageExecutor;
import be.xhibit.teletask.client.builder.message.messages.MessageSupport;
import be.xhibit.teletask.client.builder.message.messages.impl.EventMessage;
import be.xhibit.teletask.client.builder.message.messages.impl.GetMessage;
import be.xhibit.teletask.client.builder.message.messages.impl.LogMessage;
import be.xhibit.teletask.client.builder.message.messages.impl.SetMessage;
import be.xhibit.teletask.client.builder.message.strategy.KeepAliveStrategy;
import be.xhibit.teletask.client.listener.StateChangeListener;
import be.xhibit.teletask.client.mqtt.MqttPublisher;
import be.xhibit.teletask.client.mqtt.MqttStateChangeListener;
import be.xhibit.teletask.model.spec.ClientConfigSpec;
import be.xhibit.teletask.model.spec.ComponentSpec;
import be.xhibit.teletask.model.spec.Function;
import be.xhibit.teletask.server.TeletaskTestServer;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Created by IntelliJ IDEA.
* User: Bruno Braes, http://www.xhibit.be
* Date: 7/09/12
* Time: 15:40
* <p/>
* <p/>
* FunctionSet(int Fnc, int Opt, int Number, int State)
* > example to switch relays: FunctionSet(1, 0, 19, 1) -> switches relays 19 to on (=bureau).
* <p/>
* - Fnc: Fnc ( RELAY, DIMMER, MOTOR, MTRUPDOWN, LOCMOOD, TIMEDMOOD, GENMOOD, FLAG, PROCES, REGIME, SERVICE, MESSAGE) = see "Constants" below / Functions.cs for full list.
* - Opt: not required for RELAYS? value 0? (dependent on the function: see Options.cs for full list)
* - Number:
* for Fnc = FNC_RELAY, FNC_DIMMER Number = 1 to Maximum -> Number out the output relay
* for Fnc = FNC_TPKEY -> Number = Touch panel number you want to simulate
* All other Fnc -> Number = 0 to Maximum -1
* - State:
* for Fnc=FNC_DIMMER & FNC_MOTOR -> State = 0 to 255 (always use the result from function ConvPercToDimVal, may never be 3!!!)
* for Fnc = FNC_TPKEY
* -> State bit 3-0 = Key number (0 to 7)
* -> State bit 7-8 = 00 Normal Short Press, 01 Key Depressed, 10 Key Released
* for all other Fnc
* -> State = 0 or 255 (or 1) = OFF or ON
* <p/>
* Output: Return value:
* - 0 = Message successfully transmitted
* - 1 = Communication not opened
* - 2 = No Answer
* <p/>
* All commands and messages in both directions will use the same frame getLogInfo:
* STX (02h) + Length + Command Number + Parameter 1 + ... + Parameter n + ChkSm
* <p/>
* The length does not include the ChkSm-byte. The ChkSm is calculated on Command Number + Command Parameters + Length + STX.
* After the ChkSm the central unit send an acknowledge byte 0A (hex). If no acknowledge byte is send the command is not handled.
* <p/>
* --------------------------
* <p/>
* Function Set
* - Description: This command allows the CCT to set individual functions. See “methods” for detailed descriptions
* - Command number: 01h
* - Length: 6
* - Direction: From TDS to CCT.
* - Parameter 1 = Fnc
* - Parameter 2 = Outp
* - Parameter 3 = State
* <p/>
* Function Get
* - Description: When the TDS receives this command it reports the level of the specified load. See methods for detailed descriptions
* - Command number: 02h
* - Length: 5
* - Direction: From CCT to TDS
* - Parameter 1 = fnc
* - Parameter 2 = Outp
* <p/>
* Function Log On/Off
* - Description: When the TDS receives this command it (de-)activates it’s channel for reporting the function!
* This function will open/close a channel for the function
* Example: If you call this function with the parameter Fnc=FNC_RELAY and State=1, all changes on relays will occur as ‘event’! In case you set State=0 no more events will occur from relays.
* - Command number: 03h
* - Length: 5
* - Direction: From CCT to TDS
* - Parameter 1 = fnc
* - Parameter 2 = state
* <p/>
* Lux values
* To change from byte to lux = (10 (byte / 40)) - 1
* To change from lux to byte = Log10(lux + 1) * 40
* <p/>
* Constants
* The functions in the DLL use a parameter “Fnc” and can have following values
* FNC_RELAY = 1 (control or get the status of a relay)
* FNC_DIMMER = 2 (control or get the status of a dimmer)
* FNC_MOTOR = 6 (control or get the status of a Motor: On/Off)
* FNC_MTRUPDOWN = 55 (control or get the status of a Motor: Op/Down)
* FNC_LOCMOOD = 8 (control or get the status of a Local Mood)
* FNC_TIMEDMOOD = 9 (control or get the status of a Timed Local Mood)
* FNC_GENMOOD = 10 (control or get the status of a General Mood)
* FNC_FLAG = 15 (control or get the status of a Flag)
* FNC_PROCES = 3 (control or get the status of a Process function)
* FNC_REGIME = 14 (control or get the status of a Regime function)
* FNC_SERVICE = 53 (control or get the status of a Service function)
* FNC_MESSAGE = 54 (control or get the status of a Messages or Alarms)
* FNC_COND = 60 (get the status of a Condition)
* FNC_TPKEY = 52 (simulate a key press on an interface)
* FNC_GETSENSTARGET = 21 (get the status of a Sensor setting)
* <p/>
* If you are making your own interface you have to take care of the following:
* <p/>
* - With the LOG function you open a kind of 'channel' from the specific device type (ex. relays) from the central unit to your device.
* This mean that when such a device has a change the central unit will sent automatically a "report" to you.
* You only open the LOG for the devices you really need (ex. relays, dimmer, local moods, sensors)
* - When you want to know a state a a specific device you have to send a GET command, afterwards (asynchronously) you will get a "report" with the state of the device
* Normally it's not necessary to use this is you opened the LOG
* - When you want to know the state of several devices (at startup) you send a Group GET command for a specific type (ex. relays)with the numbers of all devices you want to know the state.
* Afterwards for every device you asked there will come a "report"
* - These reports are coming on the socket you open, so you have to check the bytes that are coming in, but you don't have to open a listener on a port.
* - You can send a keep alive to make sure that the central unit don't close the port because there is no activity
*/
public final class TeletaskClient implements TeletaskReceiver {
/**
* Logger responsible for logging and debugging statements.
*/
private static final Logger LOG = LoggerFactory.getLogger(TeletaskClient.class);
private Socket socket;
private OutputStream outputStream;
private InputStream inputStream;
private final ClientConfigSpec config;
private static TeletaskClient instance = null;
private final ExecutorService executorService;
private final Timer keepAliveTimer = new Timer();
private final Timer eventListenerTimer = new Timer();
private final List<StateChangeListener> stateChangeListeners = new ArrayList<>();
private TeletaskTestServer teletaskTestServer;
private EventMessageListener eventMessageListener;
/**
* Default constructor. Responsible for reading the client config (JSON).
* Singleton class. Private constructor to prevent new instance creations.
*/
private TeletaskClient(ClientConfigSpec config) {
this.config = config;
this.executorService = Executors.newSingleThreadExecutor();
}
/**
* Create of get an instance of the TDSClient.
*
* @return a new or existing TDSClient instance.
*/
public static synchronized TeletaskClient getInstance(ClientConfigSpec clientConfig) {
if (instance == null) {
instance = new TeletaskClient(clientConfig);
instance.start();
}
return instance;
}
// ################################################ PUBLIC API FUNCTIONS
public void registerStateChangeListener(StateChangeListener listener) {
this.stateChangeListeners.add(listener);
}
public void set(ComponentSpec component, String state) {
this.set(component.getFunction(), component.getNumber(), state);
}
public void set(Function function, int number, String state) {
Preconditions.checkNotNull(state, "Given state should not be null");
try {
this.execute(new SetMessage(this.getConfig(), function, number, state));
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
public void groupGet(final Function function, final int... numbers) {
try {
this.getExecutorService().submit(new Runnable() {
@Override
public void run() {
try {
TeletaskClient.this.getMessageHandler().getGroupGetStrategy().execute(TeletaskClient.this, function, numbers);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}).get();
} catch (InterruptedException e) {
LOG.error("Exception ({}) caught in groupGet: {}", e.getClass().getName(), e.getMessage(), e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
public void groupGet(Function function) {
List<? extends ComponentSpec> components = this.getConfig().getComponents(function);
if (components != null) {
this.groupGet(function, Ints.toArray(Lists.transform(components, new com.google.common.base.Function<ComponentSpec, Integer>() {
@Override
public Integer apply(ComponentSpec input) {
return input.getNumber();
}
})));
}
}
public void groupGet() {
for (Function function : Function.values()) {
this.groupGet(function);
}
// this.groupGet(Function.LOCMOOD);
}
private void sendLogEventMessages(String state) {
this.sendLogEventMessage(Function.RELAY, state);
this.sendLogEventMessage(Function.LOCMOOD, state);
this.sendLogEventMessage(Function.GENMOOD, state);
this.sendLogEventMessage(Function.MOTOR, state);
this.sendLogEventMessage(Function.DIMMER, state);
this.sendLogEventMessage(Function.COND, state);
this.sendLogEventMessage(Function.SENSOR, state);
}
public void get(Function function, int number) {
this.get(this.getConfig().getComponent(function, number));
}
public void get(ComponentSpec component) {
try {
this.execute(new GetMessage(this.getConfig(), component.getFunction(), component.getNumber()));
} catch (ExecutionException e) {
LOG.error("Exception ({}) caught in get: {}", e.getClass().getName(), e.getMessage(), e);
}
}
public void stop() {
// close all log events to stop reporting
this.sendLogEventMessages("OFF");
this.stopEventListener();
this.stopStateChangeListeners();
this.stopKeepAliveService();
this.stopExecutorService();
this.closeInputStream();
this.closeOutputStream();
this.closeSocket();
this.stopTestServer();
}
private void stopStateChangeListeners() {
for (StateChangeListener stateChangeListener : this.getStateChangeListeners()) {
stateChangeListener.stop();
}
}
private void stopTestServer() {
if (this.getTeletaskTestServer() != null) {
this.getTeletaskTestServer().stop();
}
}
private void stopKeepAliveService() {
this.getKeepAliveTimer().cancel();
}
private void stopEventListener() {
this.getEventListenerTimer().cancel();
}
private void closeSocket() {
try {
this.socket.close();
} catch (IOException e) {
LOG.error("Exception ({}) caught in stop: {}", e.getClass().getName(), e.getMessage(), e);
}
}
private void closeOutputStream() {
try {
this.getOutputStream().close();
} catch (IOException e) {
LOG.error("Exception ({}) caught in stop: {}", e.getClass().getName(), e.getMessage(), e);
}
}
private void closeInputStream() {
try {
this.getInputStream().close();
} catch (IOException e) {
LOG.error("Exception ({}) caught in stop: {}", e.getClass().getName(), e.getMessage(), e);
}
}
private void stopExecutorService() {
try {
this.getExecutorService().shutdown();
this.getExecutorService().awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
LOG.error("Exception ({}) caught in stop: {}", e.getClass().getName(), e.getMessage(), e);
}
}
@Override
public ClientConfigSpec getConfig() {
return this.config;
}
// ################################################ PRIVATE API FUNCTIONS
private void execute(MessageSupport message) throws ExecutionException {
try {
this.getExecutorService().submit(new MessageExecutor(message, this)).get();
} catch (InterruptedException e) {
LOG.error("Exception ({}) caught in execute: {}", e.getClass().getName(), e.getMessage(), e);
}
}
private void start() {
String host = this.getConfig().getHost();
int port = this.getConfig().getPort();
host = this.startTestServer(host, port);
this.registerMqttPublisher();
this.connect(host, port);
this.startEventListener();
this.groupGet();
this.startKeepAlive();
this.sendLogEventMessages("ON");
}
private void registerMqttPublisher() {
String mqttHost = System.getProperty("mqtt.host");
if (mqttHost != null) {
String mqttPort = System.getProperty("mqtt.port", "1883");
MqttPublisher publisher = new MqttPublisher(mqttHost, mqttPort);
this.registerStateChangeListener(new MqttStateChangeListener(publisher));
}
}
private String startTestServer(String host, int port) {
if (!Boolean.getBoolean("production")) {
LOG.debug("Starting test server...");
host = "localhost";
this.teletaskTestServer = new TeletaskTestServer(port, this);
new Thread(this.getTeletaskTestServer()).start();
LOG.debug("Started test server!");
}
return host;
}
private TeletaskTestServer getTeletaskTestServer() {
return this.teletaskTestServer;
}
private void startEventListener() {
this.setEventMessageListener(new EventMessageListener());
this.getEventListenerTimer().schedule(new TimerTask() {
@Override
public void run() {
TeletaskClient.this.getExecutorService().submit(TeletaskClient.this.getEventMessageListener());
// TeletaskClient.this.getEventMessageListener().run();
}
}, 0, 20);
}
private void connect(String host, int port) {
// Connect method
LOG.debug("Connecting to {}:{}", host, port);
try {
this.socket = new Socket(host, port);
this.socket.setKeepAlive(true);
this.socket.setSoTimeout(5000);
} catch (IOException e) {
LOG.error("Problem connecting to host: {}", host, e);
System.exit(1);
}
LOG.debug("Successfully Connected");
try {
this.outputStream = new DataOutputStream(this.socket.getOutputStream());
this.inputStream = new DataInputStream(this.socket.getInputStream());
} catch (IOException e) {
LOG.error("Couldn't get I/O for the connection to: {}:{}", host, port);
System.exit(1);
}
}
private void startKeepAlive() {
KeepAliveStrategy keepAliveStrategy = this.getMessageHandler().getKeepAliveStrategy();
this.getKeepAliveTimer().schedule(new KeepAliveService(keepAliveStrategy), 0, TimeUnit.MINUTES.toMillis(keepAliveStrategy.getIntervalMinutes()));
}
@Override
public MessageHandler getMessageHandler() {
return MessageHandlerFactory.getMessageHandler(this.getConfig().getCentralUnitType());
}
private void sendLogEventMessage(Function function, String state) {
try {
this.execute(new LogMessage(this.getConfig(), function, state));
} catch (ExecutionException e) {
LOG.error("Exception ({}) caught in sendLogEventMessage: {}", e.getClass().getName(), e.getMessage(), e);
}
}
/**
* Prevent cloning.
*
* @return Nothing really, because this will always result in an Exception.
* @throws CloneNotSupportedException when called.
*/
@Override
public final TeletaskClient clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
private ExecutorService getExecutorService() {
return this.executorService;
}
public OutputStream getOutputStream() {
return this.outputStream;
}
@Override
public InputStream getInputStream() {
return this.inputStream;
}
public class EventMessageListener implements Runnable {
@Override
public void run() {
try {
TeletaskClient.this.handleReceiveEvents(MessageUtilities.receive(LOG, TeletaskClient.this));
} catch (Exception e) {
LOG.error("Exception ({}) caught in run: {}", e.getClass().getName(), e.getMessage(), e);
}
}
}
public void handleReceiveEvents(Iterable<MessageSupport> messages) {
List<ComponentSpec> components = new ArrayList<>();
for (MessageSupport message : messages) {
if (message instanceof EventMessage) {
EventMessage eventMessage = (EventMessage) message;
this.handleReceiveEvent(LOG, this.getConfig(), eventMessage);
components.add(this.getConfig().getComponent(eventMessage.getFunction(), eventMessage.getNumber()));
}
}
if (!components.isEmpty()) {
for (StateChangeListener stateChangeListener : this.getStateChangeListeners()) {
stateChangeListener.receive(components);
}
}
}
public void handleReceiveEvent(Logger logger, ClientConfigSpec config, EventMessage eventMessage) {
ComponentSpec component = config.getComponent(eventMessage.getFunction(), eventMessage.getNumber());
if (component != null) {
if (logger.isDebugEnabled()) {
logger.debug("Event: \nComponent: {}\nCurrent State: {} {}", component.getDescription(), component.getState(), eventMessage.getLogInfo(eventMessage.getRawBytes()));
}
String state = eventMessage.getState();
if (component.getFunction() != Function.MOTOR || !"STOP".equals(state)) {
component.setState(state);
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("Event: \nComponent: not found in configuration {}", eventMessage.getLogInfo(eventMessage.getRawBytes()));
}
}
}
private class KeepAliveService extends TimerTask {
private final KeepAliveStrategy keepAliveStrategy;
public KeepAliveService(KeepAliveStrategy keepAliveStrategy) {
this.keepAliveStrategy = keepAliveStrategy;
}
@Override
public void run() {
try {
TeletaskClient.this.getExecutorService().execute(new Runnable() {
@Override
public void run() {
try {
KeepAliveService.this.keepAliveStrategy.execute(TeletaskClient.this);
} catch (Exception e) {
LOG.error("Exception ({}) caught in run: {} - Restarting Teletask Client Sockets", e.getClass().getName(), e.getMessage());
TeletaskClient.this.stop();
TeletaskClient.this.start();
}
}
});
} catch (Exception e) {
LOG.error("Exception ({}) caught in run: {}", e.getClass().getName(), e.getMessage(), e);
}
}
}
public Timer getKeepAliveTimer() {
return this.keepAliveTimer;
}
public Timer getEventListenerTimer() {
return this.eventListenerTimer;
}
public List<StateChangeListener> getStateChangeListeners() {
return this.stateChangeListeners;
}
public EventMessageListener getEventMessageListener() {
return this.eventMessageListener;
}
private void setEventMessageListener(EventMessageListener eventMessageListener) {
this.eventMessageListener = eventMessageListener;
}
}