]> git.basschouten.com Git - openhab-addons.git/blob
d8130c0565f6b2671f34f3db34738464e80d17ac
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.cm11a.internal;
14
15 import java.io.DataInputStream;
16 import java.io.DataOutputStream;
17 import java.io.EOFException;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.io.OutputStream;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.Calendar;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.TooManyListenersException;
29 import java.util.concurrent.ArrayBlockingQueue;
30 import java.util.concurrent.BlockingQueue;
31
32 import org.openhab.binding.cm11a.internal.handler.Cm11aAbstractHandler;
33 import org.openhab.binding.cm11a.internal.handler.Cm11aBridgeHandler;
34 import org.openhab.binding.cm11a.internal.handler.ReceivedDataListener;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 import gnu.io.CommPortIdentifier;
39 import gnu.io.NoSuchPortException;
40 import gnu.io.PortInUseException;
41 import gnu.io.SerialPort;
42 import gnu.io.SerialPortEvent;
43 import gnu.io.SerialPortEventListener;
44 import gnu.io.UnsupportedCommOperationException;
45
46 /**
47  * Driver for the CM11 X10 interface.
48  *
49  *
50  * @author Anthony Green - Original code
51  * @author Bob Raker - updates to setClock code, adapted code for use in openHAB2
52  * @see <a href="http://www.heyu.org/docs/protocol.txt">CM11 Protocol specification</a>
53  * @see <a href="http://www.rxtx.org">RXTX Serial API for Java</a>
54  */
55 public class X10Interface extends Thread implements SerialPortEventListener {
56
57     private final Logger logger = LoggerFactory.getLogger(X10Interface.class);
58
59     // X10 Function codes
60     public static final int FUNC_ALL_UNITS_OFF = 0x0;
61     public static final int FUNC_ALL_LIGHTS_ON = 0x1;
62     public static final int FUNC_ON = 0x2;
63     public static final int FUNC_OFF = 0x3;
64     public static final int FUNC_DIM = 0x4;
65     public static final int FUNC_BRIGHT = 0x5;
66     public static final int FUNC_ALL_LIGHTS_OFF = 0x6;
67     public static final int FUNC_EXTENDED = 0x7;
68     public static final int FUNC_HAIL_REQ = 0x8;
69     public static final int FUNC_HAIL_ACK = 0x9;
70     public static final int FUNC_PRESET_DIM_1 = 0xA;
71     public static final int FUNC_PRESET_DIM_2 = 0xB;
72     public static final int FUNC_EXT_DATA_TRANSFER = 0xC;
73     public static final int FUNC_STATUS_ON = 0xD;
74     public static final int FUNC_STATUS_OFF = 0xE;
75     public static final int FUNC_STATUS_REQ = 0xF;
76
77     // Definitions for the header:code bits
78     /**
79      * Bit mask for the bit that is always set in a header:code
80      */
81     static final int HEAD = 0x04;
82     /**
83      * Bit mask for Function/Address bit of header:code.
84      */
85     static final int HEAD_FUNC = 0x02;
86     /**
87      * Bit mask for standard/extended transmission bit of header:code.
88      */
89     static final int HEAD_EXTENDED = 0x01;
90
91     /**
92      * Byte sent from PC to Interface to acknowledge the receipt of a correct checksum.
93      * If the checksum was incorrect, the PC should retransmit.
94      */
95     static final int CHECKSUM_ACK = 0x00;
96     /**
97      * Byte send from Interface to PC to indicate it has sent the desired data over the
98      * X10/power lines.
99      */
100     static final int IF_READY = 0x55;
101
102     /**
103      * Byte sent from Interface to PC to request that its clock is set.
104      * Interface will send this to the PC repeatedly after a power-failure and will not respond to commands
105      * until its clock has been set.
106      */
107     static final int CLOCK_SET_REQ = 0xA5;
108     /**
109      * Byte sent from PC to interface to start a transmission that sets the interface clock.
110      */
111     static final int CLOCK_SET_HEAD = 0x9B;
112
113     /**
114      * Byte sent from interface to PC to indicate that it has X10 data pending transmission to the PC.
115      */
116     static final int DATA_READY_REQ = 0x5a;
117     static final int DATA_READY_HEAD = 0xc3;
118
119     /**
120      * This command is purely intended for the CP10.
121      * The power-strip contains an input filter and electrical surge protection
122      * that is monitored by the microcontroller. If this protection should
123      * become compromised (i.e. resulting from a lightening strike) the
124      * interface will attempt to wake the computer with a 'filter-fail poll'.
125      */
126     static final int INPUT_FILTER_FAIL_REQ = 0xf3;
127     static final int INPUT_FILTER_FAIL_HEAD = 0xf3;
128
129     /**
130      * Byte sent from PC to interface to enable interface feature that brings serial port RI high when
131      * data arrives to send to PC
132      */
133     static final int RI_ENABLE = 0xeb;
134     static final int RI_DISABLE = 0x55;
135
136     /**
137      * THe house code to be monitored. Not sure what this means, but it is part of the clock set instruction.
138      * For the moment hardcoded here to be House 'E'.
139      */
140     static final int MONITORED_HOUSE_CODE = 0x10;
141
142     static final Map<Character, Integer> HOUSE_CODES;
143     static final Map<Integer, Integer> DEVICE_CODES;
144
145     static {
146         HashMap<Character, Integer> houseCodes = new HashMap<>(16);
147         houseCodes.put('A', 0x60);
148         houseCodes.put('B', 0xE0);
149         houseCodes.put('C', 0x20);
150         houseCodes.put('D', 0xA0);
151         houseCodes.put('E', 0x10);
152         houseCodes.put('F', 0x90);
153         houseCodes.put('G', 0x50);
154         houseCodes.put('H', 0xD0);
155         houseCodes.put('I', 0x70);
156         houseCodes.put('J', 0xF0);
157         houseCodes.put('K', 0x30);
158         houseCodes.put('L', 0xB0);
159         houseCodes.put('M', 0x00);
160         houseCodes.put('N', 0x80);
161         houseCodes.put('O', 0x40);
162         houseCodes.put('P', 0xC0);
163
164         HOUSE_CODES = Collections.unmodifiableMap(houseCodes);
165
166         HashMap<Integer, Integer> deviceCodes = new HashMap<>(16);
167         deviceCodes.put(1, 0x06);
168         deviceCodes.put(2, 0x0E);
169         deviceCodes.put(3, 0x02);
170         deviceCodes.put(4, 0x0A);
171         deviceCodes.put(5, 0x01);
172         deviceCodes.put(6, 0x09);
173         deviceCodes.put(7, 0x05);
174         deviceCodes.put(8, 0x0D);
175         deviceCodes.put(9, 0x07);
176         deviceCodes.put(10, 0x0F);
177         deviceCodes.put(11, 0x03);
178         deviceCodes.put(12, 0x0B);
179         deviceCodes.put(13, 0x00);
180         deviceCodes.put(14, 0x08);
181         deviceCodes.put(15, 0x04);
182         deviceCodes.put(16, 0x0C);
183
184         DEVICE_CODES = Collections.unmodifiableMap(deviceCodes);
185     }
186
187     // Constants that control the interaction with the hardware
188     static final int IO_PORT_OPEN_TIMEOUT = 5000;
189
190     /**
191      * Number of CM11a dim increments for dimable devices
192      */
193     static final int CM11A_DIM_INCREMENTS = 22;
194
195     /**
196      * How long to wait between attempts to reconnect to the interface. (ms)
197      */
198     static final int IO_RECONNECT_INTERVAL = 5000;
199     /**
200      * Maximum number of times to retry sending a bit of data when checksum errors occur.
201      */
202     static final int IO_MAX_SEND_RETRY_COUNT = 5;
203
204     static final int SERIAL_TIMEOUT_MSEC = 4000;
205
206     // Hardware IO attributes
207     protected CommPortIdentifier portId;
208     protected SerialPort serialPort;
209     protected boolean connected = false;
210     protected DataOutputStream serialOutput;
211     protected OutputStream serialOutputStr;
212     protected DataInputStream serialInput;
213     protected InputStream serialInputStr;
214
215     // Listeners that are to be notified when new data is received from the cm11a
216     private List<ReceivedDataListener> receiveListeners = new ArrayList<>();
217
218     /**
219      * Flag to indicate that background thread should be killed. Used to deactivate plugin.
220      */
221     protected volatile boolean killThread = false;
222
223     // Scheduling attributes
224     /**
225      * Queue of as-yet un-actioned requests.
226      */
227     protected BlockingQueue<Cm11aAbstractHandler> deviceUpdateQueue = new ArrayBlockingQueue<>(256);
228
229     // Need to keep last addresses found for data that comes in over the serial interface because if the incoming
230     // command is a dim or bright the address isn't included. In addition some controllers will send the address
231     // in one message and the function in a second one
232     private List<String> lastAddresses;
233
234     /**
235      * Need to have access to BridgeHandler so it's status can be updated.
236      */
237     private Cm11aBridgeHandler bridgeHandler;
238
239     /**
240      *
241      * @param serialPort serial port device. e.g. /dev/ttyS0
242      * @throws NoSuchPortException
243      *
244      */
245     public X10Interface(String serialPort, Cm11aBridgeHandler bridgeHandler) throws NoSuchPortException {
246         super();
247         logger.trace("**** Constructing X10Interface for serial port: {} *******", serialPort);
248         portId = CommPortIdentifier.getPortIdentifier(serialPort);
249         this.bridgeHandler = bridgeHandler;
250     }
251
252     /**
253      * Establishes a serial connection to the hardware, if one is not already established.
254      */
255     protected boolean connect() {
256         if (!connected) {
257             if (serialPort != null) {
258                 logger.trace("Closing stale serialPort object before reconnecting");
259                 serialPort.close();
260             }
261             logger.debug("Connecting to X10 hardware on serial port: {}", portId.getName());
262             try {
263                 serialPort = portId.open("Openhab CM11A Binding", IO_PORT_OPEN_TIMEOUT);
264                 serialPort.setSerialPortParams(4800, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
265                         SerialPort.PARITY_NONE);
266                 serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
267                 serialPort.disableReceiveTimeout();
268                 serialPort.enableReceiveThreshold(1);
269
270                 serialOutputStr = serialPort.getOutputStream();
271                 serialOutput = new DataOutputStream(serialOutputStr);
272                 serialInputStr = serialPort.getInputStream();
273                 serialInput = new DataInputStream(serialInputStr);
274
275                 serialPort.addEventListener(this);
276                 connected = true;
277
278                 serialPort.notifyOnDataAvailable(true);
279                 serialPort.notifyOnRingIndicator(true);
280
281                 // Bob Raker - add serial timeout to prevent possible hang conditions
282                 serialPort.enableReceiveTimeout(SERIAL_TIMEOUT_MSEC);
283                 if (!serialPort.isReceiveTimeoutEnabled()) {
284                     logger.info("Serial receive timeout not supported by this driver.");
285                 }
286
287                 bridgeHandler.changeBridgeStatusToUp();
288             } catch (PortInUseException e) {
289                 String message = String.format("Serial port %s is in use by another application (%s)", portId.getName(),
290                         e.currentOwner);
291                 logger.warn("{}", message);
292                 bridgeHandler.changeBridgeStatusToDown(message);
293             } catch (UnsupportedCommOperationException e) {
294                 logger.warn("Serial port {} doesn't support the required baud/parity/stopbits or Timeout",
295                         portId.getName());
296             } catch (IOException e) {
297                 logger.warn("IO Problem with serial port {} . {}", portId.getName(), e.getMessage());
298             } catch (TooManyListenersException e) {
299                 logger.warn(
300                         "TooManyListeners error when trying to connect to serial port.  Interface is unlikely to work, raise a bug report.",
301                         e);
302                 bridgeHandler.changeBridgeStatusToDown("ToManyListenersException");
303             }
304         } else {
305             logger.trace("Already connected to hardware, skipping reconnection.");
306         }
307
308         return connected;
309     }
310
311     /**
312      * Transmits a standard (non-extended) X10 function.
313      *
314      * @param address
315      * @param function
316      * @param dims 0-22, number of dims/brights to send.
317      * @return true if update was successful
318      * @throws InvalidAddressException
319      * @throws IOException
320      */
321     public boolean sendFunction(String address, int function, int dims) throws InvalidAddressException, IOException {
322         boolean success = false;
323
324         if (!validateAddress(address)) {
325             throw new InvalidAddressException("Address " + address + " is not a valid X10 address");
326         }
327
328         int houseCode = HOUSE_CODES.get(address.charAt(0));
329         int deviceCode = DEVICE_CODES.get(Integer.parseInt(address.substring(1)));
330
331         int[] data = new int[2];
332
333         if (connect()) {
334             synchronized (serialPort) {
335                 logger.trace("Sending a standard X10 function to device: {}", address);
336                 // First send address
337                 data[0] = HEAD;
338                 data[1] = houseCode | deviceCode;
339                 sendData(data);
340
341                 // Now send function call
342                 data[0] = HEAD | HEAD_FUNC | (dims << 3);
343                 data[1] = houseCode | function;
344                 sendData(data);
345
346                 success = true;
347             }
348         }
349         return success;
350     }
351
352     /**
353      * Validates that the given string is a valid X10 address. Returns true if this is the case.
354      *
355      * @param address
356      * @return
357      */
358     public static boolean validateAddress(String address) {
359         return (!(address.length() < 2 || address.length() > 3
360                 || !HOUSE_CODES.containsKey(Character.valueOf(address.charAt(0)))
361                 || !DEVICE_CODES.containsKey(Integer.parseInt(address.substring(1)))));
362     }
363
364     /**
365      * Queues a standard (non-extended) X10 function for transmission.
366      *
367      * @param address
368      * @param function
369      * @return true for success
370      * @throws InvalidAddressException
371      * @throws IOException
372      */
373     public boolean sendFunction(String address, int function) throws InvalidAddressException, IOException {
374         return sendFunction(address, function, 0);
375     }
376
377     /**
378      * Add specified device into the queue for hardware updates.
379      *
380      * <p>
381      * If device is already queued, it will be removed from queue and moved to the end.
382      * </p>
383      *
384      * @param device
385      */
386     public void scheduleHWUpdate(Cm11aAbstractHandler device) {
387         deviceUpdateQueue.remove(device);
388         if (!deviceUpdateQueue.offer(device)) {
389             logger.warn(
390                     "X10 function call queue full.  Too many outstanding commands.  This command will be discarded");
391         }
392         logger.debug("Added item to cm11a queue for: {}", device.getThing().getLabel());
393     }
394
395     /**
396      * Sends data to the hardware and handles the checksuming and retry process.
397      *
398      * <p>
399      * When applicable, method blocks until the data has actually been sent over the powerlines using X10
400      * </p>
401      *
402      * @param data Data to be sent.
403      * @throws IOException
404      */
405     protected void sendData(int[] data) throws IOException {
406         int calcChecksum = 0;
407         int checksumResponse = -1;
408
409         // Calculate expected checksum:
410         for (int i = 0; i < data.length; i++) {
411             // Note that ints are signed in Java, but the checksum uses unsigned ints.
412             // Hence some jiggery pokery to get an unsigned int from the data int.
413             calcChecksum = (calcChecksum + (0x000000FF & (data[i]))) & 0x000000FF;
414             logger.trace("Checksum calc: int {} = {}", i, Integer.toHexString(data[i]));
415         }
416
417         if (connect()) {
418             synchronized (serialPort) {
419                 long startTime = System.currentTimeMillis(); // do some timing analysis
420
421                 // Stop background data listener as we want to have a dialogue with the interface here.
422                 serialPort.notifyOnDataAvailable(false);
423
424                 // Need to catch possible EOF exception if there is a timeout
425                 try {
426                     int retryCount = 0;
427                     while (checksumResponse != calcChecksum) {
428                         retryCount++;
429                         for (int i = 0; i < data.length; i++) {
430                             serialOutput.write(data[i]);
431                             serialOutput.flush();
432                         }
433                         long sendTime = System.currentTimeMillis();
434                         logger.trace("Sent the following data out the serial port in {} msec, {}",
435                                 (sendTime - startTime), Arrays.toString(data));
436                         checksumResponse = serialInput.readUnsignedByte();
437                         logger.trace("Attempted to send data, try number: {} Checksum expected: {} received: {}",
438                                 retryCount, Integer.toHexString(calcChecksum), Integer.toHexString(checksumResponse));
439                         long ckSumTime = System.currentTimeMillis();
440                         logger.trace("Received serial port check sum in {} msec", (ckSumTime - sendTime));
441
442                         if (checksumResponse != calcChecksum) {
443                             // On initial device power up, nothing works until we set the clock. Check to see if the
444                             // unexpected data was actually a request from interface to PC.
445                             processRequestFromIFace(checksumResponse);
446
447                             if (retryCount > IO_MAX_SEND_RETRY_COUNT) {
448                                 logger.warn("Failed to send data to X10 hardware due to too many checksum failures");
449                                 serialPort.notifyOnDataAvailable(true);
450                                 throw new IOException("Max retries exceeded");
451                             }
452                         }
453                     }
454
455                     logger.trace(
456                             "Data transmission to interface was successful, sending ACK.  X10 transmission over powerline will now commence.");
457                     long ackTime = System.currentTimeMillis();
458                     serialOutput.write(CHECKSUM_ACK);
459                     serialOutput.flush();
460
461                     int response = serialInput.readUnsignedByte();
462                     if (response == IF_READY) {
463                         long cmpltdTime = System.currentTimeMillis();
464                         logger.trace("Serial port X10 ACK completed in {} msec, TOTAL X10 TRANSMISSION TIME in {} ms",
465                                 (cmpltdTime - ackTime), (cmpltdTime - startTime));
466                     } else {
467                         logger.warn("Expected IF_READY ({}) response from hardware but received: {} instead",
468                                 Integer.toHexString(IF_READY & 0x00000FF), Integer.toHexString(response & 0x00000FF));
469                     }
470                 } catch (EOFException ex) {
471                     logger.warn(
472                             "Received EOF exception while sending X10 command after {} ms. Make sure the cm11a is connected to the serial port",
473                             (System.currentTimeMillis() - startTime));
474                 }
475                 serialPort.notifyOnDataAvailable(true);
476             }
477         }
478     }
479
480     @Override
481     public void run() {
482         logger.trace("Starting X10Interface background thread...");
483
484         // call connect so any X10 events on the powerline will be picked up by the serialEvent listener
485         connect();
486
487         while (!killThread) {
488             try {
489                 Cm11aAbstractHandler nextModule;
490                 logger.trace("Getting next module to be updated");
491                 nextModule = deviceUpdateQueue.take();
492                 logger.trace("Got a device.  Going to run it.");
493
494                 // Keep retrying to update this device until it is successful.
495                 updateHardware(nextModule);
496             } catch (InterruptedException e1) {
497                 Thread.currentThread().interrupt(); // See https://www.ibm.com/developerworks/library/j-jtp05236/ for
498                                                     // discussion of resetting interrupt flag
499                 logger.warn("Unexpected interrupt on X10 scheduling thread.  Ignoring and continuing anyway...");
500             }
501         }
502         logger.trace("Stopping background thread...");
503         this.notifyAll();
504     }
505
506     /**
507      * Perform Hardware update. Keep trying until it is successful
508      *
509      * @param nextModule - Next module that needs to be updated
510      * @throws InterruptedException
511      */
512     private void updateHardware(Cm11aAbstractHandler nextModule) throws InterruptedException {
513         boolean success = false;
514         while (!success) {
515             try {
516                 if (connect()) {
517                     nextModule.updateHardware(this);
518                     success = true;
519                 } else {
520                     Thread.sleep(IO_RECONNECT_INTERVAL);
521                 }
522             } catch (IOException e) {
523                 connected = false;
524                 String message = "IO Exception when updating module hardware.  Will retry shortly";
525                 logger.warn(message, e);
526                 bridgeHandler.changeBridgeStatusToDown(message);
527                 Thread.sleep(IO_RECONNECT_INTERVAL);
528             } catch (InvalidAddressException e) {
529                 logger.warn("Attempted to send an X10 Function call with invalid address.  Ignoring this.");
530                 success = true; // Pretend this was successful as retrying will be pointless.
531             }
532         }
533     }
534
535     @Override
536     public void serialEvent(SerialPortEvent event) {
537         try {
538             if (event.getEventType() == SerialPortEvent.DATA_AVAILABLE || event.getEventType() == SerialPortEvent.RI) {
539                 synchronized (serialPort) {
540                     logger.trace("Serial port data available or RI indicator event received");
541                     while (serialPort.isRI() && serialInput.available() <= 0) {
542                         logger.trace("Ring indicator is High but there is no data.  Waiting for data...");
543                         try {
544                             Thread.sleep(20);
545                         } catch (InterruptedException e) {
546                             logger.debug("Interrupted while sleeping", e);
547                         }
548                     }
549
550                     while (serialInput.available() > 0) {
551                         logger.trace("{} bytes of data available to be read", serialInput.available());
552                         int readint = serialInput.read();
553
554                         processRequestFromIFace(readint);
555
556                         // Wait a while before rechecking to give interface time to switch off Ring Indicator.
557                         while (serialPort.isRI() && serialInput.available() <= 0) {
558                             logger.trace("Ring indicator is High but there is no data.  Waiting for data...");
559                             try {
560                                 Thread.sleep(300);
561                             } catch (InterruptedException e) {
562                                 Thread.currentThread().interrupt();
563                                 // Reset the flag and continue
564                             }
565                         }
566                     }
567                     logger.trace(
568                             "Reading data from interface complete.  Ring Indicator has cleared and no data is left to read.");
569                 }
570             }
571         } catch (IOException e) {
572             logger.warn("IO Exception in serial port handler callback: {}", e.getMessage());
573         }
574     }
575
576     /**
577      * Processes a request made from the interface to the PC. Only handles requests initiated by the interface,
578      * not those that form part of a conversation triggered by the PC.
579      *
580      * @param readint
581      * @throws IOException
582      */
583     protected void processRequestFromIFace(int readint) throws IOException {
584         switch (readint) {
585             case CLOCK_SET_REQ:
586                 setClock();
587                 break;
588             case DATA_READY_REQ:
589                 receiveCommandData();
590                 break;
591             case INPUT_FILTER_FAIL_REQ:
592                 serialOutput.write(DATA_READY_HEAD);
593                 logger.warn(
594                         "X10 Interface has indicated that the filter and/or surge protection in the device has failed.");
595                 break;
596             default:
597                 logger.warn("Unexpected data received from X10 interface: {}", Integer.toHexString(readint));
598         }
599     }
600
601     /**
602      * Sets the internal clock on the X10 interface
603      *
604      * @throws IOException
605      */
606     private void setClock() throws IOException {
607         logger.debug("Setting clock in X10 interface");
608         Calendar cal = Calendar.getInstance();
609
610         int[] clockData = new int[7];
611         clockData[0] = CLOCK_SET_HEAD;
612         clockData[1] = cal.get(Calendar.SECOND);
613         clockData[2] = cal.get(Calendar.MINUTE) + (cal.get(Calendar.HOUR) % 2) * 60;
614         clockData[3] = cal.get(Calendar.HOUR_OF_DAY) / 2;
615         // Note: Calendar.DAY_OF_YEAR starts at 1 but the C language tm_yday starts at 0. Need 0 based DAY_OF_YEAR for
616         // cm11a
617         clockData[4] = (cal.get(Calendar.DAY_OF_YEAR) - 1) % 256;
618         // Note: Calendar.DAY_OF_WEEK is 1 based and need 0 based (i.e. Sunday = 1
619         clockData[5] = (((cal.get(Calendar.DAY_OF_YEAR - 1)) / 256) << 7)
620                 | (0x01 << (cal.get(Calendar.DAY_OF_WEEK) - 1));
621
622         // There are other flags in this final byte to do with clearing timer, battery timer and monitored status.
623         // I've no idea what they are, so have left them unset.
624         clockData[6] = MONITORED_HOUSE_CODE;
625
626         sendData(clockData);
627     }
628
629     /**
630      * Process data that the X10 interface is waiting to send to the PC
631      *
632      * @throws IOException
633      */
634     private void receiveCommandData() throws IOException {
635         logger.debug("Receiving X10 data from interface");
636
637         // Send acknowledgement to interface
638         serialOutput.write(DATA_READY_HEAD);
639
640         // Read the buffer size
641         // There might be several DATA_READY_REQ bytes which need to be ignored
642         int length = 0;
643         do {
644             length = serialInput.read();
645         } while (length == DATA_READY_REQ || length > 8); // if the length is >8 this isn't a valid length so ignore
646
647         // Next read the Function / Address Mask byte
648         int mask = serialInput.read();
649         length--; // This read counts as part of the length
650         logger.debug("Receiving [{}] bytes,  Addr/Func mask: {}", length, Integer.toBinaryString(mask));
651
652         // It is possible for the cm11a buffer to be empty in which case no processing can take place
653         if (length > 0) {
654             int[] data = new int[length];
655             for (int i = 0; i < length; i++) {
656                 int recvByte = serialInputStr.read();
657                 data[i] = recvByte;
658                 logger.debug("          Received X10 data [{}]: {}", i, Integer.toHexString(recvByte));
659             }
660
661             processCommandData(mask, data);
662         } else {
663             logger.warn("cm11a buffer was overrun. Any pending commands will be ignorred until the buffer clears.");
664         }
665     }
666
667     /**
668      * Process raw data received from the interface and convert into human readable data
669      *
670      * @param mask Function / Address Mask. Each bit corresponds to the contents of a data. A 0 bit
671      *            corresponds to an address. A 1 bit corresponds to a function.
672      * @param data
673      */
674     private void processCommandData(int mask, int[] data) {
675         int localMask = mask;
676         X10ReceivedData.X10COMMAND command = X10ReceivedData.X10COMMAND.UNDEF; // Just set it to something
677         List<String> addresses = new ArrayList<>();
678         int dims = 0;
679         List<X10ReceivedData> rcvData = new ArrayList<>();
680
681         for (int i = 0; i < data.length; i++) {
682             int d = data[i];
683             int dataType = localMask & 0x01;
684             if (dataType == 0) {
685                 // The data byte is an address
686                 int houseIndex = (d >> 4) & 0x0f;
687                 int unitIndex = d & 0x0f;
688                 addresses.add(Character.toString(X10ReceivedData.HOUSE_CODE[houseIndex])
689                         + Integer.toString(X10ReceivedData.UNIT_CODE[unitIndex]));
690             } else {
691                 // The data byte is a function
692                 command = X10ReceivedData.COMMAND_MAP.get(d & 0x0f);
693                 if (command == null) {
694                     command = X10ReceivedData.X10COMMAND.UNDEF;
695                 } else if (command == X10ReceivedData.X10COMMAND.BRIGHT || command == X10ReceivedData.X10COMMAND.DIM) {
696                     // Have to read one more byte which is the dim level
697                     if (i < data.length) {
698                         dims = data[++i];
699                         // This dims is a number between 0 and 210 and is the CHANGE in brightness level
700                         // The interface transmits 1 to 22 dims to go from full bright to full dim
701                         // therefore this need to be converted to a number between 1 and 22. Always want to dim or
702                         // brighten one increment. The conversion is therefore:
703                         dims = (dims * CM11A_DIM_INCREMENTS) / 210;
704                         dims = dims > 0 ? dims : 1;
705                     }
706                     // Also in this case no addresses would have been sent so use the saved addresses
707                     // If there were no previous commands that specified an address then lastAddress will be null. In
708                     // this case we can't do anything with this dim request.
709                     if (lastAddresses == null) {
710                         logger.info(
711                                 "cm11a received a dim command but there is no prior commands that included an address.");
712                         continue;
713                     }
714                     addresses = lastAddresses;
715                 } else if (command == X10ReceivedData.X10COMMAND.ALL_LIGHTS_OFF
716                         || command == X10ReceivedData.X10COMMAND.ALL_LIGHTS_ON
717                         || command == X10ReceivedData.X10COMMAND.ALL_UNITS_OFF) {
718                     logger.warn("cm11a received the command: {}. This command is ignored by this binding.", command);
719                     continue;
720                 } else {
721                     // A valid command must have been received.
722                     // As indicated above, some controllers send the address in one transmission and the function in a
723                     // second transaction.
724                     // Check if we have gotten an address in the transmission and if not use lastAddresses if they
725                     // are not null
726                     if (addresses.isEmpty()) {
727                         // No addresses were sent in this transmission, see if we can use lastAddresses
728                         if (lastAddresses == null) {
729                             logger.info("cm11a received a command but the transmission didn't include an address.");
730                             continue;
731                         } else {
732                             addresses = lastAddresses;
733                             logger.info(
734                                     "cm11a received a command without any addresses. Addresses from a prior reception are available and will be used.");
735                         }
736                     }
737                 }
738
739                 // Every time we get a function it is the end of the transmission and we can bundle the data into an
740                 // X10ReceivedData object.
741                 X10ReceivedData rd = new X10ReceivedData(addresses.toArray(new String[0]), command, dims);
742                 rcvData.add(rd);
743                 logger.debug("cm11a: Added received data to queue: {}", rd.toString());
744                 // reset the data objects for the next thing in the buffer, if any
745                 command = X10ReceivedData.X10COMMAND.UNDEF;
746                 // Collections.copy(lastAddresses, addresses);
747                 lastAddresses = addresses;
748                 addresses = new ArrayList<>();
749                 dims = 0;
750             }
751
752             localMask = localMask >> 1;
753         }
754
755         // Done processing buffer from cm11a. Notify interested parties about the data
756         for (X10ReceivedData rd : rcvData) {
757             logger.debug("cm11a: Converted received data to human form: {}", rd.toString());
758             notifyReceiveListeners(rd);
759         }
760     }
761
762     /**
763      * Called by classes that want to be notified when data has been received from the cm11a
764      *
765      * @param listener The class to be called
766      */
767     public void addReceivedDataListener(ReceivedDataListener listener) {
768         receiveListeners.add(listener);
769     }
770
771     /**
772      * Called to notify classes that data has been received
773      *
774      * @param rcv
775      */
776     private void notifyReceiveListeners(X10ReceivedData rcv) {
777         for (ReceivedDataListener rl : receiveListeners) {
778             rl.receivedX10Data(rcv);
779         }
780     }
781
782     /**
783      * Disconnect from hardware
784      */
785     public void disconnect() {
786         // Kill the thread that performs the hardware updates
787         killThread = true;
788
789         if (serialInput != null) {
790             try {
791                 serialInput.close();
792             } catch (IOException e) {
793                 // nothing to do if there is an issue closing the stream.
794             }
795         }
796         if (serialOutput != null) {
797             try {
798                 serialOutput.close();
799             } catch (IOException e) {
800                 // nothing to do if there is an issue closing the stream.
801             }
802         }
803
804         if (serialPort != null) {
805             serialPort.close();
806         }
807     }
808 }