2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.cm11a.internal;
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;
28 import java.util.TooManyListenersException;
29 import java.util.concurrent.ArrayBlockingQueue;
30 import java.util.concurrent.BlockingQueue;
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;
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;
47 * Driver for the CM11 X10 interface.
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>
55 public class X10Interface extends Thread implements SerialPortEventListener {
57 private final Logger logger = LoggerFactory.getLogger(X10Interface.class);
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;
77 // Definitions for the header:code bits
79 * Bit mask for the bit that is always set in a header:code
81 static final int HEAD = 0x04;
83 * Bit mask for Function/Address bit of header:code.
85 static final int HEAD_FUNC = 0x02;
87 * Bit mask for standard/extended transmission bit of header:code.
89 static final int HEAD_EXTENDED = 0x01;
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.
95 static final int CHECKSUM_ACK = 0x00;
97 * Byte send from Interface to PC to indicate it has sent the desired data over the
100 static final int IF_READY = 0x55;
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.
107 static final int CLOCK_SET_REQ = 0xA5;
109 * Byte sent from PC to interface to start a transmission that sets the interface clock.
111 static final int CLOCK_SET_HEAD = 0x9B;
114 * Byte sent from interface to PC to indicate that it has X10 data pending transmission to the PC.
116 static final int DATA_READY_REQ = 0x5a;
117 static final int DATA_READY_HEAD = 0xc3;
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'.
126 static final int INPUT_FILTER_FAIL_REQ = 0xf3;
127 static final int INPUT_FILTER_FAIL_HEAD = 0xf3;
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
133 static final int RI_ENABLE = 0xeb;
134 static final int RI_DISABLE = 0x55;
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'.
140 static final int MONITORED_HOUSE_CODE = 0x10;
142 static final Map<Character, Integer> HOUSE_CODES;
143 static final Map<Integer, Integer> DEVICE_CODES;
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);
164 HOUSE_CODES = Collections.unmodifiableMap(houseCodes);
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);
184 DEVICE_CODES = Collections.unmodifiableMap(deviceCodes);
187 // Constants that control the interaction with the hardware
188 static final int IO_PORT_OPEN_TIMEOUT = 5000;
191 * Number of CM11a dim increments for dimable devices
193 static final int CM11A_DIM_INCREMENTS = 22;
196 * How long to wait between attempts to reconnect to the interface. (ms)
198 static final int IO_RECONNECT_INTERVAL = 5000;
200 * Maximum number of times to retry sending a bit of data when checksum errors occur.
202 static final int IO_MAX_SEND_RETRY_COUNT = 5;
204 static final int SERIAL_TIMEOUT_MSEC = 4000;
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;
215 // Listeners that are to be notified when new data is received from the cm11a
216 private List<ReceivedDataListener> receiveListeners = new ArrayList<>();
219 * Flag to indicate that background thread should be killed. Used to deactivate plugin.
221 protected volatile boolean killThread = false;
223 // Scheduling attributes
225 * Queue of as-yet un-actioned requests.
227 protected BlockingQueue<Cm11aAbstractHandler> deviceUpdateQueue = new ArrayBlockingQueue<>(256);
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;
235 * Need to have access to BridgeHandler so it's status can be updated.
237 private Cm11aBridgeHandler bridgeHandler;
241 * @param serialPort serial port device. e.g. /dev/ttyS0
242 * @throws NoSuchPortException
245 public X10Interface(String serialPort, Cm11aBridgeHandler bridgeHandler) throws NoSuchPortException {
247 logger.trace("**** Constructing X10Interface for serial port: {} *******", serialPort);
248 portId = CommPortIdentifier.getPortIdentifier(serialPort);
249 this.bridgeHandler = bridgeHandler;
253 * Establishes a serial connection to the hardware, if one is not already established.
255 protected boolean connect() {
257 if (serialPort != null) {
258 logger.trace("Closing stale serialPort object before reconnecting");
261 logger.debug("Connecting to X10 hardware on serial port: {}", portId.getName());
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);
270 serialOutputStr = serialPort.getOutputStream();
271 serialOutput = new DataOutputStream(serialOutputStr);
272 serialInputStr = serialPort.getInputStream();
273 serialInput = new DataInputStream(serialInputStr);
275 serialPort.addEventListener(this);
278 serialPort.notifyOnDataAvailable(true);
279 serialPort.notifyOnRingIndicator(true);
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.");
287 bridgeHandler.changeBridgeStatusToUp();
288 } catch (PortInUseException e) {
289 String message = String.format("Serial port %s is in use by another application (%s)", portId.getName(),
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",
296 } catch (IOException e) {
297 logger.warn("IO Problem with serial port {} . {}", portId.getName(), e.getMessage());
298 } catch (TooManyListenersException e) {
300 "TooManyListeners error when trying to connect to serial port. Interface is unlikely to work, raise a bug report.",
302 bridgeHandler.changeBridgeStatusToDown("ToManyListenersException");
305 logger.trace("Already connected to hardware, skipping reconnection.");
312 * Transmits a standard (non-extended) X10 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
321 public boolean sendFunction(String address, int function, int dims) throws InvalidAddressException, IOException {
322 boolean success = false;
324 if (!validateAddress(address)) {
325 throw new InvalidAddressException("Address " + address + " is not a valid X10 address");
328 int houseCode = HOUSE_CODES.get(address.charAt(0));
329 int deviceCode = DEVICE_CODES.get(Integer.parseInt(address.substring(1)));
331 int[] data = new int[2];
334 synchronized (serialPort) {
335 logger.trace("Sending a standard X10 function to device: {}", address);
336 // First send address
338 data[1] = houseCode | deviceCode;
341 // Now send function call
342 data[0] = HEAD | HEAD_FUNC | (dims << 3);
343 data[1] = houseCode | function;
353 * Validates that the given string is a valid X10 address. Returns true if this is the case.
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)))));
365 * Queues a standard (non-extended) X10 function for transmission.
369 * @return true for success
370 * @throws InvalidAddressException
371 * @throws IOException
373 public boolean sendFunction(String address, int function) throws InvalidAddressException, IOException {
374 return sendFunction(address, function, 0);
378 * Add specified device into the queue for hardware updates.
381 * If device is already queued, it will be removed from queue and moved to the end.
386 public void scheduleHWUpdate(Cm11aAbstractHandler device) {
387 deviceUpdateQueue.remove(device);
388 if (!deviceUpdateQueue.offer(device)) {
390 "X10 function call queue full. Too many outstanding commands. This command will be discarded");
392 logger.debug("Added item to cm11a queue for: {}", device.getThing().getLabel());
396 * Sends data to the hardware and handles the checksuming and retry process.
399 * When applicable, method blocks until the data has actually been sent over the powerlines using X10
402 * @param data Data to be sent.
403 * @throws IOException
405 protected void sendData(int[] data) throws IOException {
406 int calcChecksum = 0;
407 int checksumResponse = -1;
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]));
418 synchronized (serialPort) {
419 long startTime = System.currentTimeMillis(); // do some timing analysis
421 // Stop background data listener as we want to have a dialogue with the interface here.
422 serialPort.notifyOnDataAvailable(false);
424 // Need to catch possible EOF exception if there is a timeout
427 while (checksumResponse != calcChecksum) {
429 for (int i = 0; i < data.length; i++) {
430 serialOutput.write(data[i]);
431 serialOutput.flush();
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));
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);
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");
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();
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));
467 logger.warn("Expected IF_READY ({}) response from hardware but received: {} instead",
468 Integer.toHexString(IF_READY & 0x00000FF), Integer.toHexString(response & 0x00000FF));
470 } catch (EOFException ex) {
472 "Received EOF exception while sending X10 command after {} ms. Make sure the cm11a is connected to the serial port",
473 (System.currentTimeMillis() - startTime));
475 serialPort.notifyOnDataAvailable(true);
482 logger.trace("Starting X10Interface background thread...");
484 // call connect so any X10 events on the powerline will be picked up by the serialEvent listener
487 while (!killThread) {
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.");
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...");
502 logger.trace("Stopping background thread...");
507 * Perform Hardware update. Keep trying until it is successful
509 * @param nextModule - Next module that needs to be updated
510 * @throws InterruptedException
512 private void updateHardware(Cm11aAbstractHandler nextModule) throws InterruptedException {
513 boolean success = false;
517 nextModule.updateHardware(this);
520 Thread.sleep(IO_RECONNECT_INTERVAL);
522 } catch (IOException e) {
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.
536 public void serialEvent(SerialPortEvent event) {
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...");
545 } catch (InterruptedException e) {
546 logger.debug("Interrupted while sleeping", e);
550 while (serialInput.available() > 0) {
551 logger.trace("{} bytes of data available to be read", serialInput.available());
552 int readint = serialInput.read();
554 processRequestFromIFace(readint);
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...");
561 } catch (InterruptedException e) {
562 Thread.currentThread().interrupt();
563 // Reset the flag and continue
568 "Reading data from interface complete. Ring Indicator has cleared and no data is left to read.");
571 } catch (IOException e) {
572 logger.warn("IO Exception in serial port handler callback: {}", e.getMessage());
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.
581 * @throws IOException
583 protected void processRequestFromIFace(int readint) throws IOException {
589 receiveCommandData();
591 case INPUT_FILTER_FAIL_REQ:
592 serialOutput.write(DATA_READY_HEAD);
594 "X10 Interface has indicated that the filter and/or surge protection in the device has failed.");
597 logger.warn("Unexpected data received from X10 interface: {}", Integer.toHexString(readint));
602 * Sets the internal clock on the X10 interface
604 * @throws IOException
606 private void setClock() throws IOException {
607 logger.debug("Setting clock in X10 interface");
608 Calendar cal = Calendar.getInstance();
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
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));
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;
630 * Process data that the X10 interface is waiting to send to the PC
632 * @throws IOException
634 private void receiveCommandData() throws IOException {
635 logger.debug("Receiving X10 data from interface");
637 // Send acknowledgement to interface
638 serialOutput.write(DATA_READY_HEAD);
640 // Read the buffer size
641 // There might be several DATA_READY_REQ bytes which need to be ignored
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
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));
652 // It is possible for the cm11a buffer to be empty in which case no processing can take place
654 int[] data = new int[length];
655 for (int i = 0; i < length; i++) {
656 int recvByte = serialInputStr.read();
658 logger.debug(" Received X10 data [{}]: {}", i, Integer.toHexString(recvByte));
661 processCommandData(mask, data);
663 logger.warn("cm11a buffer was overrun. Any pending commands will be ignorred until the buffer clears.");
668 * Process raw data received from the interface and convert into human readable data
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.
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<>();
679 List<X10ReceivedData> rcvData = new ArrayList<>();
681 for (int i = 0; i < data.length; i++) {
683 int dataType = localMask & 0x01;
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]));
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) {
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;
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) {
711 "cm11a received a dim command but there is no prior commands that included an address.");
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);
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
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.");
732 addresses = lastAddresses;
734 "cm11a received a command without any addresses. Addresses from a prior reception are available and will be used.");
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);
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<>();
752 localMask = localMask >> 1;
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);
763 * Called by classes that want to be notified when data has been received from the cm11a
765 * @param listener The class to be called
767 public void addReceivedDataListener(ReceivedDataListener listener) {
768 receiveListeners.add(listener);
772 * Called to notify classes that data has been received
776 private void notifyReceiveListeners(X10ReceivedData rcv) {
777 for (ReceivedDataListener rl : receiveListeners) {
778 rl.receivedX10Data(rcv);
783 * Disconnect from hardware
785 public void disconnect() {
786 // Kill the thread that performs the hardware updates
789 if (serialInput != null) {
792 } catch (IOException e) {
793 // nothing to do if there is an issue closing the stream.
796 if (serialOutput != null) {
798 serialOutput.close();
799 } catch (IOException e) {
800 // nothing to do if there is an issue closing the stream.
804 if (serialPort != null) {