]> git.basschouten.com Git - openhab-addons.git/blob
7c179477462b3b58f03caa6da95fbba7f561a731
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.irtrans.internal.handler;
14
15 import java.io.IOException;
16 import java.io.UnsupportedEncodingException;
17 import java.math.BigDecimal;
18 import java.net.InetAddress;
19 import java.net.InetSocketAddress;
20 import java.nio.ByteBuffer;
21 import java.nio.channels.ClosedChannelException;
22 import java.nio.channels.NoConnectionPendingException;
23 import java.nio.channels.NotYetConnectedException;
24 import java.nio.channels.SelectionKey;
25 import java.nio.channels.Selector;
26 import java.nio.channels.ServerSocketChannel;
27 import java.nio.channels.SocketChannel;
28 import java.util.Collection;
29 import java.util.HashSet;
30 import java.util.Iterator;
31 import java.util.List;
32 import java.util.concurrent.CopyOnWriteArrayList;
33 import java.util.concurrent.locks.Lock;
34 import java.util.concurrent.locks.ReentrantLock;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37
38 import org.apache.commons.lang3.StringUtils;
39 import org.eclipse.jdt.annotation.NonNull;
40 import org.openhab.binding.irtrans.internal.IRtransBindingConstants;
41 import org.openhab.binding.irtrans.internal.IRtransBindingConstants.Led;
42 import org.openhab.binding.irtrans.internal.IrCommand;
43 import org.openhab.core.config.core.Configuration;
44 import org.openhab.core.library.types.StringType;
45 import org.openhab.core.thing.Bridge;
46 import org.openhab.core.thing.Channel;
47 import org.openhab.core.thing.ChannelUID;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.binding.BaseBridgeHandler;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.RefreshType;
53 import org.openhab.core.util.HexUtils;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
56
57 /**
58  * The {@link EthernetBridgeHandler} is responsible for handling commands, which
59  * are sent to one of the channels.
60  *
61  * @author Karel Goderis - Initial contribution
62  *
63  */
64 public class EthernetBridgeHandler extends BaseBridgeHandler implements TransceiverStatusListener {
65
66     // List of Configuration constants
67     public static final String BUFFER_SIZE = "bufferSize";
68     public static final String IP_ADDRESS = "ipAddress";
69     public static final String IS_LISTENER = "isListener";
70     public static final String FIRMWARE_VERSION = "firmwareVersion";
71     public static final String LISTENER_PORT = "listenerPort";
72     public static final String MODE = "mode";
73     public static final String PING_TIME_OUT = "pingTimeOut";
74     public static final String PORT_NUMBER = "portNumber";
75     public static final String RECONNECT_INTERVAL = "reconnectInterval";
76     public static final String REFRESH_INTERVAL = "refreshInterval";
77     public static final String RESPONSE_TIME_OUT = "responseTimeOut";
78     public static final String COMMAND = "command";
79     public static final String LED = "led";
80     public static final String REMOTE = "remote";
81     public static final int LISTENING_INTERVAL = 100;
82
83     private static final Pattern RESPONSE_PATTERN = Pattern.compile("..(\\d{5}) (.*)", Pattern.DOTALL);
84     private static final Pattern HEX_PATTERN = Pattern.compile("RCV_HEX (.*)");
85     private static final Pattern IRDB_PATTERN = Pattern.compile("RCV_COM (.*),(.*),(.*),(.*)");
86
87     private Logger logger = LoggerFactory.getLogger(EthernetBridgeHandler.class);
88
89     private Selector selector;
90     private Thread pollingThread;
91     private SocketChannel socketChannel;
92     protected SelectionKey socketChannelKey;
93     protected ServerSocketChannel listenerChannel;
94     protected SelectionKey listenerKey;
95     protected boolean previousConnectionState;
96     private final Lock lock = new ReentrantLock();
97
98     private List<TransceiverStatusListener> transceiverStatusListeners = new CopyOnWriteArrayList<>();
99
100     /**
101      * Data structure to store the infrared commands that are 'loaded' from the
102      * configuration files. Command loading from pre-defined configuration files is not supported
103      * (anymore), but the code is maintained in case this functionality is re-added in the future
104      **/
105     protected final Collection<IrCommand> irCommands = new HashSet<>();
106
107     public EthernetBridgeHandler(Bridge bridge) {
108         super(bridge);
109         // Nothing to do here
110     }
111
112     @Override
113     public void initialize() {
114         // register ourselves as a Transceiver Status Listener
115         registerTransceiverStatusListener(this);
116
117         try {
118             selector = Selector.open();
119         } catch (IOException e) {
120             logger.debug("An exception occurred while registering the selector: '{}'", e.getMessage());
121             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, e.getMessage());
122         }
123
124         if (selector != null) {
125             if (getConfig().get(IP_ADDRESS) != null && getConfig().get(PORT_NUMBER) != null) {
126                 if (pollingThread == null) {
127                     pollingThread = new Thread(pollingRunnable, "OH-binding-" + getThing().getUID() + "-polling");
128                     pollingThread.start();
129                 }
130             } else {
131                 logger.debug("Cannot connect to IRtrans Ethernet device. IP address or port number not set.");
132                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
133                         "IP address or port number not set.");
134             }
135
136             if (getConfig().get(IS_LISTENER) != null) {
137                 configureListener((String) getConfig().get(LISTENER_PORT));
138             }
139         }
140     }
141
142     @Override
143     public void dispose() {
144         unregisterTransceiverStatusListener(this);
145
146         try {
147             if (socketChannel != null) {
148                 socketChannel.close();
149             }
150         } catch (IOException e) {
151             logger.warn("An exception occurred while closing the channel '{}': {}", socketChannel, e.getMessage());
152         }
153
154         try {
155             if (listenerChannel != null) {
156                 listenerChannel.close();
157             }
158         } catch (IOException e) {
159             logger.warn("An exception occurred while closing the channel '{}': {}", listenerChannel, e.getMessage());
160         }
161
162         try {
163             if (selector != null) {
164                 selector.close();
165             }
166         } catch (IOException e) {
167             logger.debug("An exception occurred while closing the selector: '{}'", e.getMessage());
168         }
169
170         logger.debug("Stopping the IRtrans polling Thread for {}", getThing().getUID());
171         if (pollingThread != null) {
172             pollingThread.interrupt();
173             try {
174                 pollingThread.join();
175             } catch (InterruptedException e) {
176                 Thread.currentThread().interrupt();
177             }
178             pollingThread = null;
179         }
180     }
181
182     public boolean registerTransceiverStatusListener(@NonNull TransceiverStatusListener transceiverStatusListener) {
183         return transceiverStatusListeners.add(transceiverStatusListener);
184     }
185
186     public boolean unregisterTransceiverStatusListener(@NonNull TransceiverStatusListener transceiverStatusListener) {
187         return transceiverStatusListeners.remove(transceiverStatusListener);
188     }
189
190     public void onConnectionLost() {
191         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
192
193         try {
194             if (socketChannel != null) {
195                 socketChannel.close();
196             }
197         } catch (IOException e) {
198             logger.warn("An exception occurred while closing the channel '{}': {}", socketChannel, e.getMessage());
199         }
200
201         establishConnection();
202     }
203
204     public void onConnectionResumed() {
205         configureTransceiver();
206         updateStatus(ThingStatus.ONLINE);
207     }
208
209     private void establishConnection() {
210         lock.lock();
211         try {
212             if (getConfig().get(IP_ADDRESS) != null && getConfig().get(PORT_NUMBER) != null) {
213                 try {
214                     socketChannel = SocketChannel.open();
215                     socketChannel.socket().setKeepAlive(true);
216                     socketChannel.configureBlocking(false);
217
218                     synchronized (selector) {
219                         selector.wakeup();
220                         int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE | SelectionKey.OP_CONNECT;
221                         socketChannelKey = socketChannel.register(selector, interestSet);
222                     }
223
224                     InetSocketAddress remoteAddress = new InetSocketAddress((String) getConfig().get(IP_ADDRESS),
225                             ((BigDecimal) getConfig().get(PORT_NUMBER)).intValue());
226                     socketChannel.connect(remoteAddress);
227                 } catch (IOException e) {
228                     logger.debug("An exception occurred while connecting to '{}:{}' : {}", getConfig().get(IP_ADDRESS),
229                             ((BigDecimal) getConfig().get(PORT_NUMBER)).intValue(), e.getMessage());
230                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
231                 }
232
233                 try {
234                     Thread.sleep(((BigDecimal) getConfig().get(RESPONSE_TIME_OUT)).intValue());
235                 } catch (NumberFormatException | InterruptedException e) {
236                     Thread.currentThread().interrupt();
237                     logger.debug("An exception occurred while putting a thread to sleep: '{}'", e.getMessage());
238                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
239                 }
240                 onConnectable();
241             }
242         } finally {
243             lock.unlock();
244         }
245     }
246
247     @Override
248     public void onCommandReceived(EthernetBridgeHandler bridge, IrCommand command) {
249         logger.debug("Received infrared command '{},{}' for thing '{}'", command.getRemote(), command.getCommand(),
250                 this.getThing().getUID());
251
252         for (Channel channel : getThing().getChannels()) {
253             Configuration channelConfiguration = channel.getConfiguration();
254
255             if (channel.getChannelTypeUID() != null
256                     && channel.getChannelTypeUID().getId().equals(IRtransBindingConstants.RECEIVER_CHANNEL_TYPE)) {
257                 IrCommand thingCompatibleCommand = new IrCommand();
258                 thingCompatibleCommand.setRemote((String) channelConfiguration.get(REMOTE));
259                 thingCompatibleCommand.setCommand((String) channelConfiguration.get(COMMAND));
260
261                 if (command.matches(thingCompatibleCommand)) {
262                     StringType stringType = new StringType(command.getRemote() + "," + command.getCommand());
263                     logger.debug("Received a matching infrared command '{}' for channel '{}'", stringType,
264                             channel.getUID());
265                     updateState(channel.getUID(), stringType);
266                 }
267             }
268         }
269     }
270
271     @Override
272     public void handleCommand(ChannelUID channelUID, Command command) {
273         if (!(command instanceof RefreshType)) {
274             Channel channel = this.getThing().getChannel(channelUID.getId());
275             if (channel != null) {
276                 Configuration channelConfiguration = channel.getConfiguration();
277                 if (channel.getChannelTypeUID() != null
278                         && channel.getChannelTypeUID().getId().equals(IRtransBindingConstants.BLASTER_CHANNEL_TYPE)) {
279                     if (command instanceof StringType) {
280                         String remoteName = StringUtils.substringBefore(command.toString(), ",");
281                         String irCommandName = StringUtils.substringAfter(command.toString(), ",");
282
283                         IrCommand ircommand = new IrCommand();
284                         ircommand.setRemote(remoteName);
285                         ircommand.setCommand(irCommandName);
286
287                         IrCommand thingCompatibleCommand = new IrCommand();
288                         thingCompatibleCommand.setRemote((String) channelConfiguration.get(REMOTE));
289                         thingCompatibleCommand.setCommand((String) channelConfiguration.get(COMMAND));
290
291                         if (ircommand.matches(thingCompatibleCommand)) {
292                             if (sendIRcommand(ircommand, Led.get((String) channelConfiguration.get(LED)))) {
293                                 logger.debug("Sent a matching infrared command '{}' for channel '{}'", command,
294                                         channelUID);
295                             } else {
296                                 logger.warn(
297                                         "An error occured whilst sending the infrared command '{}' for Channel '{}'",
298                                         command, channelUID);
299                             }
300                         }
301                     }
302                 }
303                 if (channel.getAcceptedItemType() != null
304                         && channel.getAcceptedItemType().equals(IRtransBindingConstants.RECEIVER_CHANNEL_TYPE)) {
305                     logger.warn("Receivers can only receive infrared commands, not send them");
306                 }
307             }
308         }
309     }
310
311     private void configureListener(String listenerPort) {
312         try {
313             listenerChannel = ServerSocketChannel.open();
314             listenerChannel.socket().bind(new InetSocketAddress(Integer.parseInt(listenerPort)));
315             listenerChannel.configureBlocking(false);
316
317             logger.info("Listening for incoming connections on {}", listenerChannel.getLocalAddress());
318
319             synchronized (selector) {
320                 selector.wakeup();
321                 try {
322                     listenerKey = listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
323                 } catch (ClosedChannelException e1) {
324                     logger.debug("An exception occurred while registering a selector: '{}'", e1.getMessage());
325                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e1.getMessage());
326                 }
327             }
328         } catch (IOException e3) {
329             logger.error(
330                     "An exception occurred while creating configuring the listener channel on port number {}: '{}'",
331                     Integer.parseInt(listenerPort), e3.getMessage());
332         }
333     }
334
335     protected void configureTransceiver() {
336         lock.lock();
337         try {
338             String putInASCIImode = "ASCI";
339             ByteBuffer response = sendCommand(putInASCIImode);
340
341             String getFirmwareVersion = "Aver" + (char) 13;
342             response = sendCommand(getFirmwareVersion);
343
344             if (response != null) {
345                 String message = stripByteCount(response).split("\0")[0];
346                 if (message != null) {
347                     if (message.contains("VERSION")) {
348                         logger.info("'{}' matches an IRtrans device with firmware {}", getThing().getUID(), message);
349                         getConfig().put(FIRMWARE_VERSION, message);
350                     } else {
351                         logger.debug("Received some non-compliant garbage ({})", message);
352                         onConnectionLost();
353                     }
354                 }
355             } else {
356                 try {
357                     logger.debug("Did not receive an answer from the IRtrans transceiver '{}' - Parsing is skipped",
358                             socketChannel.getRemoteAddress());
359                     onConnectionLost();
360                 } catch (IOException e1) {
361                     logger.debug("An exception occurred while getting a remote address: '{}'", e1.getMessage());
362                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e1.getMessage());
363                 }
364             }
365
366             int numberOfRemotes = 0;
367             int numberOfRemotesProcessed = 0;
368             int numberOfRemotesInBatch = 0;
369             String[] remoteList = getRemoteList(0);
370
371             if (remoteList.length > 0) {
372                 logger.debug("The IRtrans device for '{}' supports '{}' remotes", getThing().getUID(), remoteList[1]);
373                 numberOfRemotes = Integer.valueOf(remoteList[1]);
374                 numberOfRemotesInBatch = Integer.valueOf(remoteList[2]);
375             }
376
377             while (numberOfRemotesProcessed < numberOfRemotes) {
378                 for (int i = 1; i <= numberOfRemotesInBatch; i++) {
379                     String remote = remoteList[2 + i];
380
381                     // get remote commands
382                     String[] commands = getRemoteCommands(remote, 0);
383                     StringBuilder result = new StringBuilder();
384                     int numberOfCommands = 0;
385                     int numberOfCommandsInBatch = 0;
386                     int numberOfCommandsProcessed = 0;
387
388                     if (commands.length > 0) {
389                         numberOfCommands = Integer.valueOf(commands[1]);
390                         numberOfCommandsInBatch = Integer.valueOf(commands[2]);
391                         numberOfCommandsProcessed = 0;
392                     }
393
394                     while (numberOfCommandsProcessed < numberOfCommands) {
395                         for (int j = 1; j <= numberOfCommandsInBatch; j++) {
396                             String command = commands[2 + j];
397                             result.append(command);
398                             numberOfCommandsProcessed++;
399                             if (numberOfCommandsProcessed < numberOfCommands) {
400                                 result.append(", ");
401                             }
402                         }
403
404                         if (numberOfCommandsProcessed < numberOfCommands) {
405                             commands = getRemoteCommands(remote, numberOfCommandsProcessed);
406                             if (commands.length == 0) {
407                                 break;
408                             }
409                             numberOfCommandsInBatch = Integer.valueOf(commands[2]);
410                         } else {
411                             numberOfCommandsInBatch = 0;
412                         }
413                     }
414
415                     logger.debug("The remote '{}' on '{}' supports '{}' commands: {}", remote, getThing().getUID(),
416                             numberOfCommands, result.toString());
417
418                     numberOfRemotesProcessed++;
419                 }
420
421                 if (numberOfRemotesProcessed < numberOfRemotes) {
422                     remoteList = getRemoteList(numberOfRemotesProcessed);
423                     if (remoteList.length == 0) {
424                         break;
425                     }
426                     numberOfRemotesInBatch = Integer.valueOf(remoteList[2]);
427                 } else {
428                     numberOfRemotesInBatch = 0;
429                 }
430             }
431         } finally {
432             lock.unlock();
433         }
434     }
435
436     private String[] getRemoteCommands(String remote, int index) {
437         String getCommands = "Agetcommands " + remote + "," + index + (char) 13;
438         ByteBuffer response = sendCommand(getCommands);
439         String[] commandList = new String[0];
440
441         if (response != null) {
442             String message = stripByteCount(response).split("\0")[0];
443             logger.trace("commands returned {}", message);
444             if (message != null) {
445                 if (message.contains("COMMANDLIST")) {
446                     commandList = message.split(",");
447                 } else {
448                     logger.debug("Received some non-compliant command ({})", message);
449                     onConnectionLost();
450                 }
451             }
452         } else {
453             logger.debug("Did not receive an answer from the IRtrans transceiver for '{}' - Parsing is skipped",
454                     getThing().getUID());
455             onConnectionLost();
456         }
457
458         return commandList;
459     }
460
461     private String[] getRemoteList(int index) {
462         String getRemotes = "Agetremotes " + index + (char) 13;
463         ByteBuffer response = sendCommand(getRemotes);
464         String[] remoteList = new String[0];
465
466         if (response != null) {
467             String message = stripByteCount(response).split("\0")[0];
468             logger.trace("remotes returned {}", message);
469             if (message != null) {
470                 if (message.contains("REMOTELIST")) {
471                     remoteList = message.split(",");
472                 } else {
473                     logger.debug("Received some non-compliant command ({})", message);
474                     onConnectionLost();
475                 }
476             }
477         } else {
478             logger.debug("Did not receive an answer from the IRtrans transceiver for '{}' - Parsing is skipped",
479                     getThing().getUID());
480             onConnectionLost();
481         }
482
483         return remoteList;
484     }
485
486     public boolean sendIRcommand(IrCommand command, Led led) {
487         // construct the string we need to send to the IRtrans device
488         String output = packIRDBCommand(led, command);
489
490         lock.lock();
491         try {
492             ByteBuffer response = sendCommand(output);
493
494             if (response != null) {
495                 String message = stripByteCount(response).split("\0")[0];
496                 if (message != null && message.contains("RESULT OK")) {
497                     return true;
498                 } else {
499                     logger.debug("Received an unexpected response from the IRtrans transceiver: '{}'", message);
500                     return false;
501                 }
502             }
503         } finally {
504             lock.unlock();
505         }
506
507         return false;
508     }
509
510     private ByteBuffer sendCommand(String command) {
511         if (command != null) {
512             ByteBuffer byteBuffer = ByteBuffer.allocate(command.getBytes().length);
513             try {
514                 byteBuffer.put(command.getBytes("ASCII"));
515                 onWritable(byteBuffer);
516                 Thread.sleep(((BigDecimal) getConfig().get(RESPONSE_TIME_OUT)).intValue());
517                 return onReadable(((BigDecimal) getConfig().get(BUFFER_SIZE)).intValue(), true);
518             } catch (UnsupportedEncodingException | NumberFormatException | InterruptedException e) {
519                 Thread.currentThread().interrupt();
520                 logger.debug("An exception occurred while sending a command to the IRtrans transceiver for '{}': {}",
521                         getThing().getUID(), e.getMessage());
522                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
523             }
524         }
525
526         return null;
527     }
528
529     private Runnable pollingRunnable = new Runnable() {
530
531         @Override
532         public void run() {
533             while (true) {
534                 try {
535                     if (socketChannel == null) {
536                         previousConnectionState = false;
537                         onConnectionLost();
538                     } else {
539                         if (!previousConnectionState && socketChannel.isConnected()) {
540                             previousConnectionState = true;
541                             onConnectionResumed();
542                         }
543
544                         if (previousConnectionState && !socketChannel.isConnected()
545                                 && !socketChannel.isConnectionPending()) {
546                             previousConnectionState = false;
547                             onConnectionLost();
548                         }
549
550                         if (!socketChannel.isConnectionPending() && !socketChannel.isConnected()) {
551                             previousConnectionState = false;
552                             logger.debug("Disconnecting '{}' because of a network error", getThing().getUID());
553                             socketChannel.close();
554                             Thread.sleep(1000 * ((BigDecimal) getConfig().get(RECONNECT_INTERVAL)).intValue());
555                             establishConnection();
556                         }
557
558                         long stamp = System.currentTimeMillis();
559                         if (!InetAddress.getByName(((String) getConfig().get(IP_ADDRESS)))
560                                 .isReachable(((BigDecimal) getConfig().get(PING_TIME_OUT)).intValue())) {
561                             logger.debug(
562                                     "Ping timed out after '{}' milliseconds. Disconnecting '{}' because of a ping timeout",
563                                     System.currentTimeMillis() - stamp, getThing().getUID());
564                             socketChannel.close();
565                         }
566
567                         onConnectable();
568                         ByteBuffer buffer = onReadable(((BigDecimal) getConfig().get(BUFFER_SIZE)).intValue(), false);
569                         if (buffer != null && buffer.remaining() > 0) {
570                             onRead(buffer);
571                         }
572                     }
573
574                     onAcceptable();
575
576                     if (!Thread.currentThread().isInterrupted()) {
577                         Thread.sleep(LISTENING_INTERVAL);
578                     } else {
579                         return;
580                     }
581                 } catch (IOException e) {
582                     logger.trace("An exception occurred while polling the transceiver : '{}'", e.getMessage(), e);
583                 } catch (InterruptedException e) {
584                     Thread.currentThread().interrupt();
585                     return;
586                 }
587             }
588         }
589     };
590
591     protected void onAcceptable() {
592         lock.lock();
593         try {
594             try {
595                 selector.selectNow();
596             } catch (IOException e) {
597                 logger.debug("An exception occurred while selecting: {}", e.getMessage());
598                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
599             }
600
601             Iterator<SelectionKey> it = selector.selectedKeys().iterator();
602             while (it.hasNext()) {
603                 SelectionKey selKey = it.next();
604                 it.remove();
605                 if (selKey.isValid()) {
606                     if (selKey.isAcceptable() && selKey.equals(listenerKey)) {
607                         try {
608                             SocketChannel newChannel = listenerChannel.accept();
609                             newChannel.configureBlocking(false);
610                             logger.trace("Received a connection request from '{}'", newChannel.getRemoteAddress());
611
612                             synchronized (selector) {
613                                 selector.wakeup();
614                                 newChannel.register(selector, newChannel.validOps());
615                             }
616                         } catch (IOException e) {
617                             logger.debug("An exception occurred while accepting a connection on channel '{}': {}",
618                                     listenerChannel, e.getMessage());
619                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
620                         }
621                     }
622                 }
623             }
624         } finally {
625             lock.unlock();
626         }
627     }
628
629     protected void onConnectable() {
630         lock.lock();
631         SocketChannel aSocketChannel = null;
632         try {
633             synchronized (selector) {
634                 selector.selectNow();
635             }
636
637             Iterator<SelectionKey> it = selector.selectedKeys().iterator();
638             while (it.hasNext()) {
639                 SelectionKey selKey = it.next();
640                 it.remove();
641                 if (selKey.isValid() && selKey.isConnectable()) {
642                     aSocketChannel = (SocketChannel) selKey.channel();
643                     aSocketChannel.finishConnect();
644                     logger.trace("The channel for '{}' is connected", aSocketChannel.getRemoteAddress());
645                 }
646             }
647         } catch (IOException | NoConnectionPendingException e) {
648             if (aSocketChannel != null) {
649                 logger.debug("Disconnecting '{}' because of a socket error : '{}'", getThing().getUID(), e.getMessage(),
650                         e);
651                 try {
652                     aSocketChannel.close();
653                 } catch (IOException e1) {
654                     logger.debug("An exception occurred while closing the channel '{}': {}", socketChannel,
655                             e1.getMessage());
656                 }
657             }
658         } finally {
659             lock.unlock();
660         }
661     }
662
663     protected ByteBuffer onReadable(int bufferSize, boolean isSelective) {
664         lock.lock();
665         try {
666             synchronized (selector) {
667                 try {
668                     selector.selectNow();
669                 } catch (IOException e) {
670                     logger.error("An exception occurred while selecting: {}", e.getMessage());
671                 }
672             }
673
674             Iterator<SelectionKey> it = selector.selectedKeys().iterator();
675             while (it.hasNext()) {
676                 SelectionKey selKey = it.next();
677                 it.remove();
678                 if (selKey.isValid() && selKey.isReadable()) {
679                     SocketChannel aSocketChannel = (SocketChannel) selKey.channel();
680
681                     if ((aSocketChannel.equals(socketChannel) && isSelective) || !isSelective) {
682                         ByteBuffer readBuffer = ByteBuffer.allocate(bufferSize);
683                         int numberBytesRead = 0;
684                         boolean error = false;
685
686                         try {
687                             numberBytesRead = aSocketChannel.read(readBuffer);
688                         } catch (NotYetConnectedException e) {
689                             logger.warn("The channel '{}' is not yet connected: {}", aSocketChannel, e.getMessage());
690                             if (!aSocketChannel.isConnectionPending()) {
691                                 error = true;
692                             }
693                         } catch (IOException e) {
694                             // If some other I/O error occurs
695                             logger.warn("An IO exception occured on channel '{}': {}", aSocketChannel, e.getMessage());
696                             error = true;
697                         }
698
699                         if (numberBytesRead == -1) {
700                             error = true;
701                         }
702
703                         if (error) {
704                             logger.debug("Disconnecting '{}' because of a socket error", getThing().getUID());
705                             try {
706                                 aSocketChannel.close();
707                             } catch (IOException e1) {
708                                 logger.debug("An exception occurred while closing the channel '{}': {}", socketChannel,
709                                         e1.getMessage());
710                             }
711                         } else {
712                             readBuffer.flip();
713                             return readBuffer;
714                         }
715                     }
716                 }
717             }
718
719             return null;
720         } finally {
721             lock.unlock();
722         }
723     }
724
725     protected void onWritable(ByteBuffer buffer) {
726         lock.lock();
727         try {
728             synchronized (selector) {
729                 try {
730                     selector.selectNow();
731                 } catch (IOException e) {
732                     logger.error("An exception occurred while selecting: {}", e.getMessage());
733                 }
734             }
735
736             Iterator<SelectionKey> it = selector.selectedKeys().iterator();
737             while (it.hasNext()) {
738                 SelectionKey selKey = it.next();
739                 it.remove();
740                 if (selKey.isValid() && selKey.isWritable()) {
741                     SocketChannel aSocketChannel = (SocketChannel) selKey.channel();
742
743                     if (aSocketChannel.equals(socketChannel)) {
744                         boolean error = false;
745
746                         buffer.rewind();
747                         try {
748                             logger.trace("Sending '{}' on the channel '{}'->'{}'", new String(buffer.array()),
749                                     aSocketChannel.getLocalAddress(), aSocketChannel.getRemoteAddress());
750                             aSocketChannel.write(buffer);
751                         } catch (NotYetConnectedException e) {
752                             logger.warn("The channel '{}' is not yet connected: {}", aSocketChannel, e.getMessage());
753                             if (!aSocketChannel.isConnectionPending()) {
754                                 error = true;
755                             }
756                         } catch (ClosedChannelException e) {
757                             // If some other I/O error occurs
758                             logger.warn("The channel for '{}' is closed: {}", aSocketChannel, e.getMessage());
759                             error = true;
760                         } catch (IOException e) {
761                             // If some other I/O error occurs
762                             logger.warn("An IO exception occured on channel '{}': {}", aSocketChannel, e.getMessage());
763                             error = true;
764                         }
765
766                         if (error) {
767                             try {
768                                 aSocketChannel.close();
769                             } catch (IOException e) {
770                                 logger.warn("An exception occurred while closing the channel '{}': {}", aSocketChannel,
771                                         e.getMessage());
772                             }
773                         }
774                     }
775                 }
776             }
777         } finally {
778             lock.unlock();
779         }
780     }
781
782     protected void onRead(ByteBuffer byteBuffer) {
783         try {
784             if (logger.isTraceEnabled()) {
785                 logger.trace("Received bytebuffer : '{}'", HexUtils.bytesToHex(byteBuffer.array()));
786             }
787             int byteCount = getByteCount(byteBuffer);
788
789             while (byteCount > 0) {
790                 byte[] message = new byte[byteCount];
791                 byteBuffer.get(message, 0, byteCount);
792
793                 if (logger.isTraceEnabled()) {
794                     logger.trace("Received message : '{}'", HexUtils.bytesToHex(message));
795                 }
796
797                 String strippedBuffer = stripByteCount(ByteBuffer.wrap(message));
798
799                 if (strippedBuffer != null) {
800                     String strippedMessage = strippedBuffer.split("\0")[0];
801
802                     // IRTrans devices return "RESULT OK" when it succeeds to emit an
803                     // infrared sequence
804                     if (strippedMessage.contains("RESULT OK")) {
805                         parseOKMessage(strippedMessage);
806                     }
807
808                     // IRTrans devices return a string starting with RCV_HEX each time
809                     // it captures an infrared sequence from a remote control
810                     if (strippedMessage.contains("RCV_HEX")) {
811                         parseHexMessage(strippedMessage);
812                     }
813
814                     // IRTrans devices return a string starting with RCV_COM each time
815                     // it captures an infrared sequence from a remote control that is stored in the device's internal dB
816                     if (strippedMessage.contains("RCV_COM")) {
817                         parseIRDBMessage(strippedMessage);
818                     }
819
820                     byteCount = getByteCount(byteBuffer);
821                 } else {
822                     logger.warn("Received some non-compliant garbage '{}' - Parsing is skipped",
823                             new String(byteBuffer.array()));
824                 }
825             }
826         } catch (Exception e) {
827             logger.error("An exception occurred while reading bytebuffer '{}' : {}",
828                     HexUtils.bytesToHex(byteBuffer.array()), e.getMessage(), e);
829         }
830     }
831
832     protected int getByteCount(ByteBuffer byteBuffer) {
833         String response = new String(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
834         response = StringUtils.chomp(response);
835
836         Matcher matcher = RESPONSE_PATTERN.matcher(response);
837         if (matcher.matches()) {
838             return Integer.parseInt(matcher.group(1));
839         }
840
841         return 0;
842     }
843
844     protected String stripByteCount(ByteBuffer byteBuffer) {
845         String message = null;
846
847         String response = new String(byteBuffer.array(), 0, byteBuffer.limit());
848         response = StringUtils.chomp(response);
849
850         Matcher matcher = RESPONSE_PATTERN.matcher(response);
851         if (matcher.matches()) {
852             message = matcher.group(2);
853         }
854
855         return message;
856     }
857
858     protected void parseOKMessage(String message) {
859         // Nothing to do here
860     }
861
862     protected void parseHexMessage(String message) {
863         Matcher matcher = HEX_PATTERN.matcher(message);
864
865         if (matcher.matches()) {
866             String command = matcher.group(1);
867
868             IrCommand theCommand = null;
869             for (IrCommand aCommand : irCommands) {
870                 if (aCommand.sequenceToHEXString().equals(command)) {
871                     theCommand = aCommand;
872                     break;
873                 }
874             }
875
876             if (theCommand != null) {
877                 for (TransceiverStatusListener listener : transceiverStatusListeners) {
878                     listener.onCommandReceived(this, theCommand);
879                 }
880             } else {
881                 logger.error("{} does not match any know infrared command", command);
882             }
883         } else {
884             logger.error("{} does not match the infrared message format '{}'", message, matcher.pattern());
885         }
886     }
887
888     protected void parseIRDBMessage(String message) {
889         Matcher matcher = IRDB_PATTERN.matcher(message);
890
891         if (matcher.matches()) {
892             IrCommand command = new IrCommand();
893             command.setRemote(matcher.group(1));
894             command.setCommand(matcher.group(2));
895
896             for (TransceiverStatusListener listener : transceiverStatusListeners) {
897                 listener.onCommandReceived(this, command);
898             }
899         } else {
900             logger.error("{} does not match the IRDB infrared message format '{}'", message, matcher.pattern());
901         }
902     }
903
904     /**
905      * "Pack" the infrared command so that it can be sent to the IRTrans device
906      *
907      * @param led the led
908      * @param command the the command
909      * @return a string which is the full command to be sent to the device
910      */
911     protected String packIRDBCommand(Led led, IrCommand command) {
912         String output = new String();
913
914         output = "Asnd ";
915         output += command.getRemote();
916         output += ",";
917         output += command.getCommand();
918         output += ",l";
919         output += led.toString();
920
921         output += "\r\n";
922
923         return output;
924     }
925
926     /**
927      * "Pack" the infrared command so that it can be sent to the IRTrans device
928      *
929      * @param led the led
930      * @param command the the command
931      * @return a string which is the full command to be sent to the device
932      */
933     protected String packHexCommand(Led led, IrCommand command) {
934         String output = new String();
935
936         output = "Asndhex ";
937         output += "L";
938         output += led.toString();
939
940         output += ",";
941         output += "H" + command.toHEXString();
942
943         output += (char) 13;
944
945         return output;
946     }
947 }