/**
* 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.powermax.internal.message;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.powermax.internal.connector.PowerMaxConnector;
import org.openhab.binding.powermax.internal.connector.PowerMaxEventListener;
import org.openhab.binding.powermax.internal.connector.PowerMaxSerialConnector;
import org.openhab.binding.powermax.internal.connector.PowerMaxTcpConnector;
import org.openhab.binding.powermax.internal.state.PowerMaxPanelSettings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A class that manages the communication with the Visonic alarm system
*
* Visonic does not provide a specification of the RS232 protocol and, thus,
* the binding uses the available protocol specification given at the ​domoticaforum
* http://www.domoticaforum.eu/viewtopic.php?f=68&t=6581
*
* @author Laurent Garnier
* @since 1.9.0
*/
public class PowerMaxCommDriver {
private static final Logger logger = LoggerFactory.getLogger(PowerMaxCommDriver.class);
private static final int DEFAULT_TCP_PORT = 80;
private static final int TCP_CONNECTION_TIMEOUT = 5000;
private static final int DEFAULT_BAUD_RATE = 9600;
private static final int WAITING_DELAY_FOR_RESPONSE = 750;
/** The unique instance of this class */
private static PowerMaxCommDriver theCommDriver = null;
/** The serial or TCP connecter used to communicate with the PowerMax alarm system */
private PowerMaxConnector connector = null;
/** The last message sent to the the PowerMax alarm system */
private PowerMaxBaseMessage lastSendMsg = null;
/** The message queue of messages to be sent to the the PowerMax alarm system */
private ConcurrentLinkedQueue<PowerMaxBaseMessage> msgQueue = new ConcurrentLinkedQueue<PowerMaxBaseMessage>();
private boolean downloadRunning = false;
/** The time in milliseconds used to set time and date */
private Long syncTimeCheck = null;
/**
* Constructor for Serial Connection
*
* @param sPort
* the serial port name
*/
private PowerMaxCommDriver(String sPort) {
String serialPort = StringUtils.isNotBlank(sPort) ? sPort : null;
if (serialPort != null) {
connector = new PowerMaxSerialConnector(serialPort, DEFAULT_BAUD_RATE);
} else {
connector = null;
}
}
/**
* Constructor for TCP connection
*
* @param ip
* the IP address
* @param port
* TCP port number; default port is used if value <= 0
*/
private PowerMaxCommDriver(String ip, int port) {
String ipAddress = StringUtils.isNotBlank(ip) ? ip : null;
int tcpPort = (port > 0) ? port : DEFAULT_TCP_PORT;
if (ipAddress != null) {
connector = new PowerMaxTcpConnector(ipAddress, tcpPort, TCP_CONNECTION_TIMEOUT);
} else {
connector = null;
}
}
/**
* Get the communication driver in charge of the communication with the PowerMax alarm system
*
* @return the unique instance of class PowerMaxCommDriver
*/
public static PowerMaxCommDriver getTheCommDriver() {
return theCommDriver;
}
/**
* Initialize a new communication driver in charge of the communication with the PowerMax alarm system
*
* @param sPort
* the serial port name
* @param ip
* the IP address
* @param port
* TCP port number; default port is used if value <= 0
*/
public static void initTheCommDriver(String sPort, String ip, int port) {
if (sPort != null) {
theCommDriver = new PowerMaxCommDriver(sPort);
} else if (ip != null) {
theCommDriver = new PowerMaxCommDriver(ip, port);
} else {
theCommDriver = null;
}
}
/**
* Add event listener
*
* @param listener
* the listener to be added
*/
public synchronized void addEventListener(PowerMaxEventListener listener) {
if (connector != null) {
connector.addEventListener(listener);
}
}
/**
* Remove event listener
*
* @param listener
* the listener to be removed
*/
public synchronized void removeEventListener(PowerMaxEventListener listener) {
if (connector != null) {
connector.removeEventListener(listener);
}
}
/**
* Connect to the PowerMax alarm system
*
* @return true if connected or false if not
*/
public boolean open() {
if (connector != null) {
connector.open();
}
lastSendMsg = null;
return isConnected();
}
/**
* Close the connection to the PowerMax alarm system.
*
* @return true if connected or false if not
*/
public boolean close() {
if (connector != null) {
connector.close();
}
downloadRunning = false;
return isConnected();
}
/**
* @return true if connected to the PowerMax alarm system or false if not
*/
public boolean isConnected() {
return (connector != null) && connector.isConnected();
}
/**
* @return the last message sent to the PowerMax alarm system
*/
public synchronized PowerMaxBaseMessage getLastSendMsg() {
return lastSendMsg;
}
/**
* @return the time in milliseconds used to set time and date
*/
public Long getSyncTimeCheck() {
return syncTimeCheck;
}
/**
* Compute the CRC of a message
*
* @param data
* the buffer containing the message
* @param len
* the size of the message in the buffer
*
* @return the computed CRC
*/
public static byte computeCRC(byte[] data, int len) {
long checksum = 0;
for (int i = 1; i < (len - 2); i++) {
checksum = checksum + (data[i] & 0x000000FF);
}
checksum = 0xFF - (checksum % 0xFF);
if (checksum == 0xFF) {
checksum = 0;
}
return (byte) checksum;
}
/**
* Send an ACK for a received message
*
* @param msg
* the received message object
* @param ackType
* the type of ACK to be sent
*
* @return true if the ACK was sent or false if not
*/
public synchronized boolean sendAck(PowerMaxBaseMessage msg, byte ackType) {
int code = msg.getCode();
byte[] rawData = msg.getRawData();
byte[] ackData;
if ((code >= 0x80) || ((code < 0x10) && (rawData[rawData.length - 3] == 0x43))) {
ackData = new byte[] { 0x0D, ackType, 0x43, 0x00, 0x0A };
} else {
ackData = new byte[] { 0x0D, ackType, 0x00, 0x0A };
}
if (logger.isDebugEnabled()) {
logger.debug("sendAck(): sending message {}", DatatypeConverter.printHexBinary(ackData));
}
boolean done = sendMessage(ackData);
if (!done) {
logger.debug("sendAck(): failed");
}
return done;
}
/**
* Send a message to the PowerMax alarm panel to change arm mode
*
* @param armMode
* the arm mode. Allowed values are: Disarmed, Stay, Armed,
* StayInstant, ArmedInstant, Night, NightInstant
* @param pinCode
* the PIN code. A string of 4 characters is expected
*
* @return true if the message was sent or false if not
*/
public boolean requestArmMode(String armMode, String pinCode) {
logger.debug("requestArmMode(): armMode = {}", armMode);
boolean done = false;
HashMap<String, Byte> codes = new HashMap<String, Byte>();
codes.put("Disarmed", (byte) 0x00);
codes.put("Stay", (byte) 0x04);
codes.put("Armed", (byte) 0x05);
codes.put("StayInstant", (byte) 0x14);
codes.put("ArmedInstant", (byte) 0x15);
codes.put("Night", (byte) 0x04);
codes.put("NightInstant", (byte) 0x14);
Byte code = codes.get(armMode);
if (code == null) {
logger.warn("PowerMax alarm binding: invalid requested arm mode: {}", armMode);
} else if ((pinCode == null) || (pinCode.length() != 4)) {
logger.warn("PowerMax alarm binding: requested arm mode rejected due to invalid PIN code: {}", armMode);
} else {
try {
byte[] dynPart = new byte[3];
dynPart[0] = code;
dynPart[1] = (byte) Integer.parseInt(pinCode.substring(0, 2), 16);
dynPart[2] = (byte) Integer.parseInt(pinCode.substring(2, 4), 16);
done = sendMessage(new PowerMaxBaseMessage(PowerMaxSendType.ARM, dynPart), false, 0);
} catch (NumberFormatException e) {
logger.warn("PowerMax alarm binding: requested arm mode rejected due to invalid PIN code: {}", armMode);
}
}
return done;
}
/**
* Send a message to the PowerMax alarm panel to change PGM or X10 zone state
*
* @param action
* the requested action. Allowed values are: OFF, ON, DIM, BRIGHT
* @param device
* the X10 device number. null is expected for PGM
*
* @return true if the message was sent or false if not
*/
public boolean sendPGMX10(String action, Byte device) {
logger.debug("sendPGMX10(): action = {}, device = {}", action, device);
boolean done = false;
HashMap<String, Byte> codes = new HashMap<String, Byte>();
codes.put("OFF", (byte) 0x00);
codes.put("ON", (byte) 0x01);
codes.put("DIM", (byte) 0x0A);
codes.put("BRIGHT", (byte) 0x0B);
Byte code = codes.get(action);
if (code == null) {
logger.warn("PowerMax alarm binding: invalid PGM/X10 command: {}", action);
} else if ((device != null)
&& ((device < 1) || (device >= PowerMaxPanelSettings.getThePanelSettings().getNbPGMX10Devices()))) {
logger.warn("PowerMax alarm binding: invalid X10 device id: {}", device);
} else {
int val = (device == null) ? 1 : (1 << device);
byte[] dynPart = new byte[3];
dynPart[0] = code;
dynPart[1] = (byte) (val & 0x000000FF);
dynPart[2] = (byte) (val >> 8);
done = sendMessage(new PowerMaxBaseMessage(PowerMaxSendType.X10PGM, dynPart), false, 0);
}
return done;
}
/**
* Send a message to the PowerMax alarm panel to bypass a zone or to not bypass a zone
*
* @param bypass
* true to bypass the zone; false to not bypass the zone
* @param zone
* the zone number (first zone is number 1)
* @param pinCode
* the PIN code. A string of 4 characters is expected
*
* @return true if the message was sent or false if not
*/
public boolean sendZoneBypass(boolean bypass, byte zone, String pinCode) {
logger.debug("sendZoneBypass(): bypass = {}, zone = {}", bypass ? "true" : "false", zone);
boolean done = false;
if ((pinCode == null) || (pinCode.length() != 4)) {
logger.warn("PowerMax alarm binding: zone bypass rejected due to invalid PIN code");
} else if ((zone < 1) || (zone > PowerMaxPanelSettings.getThePanelSettings().getNbZones())) {
logger.warn("PowerMax alarm binding: invalid zone number: {}", zone);
} else {
try {
int val = (1 << (zone - 1));
byte[] dynPart = new byte[10];
dynPart[0] = (byte) Integer.parseInt(pinCode.substring(0, 2), 16);
dynPart[1] = (byte) Integer.parseInt(pinCode.substring(2, 4), 16);
int i;
for (i = 2; i < 10; i++) {
dynPart[i] = 0;
}
i = bypass ? 2 : 6;
dynPart[i++] = (byte) (val & 0x000000FF);
dynPart[i++] = (byte) ((val >> 8) & 0x000000FF);
dynPart[i++] = (byte) ((val >> 16) & 0x000000FF);
dynPart[i++] = (byte) ((val >> 24) & 0x000000FF);
done = sendMessage(new PowerMaxBaseMessage(PowerMaxSendType.BYPASS, dynPart), false, 0);
if (done) {
done = sendMessage(new PowerMaxBaseMessage(PowerMaxSendType.BYPASSTAT), false, 0);
}
} catch (NumberFormatException e) {
logger.warn("PowerMax alarm binding: zone bypass rejected due to invalid PIN code");
}
}
return done;
}
/**
* Send a message to set the alarm time and date using the system time and date
*
* @return true if the message was sent or false if not
*/
public boolean sendSetTime() {
logger.debug("sendSetTime()");
boolean done = false;
GregorianCalendar cal = new GregorianCalendar();
if (cal.get(Calendar.YEAR) >= 2000) {
logger.debug(String.format("sendSetTime(): sync time %02d/%02d/%04d %02d:%02d:%02d",
cal.get(Calendar.DAY_OF_MONTH), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.YEAR),
cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND)));
byte[] dynPart = new byte[6];
dynPart[0] = (byte) cal.get(Calendar.SECOND);
dynPart[1] = (byte) cal.get(Calendar.MINUTE);
dynPart[2] = (byte) cal.get(Calendar.HOUR_OF_DAY);
dynPart[3] = (byte) cal.get(Calendar.DAY_OF_MONTH);
dynPart[4] = (byte) (cal.get(Calendar.MONTH) + 1);
dynPart[5] = (byte) (cal.get(Calendar.YEAR) - 2000);
done = sendMessage(new PowerMaxBaseMessage(PowerMaxSendType.SETTIME, dynPart), false, 0);
cal.set(Calendar.MILLISECOND, 0);
syncTimeCheck = cal.getTimeInMillis();
} else {
logger.warn(
"PowerMax alarm binding: time not synchronized; please correct the date/time of your openHAB server");
syncTimeCheck = null;
}
return done;
}
/**
* Send a message to the PowerMax alarm panel to get all the event logs
*
* @param pinCode
* the PIN code. A string of 4 characters is expected
*
* @return true if the message was sent or false if not
*/
public boolean requestEventLog(String pinCode) {
logger.debug("requestEventLog()");
boolean done = false;
if ((pinCode == null) || (pinCode.length() != 4)) {
logger.warn("PowerMax alarm binding: requested event log rejected due to invalid PIN code");
} else {
try {
byte[] dynPart = new byte[3];
dynPart[0] = (byte) Integer.parseInt(pinCode.substring(0, 2), 16);
dynPart[1] = (byte) Integer.parseInt(pinCode.substring(2, 4), 16);
done = sendMessage(new PowerMaxBaseMessage(PowerMaxSendType.EVENTLOG, dynPart), false, 0);
} catch (NumberFormatException e) {
logger.warn("PowerMax alarm binding: requested event log rejected due to invalid PIN code");
}
}
return done;
}
/**
* Start downloading panel setup
*
* @return true if the message was sent or the sending is delayed; false in other cases
*/
public synchronized boolean startDownload() {
if (downloadRunning) {
logger.info("PowerMax alarm binding: download not started as one is in progress");
return false;
} else {
downloadRunning = true;
return sendMessage(PowerMaxSendType.DOWNLOAD);
}
}
/**
* Act the exit of the panel setup
*/
public synchronized void exitDownload() {
downloadRunning = false;
}
/**
* Send a ENROLL message
*
* @return true if the message was sent or the sending is delayed; false in other cases
*/
public boolean enrollPowerlink() {
return sendMessage(new PowerMaxBaseMessage(PowerMaxSendType.ENROLL), true, 0);
}
/**
* Send a message or delay the sending if time frame for receiving response is not ended
*
* @param msgType
* the message type to be sent
*
* @return true if the message was sent or the sending is delayed; false in other cases
*/
public boolean sendMessage(PowerMaxSendType msgType) {
return sendMessage(new PowerMaxBaseMessage(msgType), false, 0);
}
/**
* Delay the sending of a message
*
* @param msgType
* the message type to be sent
*
* @param waitTime
* the delay in seconds to wait
*
* @return true if the sending is delayed; false in other cases
*/
public boolean sendMessageLater(PowerMaxSendType msgType, int waitTime) {
return sendMessage(new PowerMaxBaseMessage(msgType), false, waitTime);
}
/**
* Send a message or delay the sending if time frame for receiving response is not ended
*
* @param msg
* the message to be sent
* @param immediate
* true if the message has to be send without considering timing
* @param waitTime
* the delay in seconds to wait
*
* @return true if the message was sent or the sending is delayed; false in other cases
*/
private synchronized boolean sendMessage(PowerMaxBaseMessage msg, boolean immediate, int waitTime) {
if ((waitTime > 0) && (msg != null)) {
logger.debug("sendMessage(): delay ({} s) sending message (type {})", waitTime,
msg.getSendType().toString());
Timer timer = new Timer();
// Don't queue the message
timer.schedule(new DelayedSendTask(msg), waitTime * 1000);
return true;
}
if (msg == null) {
msg = msgQueue.peek();
if (msg == null) {
logger.debug("sendMessage(): nothing to send");
return false;
}
}
// Delay sending if time frame for receiving response is not ended
long delay = WAITING_DELAY_FOR_RESPONSE - (System.currentTimeMillis() - connector.getWaitingForResponse());
PowerMaxBaseMessage msgToSend = msg;
if (!immediate) {
msgToSend = msgQueue.peek();
if (msgToSend != msg) {
logger.debug("sendMessage(): add message in queue (type {})", msg.getSendType().toString());
msgQueue.offer(msg);
msgToSend = msgQueue.peek();
}
if ((msgToSend != msg) && (delay > 0)) {
return true;
} else if ((msgToSend == msg) && (delay > 0)) {
if (delay < 100) {
delay = 100;
}
logger.debug("sendMessage(): delay ({} ms) sending message (type {})", delay,
msgToSend.getSendType().toString());
Timer timer = new Timer();
timer.schedule(new DelayedSendTask(null), delay);
return true;
} else {
msgToSend = msgQueue.poll();
}
}
if (logger.isDebugEnabled()) {
logger.debug("sendMessage(): sending {} message {}", msgToSend.getSendType().toString(),
DatatypeConverter.printHexBinary(msgToSend.getRawData()));
}
boolean done = sendMessage(msgToSend.getRawData());
if (done) {
lastSendMsg = msgToSend;
connector.setWaitingForResponse(System.currentTimeMillis());
if (!immediate && (msgQueue.peek() != null)) {
logger.debug("sendMessage(): delay sending next message (type {})",
msgQueue.peek().getSendType().toString());
Timer timer = new Timer();
timer.schedule(new DelayedSendTask(null), WAITING_DELAY_FOR_RESPONSE);
}
} else {
logger.debug("sendMessage(): failed");
}
return done;
}
/**
* Send a message to the PowerMax alarm panel
*
* @param data
* the data buffer containing the message to be sent
*
* @return true if the message was sent or false if not
*/
private boolean sendMessage(byte[] data) {
boolean done = false;
if (isConnected()) {
data[data.length - 2] = computeCRC(data, data.length);
connector.sendMessage(data);
done = connector.isConnected();
} else {
logger.debug("sendMessage(): aborted (not connected)");
}
return done;
}
/**
* A class used to delay the sending of a message
*/
private class DelayedSendTask extends TimerTask {
private PowerMaxBaseMessage msg;
private DelayedSendTask(PowerMaxBaseMessage msg) {
super();
this.msg = msg;
}
@Override
public void run() {
logger.debug("Time to send next message");
sendMessage(msg, false, 0);
}
}
}