/**
* Copyright (c) 2010-2014, openHAB.org and others.
*
* 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.plugwise.internal;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.TooManyListenersException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.plugwise.PlugwiseCommandType;
import org.openhab.binding.plugwise.protocol.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.quartz.JobBuilder.*;
import static org.quartz.TriggerBuilder.*;
import static org.quartz.SimpleScheduleBuilder.*;
import org.quartz.impl.matchers.KeyMatcher;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import gnu.io.CommPortIdentifier;
import gnu.io.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.SerialPortEvent;
import gnu.io.SerialPortEventListener;
import gnu.io.UnsupportedCommOperationException;
/**
* This class represents a Plugwise Stick that is connected to a serial port on the host.
* This class borrows heavily from the Serial binding for the serial port communication
*
* @author Karel Goderis
* @since 1.1.0
*/
public class Stick extends PlugwiseDevice implements SerialPortEventListener{
private static final Logger logger = LoggerFactory.getLogger(Stick.class);
/** Plugwise protocol header code (hex) */
private final static String PROTOCOL_HEADER = "\u0005\u0005\u0003\u0003";
/** Plugwise protocol trailer code (hex) */
private final static String PROTOCOL_TRAILER = "\r\n";
/** counter to track Quartz Jobs */
private static int counter=0;
// Serial communication fields
private String port;
private CommPortIdentifier portId;
private SerialPort serialPort;
private WritableByteChannel outputChannel;
private ByteBuffer readBuffer;
// Queue fields
protected static int maxBufferSize = 1024;
protected final ReentrantLock queueLock = new ReentrantLock();
protected final ReentrantLock receiveLock = new ReentrantLock();
protected ArrayBlockingQueue<Message> sendQueue = new ArrayBlockingQueue<Message>(maxBufferSize,true);
protected ArrayBlockingQueue<Message> sentQueue = new ArrayBlockingQueue<Message>(maxBufferSize,true);
protected ArrayBlockingQueue<Message> receivedQueue = new ArrayBlockingQueue<Message>(maxBufferSize,true);
// Stick fields
private boolean initialised = false;
protected List<PlugwiseDevice> plugwiseDeviceCache = Collections.synchronizedList(new ArrayList<PlugwiseDevice>());
private PlugwiseBinding binding;
/** default interval for sending messages on the ZigBee network */
private int interval = 50 ;
/** default maximum number of attempts to send a message */
private int maxRetries = 1;
public Stick(String port, PlugwiseBinding binding) {
super("", PlugwiseDevice.DeviceType.Stick, "stick");
this.port = port;
this.binding = binding;
plugwiseDeviceCache.add(this);
try {
initialize();
} catch (PlugwiseInitializationException e) {
logger.error("Failed to initialize Plugwise stick: {}", e.getLocalizedMessage());
initialised = false;
}
}
protected static Comparator<PlugwiseDevice> plugComparator = new Comparator<PlugwiseDevice>() {
public int compare(PlugwiseDevice u1, PlugwiseDevice u2) {
return u1.getMAC().compareTo(u2.getMAC());
}
};
protected static Comparator<PlugwiseDevice> friendlyPlugComparator = new Comparator<PlugwiseDevice>() {
public int compare(PlugwiseDevice u1, PlugwiseDevice u2) {
return u1.getFriendlyName().compareTo(u2.getFriendlyName());
}
};
protected PlugwiseDevice getDevice(String id) {
PlugwiseDevice someDevice = getDeviceByMAC(id);
if(someDevice == null) {
return getDeviceByName(id);
} else {
return someDevice;
}
}
protected PlugwiseDevice getDeviceByMAC(String MAC) {
PlugwiseDevice queryDevice = new PlugwiseDevice(MAC, null,"");
Collections.sort(plugwiseDeviceCache,plugComparator);
int index = Collections.binarySearch(plugwiseDeviceCache, queryDevice , plugComparator);
if(index >= 0) {
return plugwiseDeviceCache.get(index);
}
else {
return null;
}
}
protected PlugwiseDevice getDeviceByName(String name) {
PlugwiseDevice queryDevice = new PlugwiseDevice(null, null,name);
Collections.sort(plugwiseDeviceCache,friendlyPlugComparator);
int index = Collections.binarySearch(plugwiseDeviceCache, queryDevice , friendlyPlugComparator);
if(index >= 0) {
return plugwiseDeviceCache.get(index);
}
else {
return null;
}
}
public String getPort() {
return port;
}
public void setInterval(int interval) {
this.interval = interval;
}
public void setRetries(int retries) {
this.maxRetries = retries;
}
public boolean isInitialised() {
return initialised;
}
/**
* Initialize this device and open the serial port
*
* @throws PlugwiseInitializationException if port can not be opened
*/
@SuppressWarnings("rawtypes")
private void initialize() throws PlugwiseInitializationException {
//Flush the deviceCache
if(this.plugwiseDeviceCache!=null) {
plugwiseDeviceCache = Collections.synchronizedList(new ArrayList<PlugwiseDevice>());
}
// parse ports and if the default port is found, initialized the reader
Enumeration portList = CommPortIdentifier.getPortIdentifiers();
while (portList.hasMoreElements()) {
CommPortIdentifier id = (CommPortIdentifier) portList.nextElement();
if (id.getPortType() == CommPortIdentifier.PORT_SERIAL) {
if (id.getName().equals(port)) {
logger.debug("Serial port '{}' has been found.", port);
portId = id;
}
}
}
if (portId != null) {
// initialize serial port
try {
serialPort = (SerialPort) portId.open("openHAB", 2000);
} catch (PortInUseException e) {
throw new PlugwiseInitializationException(e);
}
try {
serialPort.addEventListener(this);
} catch (TooManyListenersException e) {
throw new PlugwiseInitializationException(e);
}
// activate the DATA_AVAILABLE notifier
serialPort.notifyOnDataAvailable(true);
try {
// set port parameters
serialPort.setSerialPortParams(115200, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
} catch (UnsupportedCommOperationException e) {
throw new PlugwiseInitializationException(e);
}
try {
// get the output stream
outputChannel = Channels.newChannel(serialPort.getOutputStream());
} catch (IOException e) {
throw new PlugwiseInitializationException(e);
}
} else {
StringBuilder sb = new StringBuilder();
portList = CommPortIdentifier.getPortIdentifiers();
while (portList.hasMoreElements()) {
CommPortIdentifier id = (CommPortIdentifier) portList.nextElement();
if (id.getPortType() == CommPortIdentifier.PORT_SERIAL) {
sb.append(id.getName() + "\n");
}
}
throw new PlugwiseInitializationException("Serial port '" + port + "' could not be found. Available ports are:\n" + sb.toString());
}
// set up the Quartz jobs
Scheduler sched = null;
try {
sched = StdSchedulerFactory.getDefaultScheduler();
} catch (SchedulerException e) {
logger.error("Error getting a reference to the Quartz Scheduler");
}
JobDataMap map = new JobDataMap();
map.put("Stick", this);
JobDetail job = newJob(SendJob.class)
.withIdentity("Send-0", "Plugwise")
.usingJobData(map)
.build();
Trigger trigger = newTrigger()
.withIdentity("Send-0", "Plugwise")
.startNow()
.build();
try {
sched.getListenerManager().addJobListener(new SendJobListener("JobListener-"+job.getKey().toString()), KeyMatcher.keyEquals(job.getKey()));
} catch (SchedulerException e1) {
logger.error("An exception occured while attaching a Quartz Send Job Listener");
}
try {
sched.scheduleJob(job, trigger);
} catch (SchedulerException e) {
logger.error("Error scheduling a job with the Quartz Scheduler");
}
map = new JobDataMap();
map.put("Stick", this);
job = newJob(ProcessMessageJob.class)
.withIdentity("ProcessMessage", "Plugwise")
.usingJobData(map)
.build();
trigger = newTrigger()
.withIdentity("ProcessMessage", "Plugwise")
.startNow()
.withSchedule(simpleSchedule()
.repeatForever()
.withIntervalInMilliseconds(50))
.build();
try {
sched.scheduleJob(job, trigger);
} catch (SchedulerException e) {
logger.error("Error scheduling a job with the Quartz Scheduler");
}
// initialise the Stick
initialised = true;
InitialiseRequestMessage message = new InitialiseRequestMessage();
sendMessage(message);
}
/**
* Close this serial device associated with the Stick
*/
public void close() {
serialPort.removeEventListener();
try {
IOUtils.closeQuietly(serialPort.getInputStream());
IOUtils.closeQuietly(serialPort.getOutputStream());
serialPort.close();
} catch (IOException e) {
logger.error("An exception occurred while closing the serial port {} ({})", serialPort,e.getMessage());
}
initialised = false;
}
public void serialEvent(SerialPortEvent event) {
switch (event.getEventType()) {
case SerialPortEvent.BI:
case SerialPortEvent.OE:
case SerialPortEvent.FE:
case SerialPortEvent.PE:
case SerialPortEvent.CD:
case SerialPortEvent.CTS:
case SerialPortEvent.DSR:
case SerialPortEvent.RI:
case SerialPortEvent.OUTPUT_BUFFER_EMPTY:
break;
case SerialPortEvent.DATA_AVAILABLE:
// we get here if data has been received
boolean newlineFound = false;
if(readBuffer == null) {
readBuffer = ByteBuffer.allocate(maxBufferSize);
}
try {
// read data from serial device
while (serialPort.getInputStream().available() > 0) {
int aByte = serialPort.getInputStream().read();
if ((aByte)==13) {
readBuffer.put((byte)aByte);
int cr = serialPort.getInputStream().read();
readBuffer.put((byte)cr);
newlineFound = true;
break;
}
//Plugwise sends ASCII data, but for some unknown reason we sometimes get data with unsigned byte value >127
//which in itself is very strange. We filter these out for the time being
if(aByte < 128) {
readBuffer.put((byte)aByte);
}
}
// process data
if(readBuffer.position()!=0 && newlineFound==true) {
readBuffer.flip();
parseAndQueue(readBuffer);
readBuffer = null;
}
} catch (IOException e) {
logger.debug("Error receiving data on serial port {}: {}", new String[] { port, e.getMessage() });
}
break;
}
}
public void sendMessage(Message message) {
if(message!= null && isInitialised() ) {
try {
logger.debug("sendMessage: Stick send message: {}", message.toString());
sendQueue.put(message);
} catch (InterruptedException e) {
logger.error("Error sending Plugwise message: {}", message.toString());
}
}
}
public boolean postUpdate(String MAC, PlugwiseCommandType type, Object value) {
if(MAC != null && type != null && value != null) {
binding.postUpdate(MAC,type, value);
return true;
} else {
return false;
}
}
/**
* Parse a buffer into a Message and put it in the appropriate queue for further processing
*
* @param readBuffer - the string to parse
*/
private void parseAndQueue(ByteBuffer readBuffer) {
if(readBuffer != null) {
Pattern RESPONSE_PATTERN = Pattern.compile("(.{4})(\\w{4})(\\w{4})(\\w*?)(\\w{4})");
String response = new String(readBuffer.array(), 0, readBuffer.limit());
response = StringUtils.chomp(response);
Matcher matcher = RESPONSE_PATTERN.matcher(response);
if(matcher.matches()) {
String protocolHeader = matcher.group(1);
String command = matcher.group(2);
String sequence = matcher.group(3);
String payload = matcher.group(4);
String CRC = matcher.group(5);
if(protocolHeader.equals(PROTOCOL_HEADER)) {
String calculatedCRC = getCRCFromString(command + sequence + payload);
if(calculatedCRC.equals(CRC)) {
logger.debug("parseAndQueue: Parsing Plugwise protocol data unit: command:{} sequence:{} payload:{}", new String[] { MessageType.forValue(Integer.parseInt(command,16)).toString(), Integer.toString(Integer.parseInt(sequence,16)),payload});
Message theMessage = null;
switch(MessageType.forValue(Integer.parseInt(command,16))){
case ACKNOWLEDGEMENT:
theMessage = new AcknowledgeMessage(Integer.parseInt(sequence,16), payload);
break;
case NODE_AVAILABLE:
theMessage = new NodeAvailableMessage(Integer.parseInt(sequence,16), payload);
break;
case INITIALISE_RESPONSE:
theMessage = new InitialiseResponseMessage(Integer.parseInt(sequence,16), payload);
break;
case DEVICE_ROLECALL_RESPONSE:
theMessage = new RoleCallResponseMessage(Integer.parseInt(sequence,16), payload);
break;
case DEVICE_CALIBRATION_RESPONSE:
theMessage = new CalibrationResponseMessage(Integer.parseInt(sequence,16), payload);
break;
case DEVICE_INFORMATION_RESPONSE:
theMessage = new InformationResponseMessage(Integer.parseInt(sequence,16), payload);
break;
case REALTIMECLOCK_GET_RESPONSE:
theMessage = new RealTimeClockGetResponseMessage(Integer.parseInt(sequence,16), payload);
break;
case CLOCK_GET_RESPONSE:
theMessage = new ClockGetResponseMessage(Integer.parseInt(sequence,16), payload);
break;
case POWER_BUFFER_RESPONSE:
theMessage = new PowerBufferResponseMessage(Integer.parseInt(sequence,16), payload);
break;
case POWER_INFORMATION_RESPONSE:
theMessage = new PowerInformationResponseMessage(Integer.parseInt(sequence,16), payload);
break;
default:
logger.debug("parseAndQueue: Received unrecognized Plugwise protocol data unit: command:{} sequence:{} payload:{}", new String[] { command, Integer.toString(Integer.parseInt(sequence,16)),payload});
break;
};
if(theMessage != null) {
try {
receiveLock.lock();
logger.debug("parseAndQueue: {} messages before the message ({}) put in the receiveQ",receivedQueue.size(),theMessage.toString());
receivedQueue.put(theMessage);
receiveLock.unlock();
} catch (InterruptedException e) {
logger.error("Error queueing Plugwise protocol data unit: command:{} sequence:{} payload:{}", new String[] { MessageType.forValue(Integer.parseInt(command,16)).toString(), Integer.toString(Integer.parseInt(sequence,16)),payload});
}
}
} else {
logger.error("Plugwise protocol CRC error: {} does not match {} in message", new String[] { calculatedCRC, CRC});
}
} else {
logger.debug("parseAndQueue: Plugwise protocol header error: {} in message {}", new String[] { protocolHeader, response});
}
} else {
if(!response.contains("APSRequestNodeInfo")) {
logger.error("Plugwise protocol message error: {} ", response);
}
}
}
}
public boolean processMessage(Message message) {
if(message!=null) {
// deal with the messages that are destined to a very specific plugwise device, and only if we already have a reference to them
switch(message.getType()) {
case ACKNOWLEDGEMENT:
if(((AcknowledgeMessage)message).isExtended()) {
switch(((AcknowledgeMessage)message).getExtensionCode()) {
case CIRCLEPLUS:
CirclePlus circlePlus11 = (CirclePlus) getDeviceByMAC(((AcknowledgeMessage)message).getCirclePlusMAC());
if(!((AcknowledgeMessage)message).getCirclePlusMAC().equals("") && circlePlus11==null) {
circlePlus11 = new CirclePlus(((AcknowledgeMessage)message).getCirclePlusMAC(),this);
plugwiseDeviceCache.add(circlePlus11);
logger.debug("Added a CirclePlus with MAC {} to the cache", circlePlus11.getMAC());
}
circlePlus11.updateInformation();
circlePlus11.calibrate();
circlePlus11.setClock();
if(circlePlus11 != null) {
// initiate a "role call" request in the network
circlePlus11.roleCall(0);
}
break;
case TIMEOUT:
// we put the message back in the queue, without tagging it
logger.error("Timeout sending Plugwise message : {}", ((AcknowledgeMessage)message).toString());
// traverse the sent Q for the
Iterator<Message> messageIterator= sentQueue.iterator();
Message aMessage = null;
while(messageIterator.hasNext()) {
aMessage = messageIterator.next();
if(aMessage.getSequenceNumber() == message.getSequenceNumber()) {
logger.debug("processMessage: timeout : removing a msg from the senTq: {}",aMessage.toString());
sentQueue.remove(aMessage);
break;
}
}
if(aMessage != null) {
//reset the sequence number and put it back in the send Q
aMessage.setSequenceNumber(0);
sendMessage(aMessage);
}
return false;
case ON:
//Protocol Reverse Engineering: We have to decide whether we trust the ACK messages sent back to the Stick or not.
// If we do, then uncomment this line. If not, we will rely on a formal DEVICE_INFORMATION_REQUEST to get
// the real state of the Circle(+)
// postUpdate(((AcknowledgeMessage)message).getExtendedMAC(), PlugwiseCommandType.CURRENTSTATE, ((AcknowledgeMessage)message).isOn());
break;
case OFF:
//Protocol Reverse Engineering: : Idem as in ON
// postUpdate(((AcknowledgeMessage)message).getExtendedMAC(), PlugwiseCommandType.CURRENTSTATE, ((AcknowledgeMessage)message).isOff());
break;
default:
logger.debug("Plugwise Unknown Acknowledgement message Extension");
break;
}
}
return true;
case INITIALISE_RESPONSE:
MAC = ((InitialiseResponseMessage)message).getMAC();
initialised = true;
// is the network online?
if(((InitialiseResponseMessage)message).isOnline()) {
CirclePlus circlePlus = (CirclePlus) getDeviceByMAC(((InitialiseResponseMessage)message).getCirclePlusMAC());
if(!((InitialiseResponseMessage)message).getCirclePlusMAC().equals("") && circlePlus==null) {
circlePlus = new CirclePlus(((InitialiseResponseMessage)message).getCirclePlusMAC(),this);
plugwiseDeviceCache.add(circlePlus);
logger.debug("Added a CirclePlus with MAC {} to the cache", circlePlus.getMAC());
}
circlePlus.updateInformation();
circlePlus.calibrate();
circlePlus.setClock();
if(circlePlus != null) {
// initiate a "role call" request in the network
circlePlus.roleCall(0);
}
} else {
logger.debug("The network is not online. nothing to do here");
}
return true;
case NODE_AVAILABLE:
String node = ((NodeAvailableMessage)message).getMAC();
Circle someCircle = (Circle) getDeviceByMAC(node);
if(someCircle == null) {
Circle newCircle = new Circle(node, this,node);
plugwiseDeviceCache.add(newCircle);
// confirm to the new node that it is added to the network
NodeAvailableResponseMessage response = new NodeAvailableResponseMessage(true, node);
sendMessage(response);
newCircle.updateInformation();
newCircle.calibrate();
}
return true;
default:
return super.processMessage(message);
}
}
return false;
}
private String getCRCFromString(String buffer) {
int crc = 0x0000;
int polynomial = 0x1021; // 0001 0000 0010 0001 (0, 5, 12)
byte[] bytes = new byte[0];
try {
bytes = buffer.getBytes("ASCII");
} catch (UnsupportedEncodingException e) {
logger.debug("Could not fetch ASCII bytes from String ",buffer);
}
for (byte b : bytes) {
for (int i = 0; i < 8; i++) {
boolean bit = ((b >> (7-i) & 1) == 1);
boolean c15 = ((crc >> 15 & 1) == 1);
crc <<= 1;
if (c15 ^ bit) crc ^= polynomial;
}
}
crc &= 0xffff;
return(String.format("%04X", crc).toUpperCase());
}
public static class PowerInformationJob implements Job {
public void execute(JobExecutionContext context)
throws JobExecutionException {
// get the reference to the Stick
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
Stick theStick = (Stick) dataMap.get("Stick");
String MAC = (String) dataMap.get("MAC");
if(theStick.isInitialised()) {
PlugwiseDevice device = theStick.getDeviceByMAC(MAC);
if(device!=null){
if(device.getType().equals(DeviceType.Circle) || device.getType().equals(DeviceType.CirclePlus)) {
((Circle)device).updateCurrentEnergy();
}
}
}
}
}
public static class SendJob implements Job {
private Stick theStick;
public void execute(JobExecutionContext context)
throws JobExecutionException {
// get the reference to the Stick
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
theStick = (Stick) dataMap.get("Stick");
// logger.debug("SendJob: Executing Quartz Send Job");
if(theStick.isInitialised()) {
// loop through the send queue and send out all messages
logger.debug("SendJob: {} messages in the sendQ ",theStick.sendQueue.size());
Message message = theStick.sendQueue.poll();
while(message != null) {
sendMessage(message);
try {
Thread.sleep(theStick.interval);
} catch (InterruptedException e) {
logger.debug("An exception occurred while putting the Plugwise SendJob thread to sleep : {}",e.getMessage());
}
logger.debug("SendJob: in loop: {} messages in the sendQ ",theStick.sendQueue.size());
message = theStick.sendQueue.poll();
}
}
}
private boolean sendMessage(Message message) {
if (message != null) {
if(message.getAttempts() < theStick.maxRetries) {
message.increaseAttempts();
logger.debug("sendMessage: Sending Plugwise protocol data unit: attempts: {} MAC:{} command:{} sequence:{} full HEX:{}", new String[] { Integer.toString(message.getAttempts()),message.getMAC(),message.getType().toString(), Integer.toString(message.getSequenceNumber()),message.toHexString()});
String packedString = PROTOCOL_HEADER + message.toHexString() + PROTOCOL_TRAILER;
ByteBuffer bytebuffer = ByteBuffer.allocate(packedString.length());
bytebuffer.put(packedString.getBytes());
bytebuffer.rewind();
logger.debug("sendMessage: Locking the queues");
theStick.queueLock.lock();
try {
logger.debug("sendMessage: Writing message to the outputchannel of the stick : {}",message.toString());
theStick.outputChannel.write(bytebuffer);
} catch (IOException e) {
logger.error("Error writing '{}' to serial port {}: {}", new String[] { packedString, theStick.port, e.getMessage() });
}
// wait for the confirmation message by inspecting the received Q
Message lastMessage = null;
logger.debug("sendMessage: Entering loop for lastMessage");
while(lastMessage==null) {
// logger.debug("sendMessage: Locking the Receive queues");
theStick.receiveLock.lock();
Iterator<Message> messageIterator= theStick.receivedQueue.iterator();
while(messageIterator.hasNext()) {
Message aMessage = messageIterator.next();
if(aMessage.getType().equals(MessageType.ACKNOWLEDGEMENT)) {
if(!((AcknowledgeMessage)aMessage).isExtended()) {
lastMessage = aMessage;
logger.debug("sendMessage: Removing an ACK from the RecQ: {}",lastMessage.toString());
theStick.receivedQueue.remove(lastMessage);
break;
}
}
}
// logger.debug("sendMessage: Unlocking the Receive queues");
theStick.receiveLock.unlock();
}
logger.debug("sendMessage: Exiting loop for lastMessage");
AcknowledgeMessage ack = (AcknowledgeMessage) lastMessage;
if(!ack.isSuccess()) {
if(ack.isError()) {
logger.error("Error sending Plugwise message: Negative ACK: {}", packedString);
}
} else {
// update the sent message with the new sequence number
message.setSequenceNumber(ack.getSequenceNumber());
// place the sent message in the sent Q
try {
logger.debug("sendMessage: putting message in the senTq : {}",message.toString());
if(theStick.sentQueue.size()==maxBufferSize) {
// For some @#$@#$ reason plugwise devices, or the Stick, does not send responses
// to Requests. They clog the sentQueue. Let's flush some part of the queue
Message someMessage = theStick.sentQueue.poll();
logger.debug("Flushing a message from the sentQueue: {}",someMessage);
}
theStick.sentQueue.put(message);
logger.debug("sendMessage: there are now {} msg in the senTq",theStick.sentQueue.size());
//logger.debug("senTq is now {}",theStick.sentQueue);
} catch (InterruptedException e) {
logger.error("Error storing Plugwise message in the sent queue: {}", message.toString());
}
}
logger.debug("sendMessage: Unlocking the queues");
theStick.queueLock.unlock();
return true;
} else {
// max attempts reached
// we give up, and to a network reset
logger.error("Giving finally up on Plugwise protocol data unit after attempts: {} MAC:{} command:{} sequence:{} payload:{}", new String[] { Integer.toString(message.getAttempts()),message.getMAC(),message.getType().toString(), Integer.toString(message.getSequenceNumber()),message.getPayLoad()});
}
}
return false;
}
}
public class SendJobListener implements JobListener {
private String name;
public SendJobListener(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void jobToBeExecuted(JobExecutionContext context) {
// do something with the event
}
public void jobWasExecuted(JobExecutionContext context,
JobExecutionException jobException) {
// get the reference to the Stick
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
Stick theStick = (Stick) dataMap.get("Stick");
// logger.debug("SendJobListener: SJobListeren reschedule a job");
Scheduler sched = null;
try {
sched = StdSchedulerFactory.getDefaultScheduler();
} catch (SchedulerException e) {
logger.error("Error getting a reference to the Quartz Scheduler");
}
JobDataMap map = new JobDataMap();
map.put("Stick", theStick);
Stick.counter++;
JobDetail job = newJob(SendJob.class)
.withIdentity("Send-"+Stick.counter, "Plugwise")
.usingJobData(map)
.build();
Trigger trigger = newTrigger()
.withIdentity("Send-"+Stick.counter, "Plugwise")
.startNow()
.build();
try {
sched.getListenerManager().addJobListener(new SendJobListener("JobListener-"+job.getKey().toString()), KeyMatcher.keyEquals(job.getKey()));
} catch (SchedulerException e1) {
logger.error("An exception occured while attaching a Quartz Send Job Listener");
}
try {
sched.scheduleJob(job, trigger);
} catch (SchedulerException e) {
logger.error("Error scheduling a job with the Quartz Scheduler : {}",e.getMessage());
}
}
public void jobExecutionVetoed(JobExecutionContext context) {
// do something with the event
}
}
public static class ProcessMessageJob implements Job {
private Stick theStick;
public void execute(JobExecutionContext context)
throws JobExecutionException {
// get the reference to the Stick
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
theStick = (Stick) dataMap.get("Stick");
if(theStick.isInitialised()) {
logger.debug("ProcessMessageJob: Locking the queues");
theStick.queueLock.lock();
logger.debug("ProcessMessageJob: there are {} msg in the receivedQ",theStick.receivedQueue.size());
Message message = theStick.receivedQueue.poll();
logger.debug("ProcessMessageJob: Unlocking the queues");
theStick.queueLock.unlock();
if(message != null) {
PlugwiseDevice target = theStick.getDeviceByMAC(message.getMAC());
boolean result = false;
if(target!=null) {
result = target.processMessage(message);
}
else{
// if we can not find the target MAC for this message, we let the stick deal with it
result = theStick.processMessage(message);
}
// after processing the response to a message, we remove any reference to the original request stored in the sent Q
// WARNING: We assume that each request sent out can only be followed bye EXACTLY ONE response - so far it seems that the PW protocol is operating in that way
if(result) {
Iterator<Message> messageIterator= theStick.sentQueue.iterator();
while(messageIterator.hasNext()) {
Message aMessage = messageIterator.next();
if(aMessage.getSequenceNumber() == message.getSequenceNumber()) {
logger.debug("execute: removing a msg from the senTq: {}",aMessage.toString());
theStick.sentQueue.remove(aMessage);
break;
}
}
}
}
}
}
}
public static class PowerBufferJob implements Job {
@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
// get the reference to the Stick
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
Stick theStick = (Stick) dataMap.get("Stick");
String MAC = (String) dataMap.get("MAC");
if(theStick.isInitialised()) {
PlugwiseDevice device = theStick.getDeviceByMAC(MAC);
if(device!=null){
if(device.getType().equals(DeviceType.Circle) || device.getType().equals(DeviceType.CirclePlus)) {
((Circle)device).updateEnergy(false);
}
}
}
}
}
public static class ClockJob implements Job {
@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
// get the reference to the Stick
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
Stick theStick = (Stick) dataMap.get("Stick");
String MAC = (String) dataMap.get("MAC");
if(theStick.isInitialised()) {
PlugwiseDevice device = theStick.getDeviceByMAC(MAC);
if(device!=null){
if(device.getType().equals(DeviceType.Circle) || device.getType().equals(DeviceType.CirclePlus)) {
((Circle)device).updateSystemClock();
}
}
}
}
}
public static class RealTimeClockJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// get the reference to the Stick
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
Stick theStick = (Stick) dataMap.get("Stick");
String MAC = (String) dataMap.get("MAC");
if(theStick.isInitialised()) {
PlugwiseDevice device = theStick.getDeviceByMAC(MAC);
if(device!=null){
if(device.getType().equals(DeviceType.CirclePlus)) {
((CirclePlus)device).updateRealTimeClock();
}
}
}
}
}
public static class InformationJob implements Job {
@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
// get the reference to the Stick
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
Stick theStick = (Stick) dataMap.get("Stick");
String MAC = (String) dataMap.get("MAC");
if(theStick.isInitialised()) {
PlugwiseDevice device = theStick.getDeviceByMAC(MAC);
if(device != null) {
if(device.getType().equals(DeviceType.Circle) || device.getType().equals(DeviceType.CirclePlus)) {
((Circle)device).updateInformation();
}
}
}
}
}
}