org.openhab.binding.plugwise.internal.Stick.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.binding.plugwise.internal.Stick.java

Source

/**
 * Copyright (c) 2010-2015, 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();
                    }
                }
            }
        }
    }
}