]> git.basschouten.com Git - openhab-addons.git/blob
63fed15c9862b7e24d7683ea6ab54268f8a0f781
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.globalcache.internal.handler;
14
15 import static org.openhab.binding.globalcache.internal.GlobalCacheBindingConstants.*;
16
17 import java.io.BufferedInputStream;
18 import java.io.BufferedReader;
19 import java.io.ByteArrayOutputStream;
20 import java.io.DataOutputStream;
21 import java.io.IOException;
22 import java.io.InputStreamReader;
23 import java.io.UnsupportedEncodingException;
24 import java.net.InetAddress;
25 import java.net.InetSocketAddress;
26 import java.net.NetworkInterface;
27 import java.net.Socket;
28 import java.net.SocketException;
29 import java.net.URLDecoder;
30 import java.net.URLEncoder;
31 import java.net.UnknownHostException;
32 import java.util.concurrent.LinkedBlockingQueue;
33 import java.util.concurrent.ScheduledExecutorService;
34 import java.util.concurrent.ScheduledFuture;
35 import java.util.concurrent.TimeUnit;
36 import java.util.concurrent.atomic.AtomicInteger;
37 import java.util.regex.Pattern;
38
39 import org.apache.commons.lang.StringUtils;
40 import org.eclipse.jdt.annotation.NonNull;
41 import org.openhab.binding.globalcache.internal.GlobalCacheBindingConstants.CommandType;
42 import org.openhab.binding.globalcache.internal.command.CommandGetstate;
43 import org.openhab.binding.globalcache.internal.command.CommandGetversion;
44 import org.openhab.binding.globalcache.internal.command.CommandSendir;
45 import org.openhab.binding.globalcache.internal.command.CommandSendserial;
46 import org.openhab.binding.globalcache.internal.command.CommandSetstate;
47 import org.openhab.binding.globalcache.internal.command.RequestMessage;
48 import org.openhab.binding.globalcache.internal.command.ResponseMessage;
49 import org.openhab.core.common.ThreadPoolManager;
50 import org.openhab.core.library.types.OnOffType;
51 import org.openhab.core.library.types.StringType;
52 import org.openhab.core.thing.Channel;
53 import org.openhab.core.thing.ChannelUID;
54 import org.openhab.core.thing.Thing;
55 import org.openhab.core.thing.ThingStatus;
56 import org.openhab.core.thing.ThingStatusDetail;
57 import org.openhab.core.thing.ThingTypeUID;
58 import org.openhab.core.thing.binding.BaseThingHandler;
59 import org.openhab.core.transform.TransformationException;
60 import org.openhab.core.transform.TransformationHelper;
61 import org.openhab.core.transform.TransformationService;
62 import org.openhab.core.types.Command;
63 import org.openhab.core.types.RefreshType;
64 import org.osgi.framework.BundleContext;
65 import org.osgi.framework.FrameworkUtil;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
68
69 /**
70  * The {@link GlobalCacheHandler} is responsible for handling commands, which are
71  * sent to one of the channels.
72  *
73  * @author Mark Hilbush - Initial contribution
74  */
75 public class GlobalCacheHandler extends BaseThingHandler {
76     private Logger logger = LoggerFactory.getLogger(GlobalCacheHandler.class);
77
78     private final BundleContext bundleContext;
79
80     private static final String GLOBALCACHE_THREAD_POOL = "globalCacheHandler";
81
82     private InetAddress ifAddress;
83     private CommandProcessor commandProcessor;
84     private ScheduledExecutorService scheduledExecutorService = ThreadPoolManager
85             .getScheduledPool(GLOBALCACHE_THREAD_POOL + "-" + thingID());
86     private ScheduledFuture<?> scheduledFuture;
87
88     private LinkedBlockingQueue<RequestMessage> sendQueue = null;
89
90     private String ipv4Address;
91
92     // IR transaction counter
93     private AtomicInteger irCounter;
94
95     // Character set to use for URL encoding & decoding
96     private String CHARSET = "ISO-8859-1";
97
98     public GlobalCacheHandler(@NonNull Thing gcDevice, String ipv4Address) {
99         super(gcDevice);
100         irCounter = new AtomicInteger(1);
101         commandProcessor = new CommandProcessor();
102         scheduledFuture = null;
103         this.ipv4Address = ipv4Address;
104         this.bundleContext = FrameworkUtil.getBundle(GlobalCacheHandler.class).getBundleContext();
105     }
106
107     @Override
108     public void initialize() {
109         logger.debug("Initializing thing {}", thingID());
110         try {
111             ifAddress = InetAddress.getByName(ipv4Address);
112             logger.debug("Handler using address {} on network interface {}", ifAddress.getHostAddress(),
113                     NetworkInterface.getByInetAddress(ifAddress).getName());
114         } catch (SocketException e) {
115             logger.error("Handler got Socket exception creating multicast socket: {}", e.getMessage());
116             markThingOfflineWithError(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No suitable network interface");
117             return;
118         } catch (UnknownHostException e) {
119             logger.error("Handler got UnknownHostException getting local IPv4 network interface: {}", e.getMessage());
120             markThingOfflineWithError(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No suitable network interface");
121             return;
122         }
123         scheduledFuture = scheduledExecutorService.schedule(commandProcessor, 2, TimeUnit.SECONDS);
124     }
125
126     @Override
127     public void dispose() {
128         logger.debug("Disposing thing {}", thingID());
129         commandProcessor.terminate();
130         if (scheduledFuture != null) {
131             scheduledFuture.cancel(false);
132         }
133     }
134
135     @Override
136     public void handleCommand(ChannelUID channelUID, Command command) {
137         if (command == null) {
138             logger.warn("Command passed to handler for thing {} is null", thingID());
139             return;
140         }
141
142         // Don't try to send command if the device is not online
143         if (!isOnline()) {
144             logger.debug("Can't handle command {} because handler for thing {} is not ONLINE", command, thingID());
145             return;
146         }
147
148         Channel channel = thing.getChannel(channelUID.getId());
149         if (channel == null) {
150             logger.warn("Unknown channel {} for thing {}; is item defined correctly", channelUID.getId(), thingID());
151             return;
152         }
153
154         // Get module and connector properties for this channel
155         String modNum = channel.getProperties().get(CHANNEL_PROPERTY_MODULE);
156         String conNum = channel.getProperties().get(CHANNEL_PROPERTY_CONNECTOR);
157         if (modNum == null || conNum == null) {
158             logger.error("Channel {} of thing {} has no module/connector property", channelUID.getId(), thingID());
159             return;
160         }
161
162         if (command instanceof RefreshType) {
163             handleRefresh(modNum, conNum, channel);
164             return;
165         }
166
167         switch (channel.getChannelTypeUID().getId()) {
168             case CHANNEL_TYPE_CC:
169                 handleContactClosure(modNum, conNum, command, channelUID);
170                 break;
171
172             case CHANNEL_TYPE_IR:
173                 handleInfrared(modNum, conNum, command, channelUID);
174                 break;
175
176             case CHANNEL_TYPE_SL:
177                 handleSerial(modNum, conNum, command, channelUID);
178                 break;
179
180             case CHANNEL_TYPE_SL_DIRECT:
181                 handleSerialDirect(modNum, conNum, command, channelUID);
182                 break;
183
184             default:
185                 logger.warn("Thing {} has unknown channel type {}", thingID(), channel.getChannelTypeUID().getId());
186                 break;
187         }
188     }
189
190     private void handleContactClosure(String modNum, String conNum, Command command, ChannelUID channelUID) {
191         logger.debug("Handling CC command {} on channel {} of thing {}", command, channelUID.getId(), thingID());
192
193         if (command instanceof OnOffType) {
194             CommandSetstate setstate = new CommandSetstate(thing, command, sendQueue, modNum, conNum);
195             setstate.execute();
196         }
197     }
198
199     private void handleInfrared(String modNum, String conNum, Command command, ChannelUID channelUID) {
200         logger.debug("Handling infrared command {} on channel {} of thing {}", command, channelUID.getId(), thingID());
201
202         String irCode = lookupCode(command);
203         if (irCode != null) {
204             CommandSendir sendir = new CommandSendir(thing, command, sendQueue, modNum, conNum, irCode, getCounter());
205             sendir.execute();
206         }
207     }
208
209     private void handleSerial(String modNum, String conNum, Command command, ChannelUID channelUID) {
210         logger.debug("Handle serial command {} on channel {} of thing {}", command, channelUID.getId(), thingID());
211
212         String slCode = lookupCode(command);
213         if (slCode != null) {
214             CommandSendserial sendserial = new CommandSendserial(thing, command, sendQueue, modNum, conNum, slCode);
215             sendserial.execute();
216         }
217     }
218
219     private void handleSerialDirect(String modNum, String conNum, Command command, ChannelUID channelUID) {
220         logger.debug("Handle serial command {} on channel {} of thing {}", command, channelUID.getId(), thingID());
221
222         CommandSendserial sendserial = new CommandSendserial(thing, command, sendQueue, modNum, conNum,
223                 command.toString());
224         sendserial.execute();
225     }
226
227     private void handleRefresh(String modNum, String conNum, Channel channel) {
228         // REFRESH makes sense only for CC channels because we can query the device for the relay state
229         if (channel.getChannelTypeUID().getId().equals(CHANNEL_TYPE_CC)) {
230             logger.debug("Handle REFRESH command on channel {} for thing {}", channel.getUID().getId(), thingID());
231
232             CommandGetstate getstate = new CommandGetstate(thing, sendQueue, modNum, conNum);
233             getstate.execute();
234             if (getstate.isSuccessful()) {
235                 updateState(channel.getUID(), getstate.state());
236             }
237         }
238     }
239
240     private int getCounter() {
241         return irCounter.getAndIncrement();
242     }
243
244     /*
245      * Look up the IR or serial command code in the MAP file.
246      *
247      */
248     private String lookupCode(Command command) {
249         if (command.toString() == null) {
250             logger.warn("Unable to perform transform on null command string");
251             return null;
252         }
253
254         String mapFile = (String) thing.getConfiguration().get(THING_CONFIG_MAP_FILENAME);
255         if (StringUtils.isEmpty(mapFile)) {
256             logger.warn("MAP file is not defined in configuration of thing {}", thingID());
257             return null;
258         }
259
260         TransformationService transformService = TransformationHelper.getTransformationService(bundleContext, "MAP");
261         if (transformService == null) {
262             logger.error("Failed to get MAP transformation service for thing {}; is bundle installed?", thingID());
263             return null;
264         }
265
266         String code;
267         try {
268             code = transformService.transform(mapFile, command.toString());
269
270         } catch (TransformationException e) {
271             logger.error("Failed to transform {} for thing {} using map file '{}', exception={}", command, thingID(),
272                     mapFile, e.getMessage());
273             return null;
274         }
275
276         if (StringUtils.isEmpty(code)) {
277             logger.warn("No entry for {} in map file '{}' for thing {}", command, mapFile, thingID());
278             return null;
279         }
280
281         logger.debug("Transformed {} for thing {} with map file '{}'", command, thingID(), mapFile);
282
283         // Determine if the code is hex format. If so, convert to GC format
284         if (isHexCode(code)) {
285             logger.debug("Code is in hex format, convert to GC format");
286             try {
287                 code = convertHexToGC(code);
288                 logger.debug("Converted hex code is: {}", code);
289             } catch (HexCodeConversionException e) {
290                 logger.info("Failed to convert hex code to globalcache format: {}", e.getMessage());
291                 return null;
292             }
293         }
294         return code;
295     }
296
297     /*
298      * Check if the string looks like a hex code; if not then assume it's GC format
299      */
300     private boolean isHexCode(String code) {
301         Pattern pattern = Pattern.compile("0000( +[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f])+");
302         return pattern.matcher(code).find();
303     }
304
305     /*
306      * Convert a hex code IR string to a Global Cache formatted IR string
307      */
308     private String convertHexToGC(String hexCode) throws HexCodeConversionException {
309         // Magic number for converting frequency to GC format
310         final int freqConversionFactor = 4145146;
311         final int repeat = 1;
312         int frequency;
313         int sequence1Length;
314         int offset;
315
316         String[] hexCodeArray = hexCode.trim().split(" ");
317
318         if (hexCodeArray.length < 5) {
319             throw new HexCodeConversionException("Hex code is too short");
320         }
321
322         if (!hexCodeArray[0].equals("0000")) {
323             throw new HexCodeConversionException("Illegal hex code element 0, should be 0000");
324         }
325
326         try {
327             // Use magic number to get frequency
328             frequency = Math.round(freqConversionFactor / Integer.parseInt(hexCodeArray[1], 16));
329         } catch (Exception e) {
330             throw new HexCodeConversionException("Unable to convert frequency from element 1");
331         }
332
333         try {
334             // Offset is derived from sequenceLength1
335             sequence1Length = Integer.parseInt(hexCodeArray[2], 16);
336             offset = (sequence1Length * 2) + 1;
337         } catch (Exception e) {
338             throw new HexCodeConversionException("Unable to convert offset from element 2");
339         }
340
341         // sequenceLength2 (hexCodeArray[3]) is not used
342
343         StringBuilder gcCode = new StringBuilder();
344         gcCode.append(frequency);
345         gcCode.append(",");
346         gcCode.append(repeat);
347         gcCode.append(",");
348         gcCode.append(offset);
349
350         try {
351             // The remaining fields are just converted to decimal
352             for (int i = 4; i < hexCodeArray.length; i++) {
353                 gcCode.append(",");
354                 gcCode.append(Integer.parseInt(hexCodeArray[i], 16));
355             }
356         } catch (Exception e) {
357             throw new HexCodeConversionException("Unable to convert remaining hex code string");
358         }
359
360         return gcCode.toString();
361     }
362
363     public static String getAsHexString(byte[] b) {
364         StringBuilder sb = new StringBuilder();
365
366         for (int j = 0; j < b.length; j++) {
367             String s = String.format("%02x ", b[j] & 0xff);
368             sb.append(s);
369         }
370         return sb.toString();
371     }
372
373     public String getIP() {
374         return thing.getConfiguration().get(THING_PROPERTY_IP).toString();
375     }
376
377     public String getFlexActiveCable() {
378         return thing.getConfiguration().get(THING_CONFIG_ACTIVECABLE).toString();
379     }
380
381     private String thingID() {
382         // Return segments 2 & 3 only
383         String s = thing.getUID().getAsString();
384         return s.substring(s.indexOf(':') + 1);
385     }
386
387     /*
388      * Manage the ONLINE/OFFLINE status of the thing
389      */
390     private void markThingOnline() {
391         if (!isOnline()) {
392             logger.debug("Changing status of {} from {}({}) to ONLINE", thingID(), getStatus(), getDetail());
393             updateStatus(ThingStatus.ONLINE);
394         }
395     }
396
397     private void markThingOffline() {
398         if (isOnline()) {
399             logger.debug("Changing status of {} from {}({}) to OFFLINE", thingID(), getStatus(), getDetail());
400             updateStatus(ThingStatus.OFFLINE);
401         }
402     }
403
404     private void markThingOfflineWithError(ThingStatusDetail statusDetail, String statusMessage) {
405         // If it's offline with no detail or if it's not offline, mark it offline with detailed status
406         if ((isOffline() && getDetail().equals(ThingStatusDetail.NONE)) || !isOffline()) {
407             logger.debug("Changing status of {} from {}({}) to OFFLINE({})", thingID(), getStatus(), getDetail(),
408                     statusDetail);
409             updateStatus(ThingStatus.OFFLINE, statusDetail, statusMessage);
410             return;
411         }
412     }
413
414     private boolean isOnline() {
415         return thing.getStatus().equals(ThingStatus.ONLINE);
416     }
417
418     private boolean isOffline() {
419         return thing.getStatus().equals(ThingStatus.OFFLINE);
420     }
421
422     private ThingStatus getStatus() {
423         return thing.getStatus();
424     }
425
426     private ThingStatusDetail getDetail() {
427         return thing.getStatusInfo().getStatusDetail();
428     }
429
430     /**
431      * The {@link HexCodeConversionException} class is responsible for
432      *
433      * @author Mark Hilbush - Initial contribution
434      */
435     private class HexCodeConversionException extends Exception {
436         private static final long serialVersionUID = -4422352677677729196L;
437
438         public HexCodeConversionException(String message) {
439             super(message);
440         }
441     }
442
443     /**
444      * The {@link CommandProcessor} class is responsible for handling communication with the GlobalCache
445      * device. It waits for requests to arrive on a queue. When a request arrives, it sends the command to the
446      * GlobalCache device, waits for a response from the device, parses the response, then responds to the caller by
447      * placing a message in a response queue. Device response time is typically well below 100 ms, hence the reason
448      * fgor a relatively low timeout when reading the response queue.
449      *
450      * @author Mark Hilbush - Initial contribution
451      */
452     private class CommandProcessor extends Thread {
453         private Logger logger = LoggerFactory.getLogger(CommandProcessor.class);
454
455         private boolean terminate = false;
456         private final String TERMINATE_COMMAND = "terminate";
457
458         private final int SEND_QUEUE_MAX_DEPTH = 10;
459         private final int SEND_QUEUE_TIMEOUT = 2000;
460
461         private ConnectionManager connectionManager;
462
463         public CommandProcessor() {
464             super("GlobalCache Command Processor");
465             sendQueue = new LinkedBlockingQueue<>(SEND_QUEUE_MAX_DEPTH);
466             logger.debug("Processor for thing {} created request queue, depth={}", thingID(), SEND_QUEUE_MAX_DEPTH);
467         }
468
469         public void terminate() {
470             logger.debug("Processor for thing {} is being marked ready to terminate.", thingID());
471
472             try {
473                 // Send the command processor a terminate message
474                 sendQueue.put(new RequestMessage(TERMINATE_COMMAND, null, null, null));
475             } catch (InterruptedException e) {
476                 Thread.currentThread().interrupt();
477                 terminate = true;
478             }
479         }
480
481         @Override
482         public void run() {
483             logger.debug("Command processor STARTING for thing {} at IP {}", thingID(), getIP());
484             connectionManager = new ConnectionManager();
485             connectionManager.connect();
486             connectionManager.scheduleConnectionMonitorJob();
487             sendQueue.clear();
488             terminate = false;
489
490             try {
491                 RequestMessage requestMessage;
492                 while (!terminate) {
493                     requestMessage = sendQueue.poll(SEND_QUEUE_TIMEOUT, TimeUnit.MILLISECONDS);
494                     if (requestMessage != null) {
495                         if (requestMessage.getCommandName().equals(TERMINATE_COMMAND)) {
496                             logger.debug("Processor for thing {} received terminate message", thingID());
497                             break;
498                         }
499
500                         String deviceReply;
501                         connectionManager.connect();
502                         if (connectionManager.isConnected()) {
503                             try {
504                                 long startTime = System.currentTimeMillis();
505                                 if (requestMessage.isCommand()) {
506                                     writeCommandToDevice(requestMessage);
507                                     deviceReply = readReplyFromDevice(requestMessage);
508                                 } else {
509                                     writeSerialToDevice(requestMessage);
510                                     deviceReply = "successful";
511                                 }
512                                 long endTime = System.currentTimeMillis();
513                                 logger.debug("Transaction '{}' for thing {} at {} took {} ms",
514                                         requestMessage.getCommandName(), thingID(), getIP(), endTime - startTime);
515
516                             } catch (IOException e) {
517                                 logger.error("Comm error for thing {} at {}: {}", thingID(), getIP(), e.getMessage());
518                                 deviceReply = "ERROR: " + e.getMessage();
519                                 connectionManager.setCommError(deviceReply);
520                                 connectionManager.disconnect();
521                             }
522                         } else {
523                             deviceReply = "ERROR: " + "No connection to device";
524                         }
525
526                         logger.trace("Processor for thing {} queuing response message: {}", thingID(), deviceReply);
527                         requestMessage.getReceiveQueue().put(new ResponseMessage(deviceReply));
528                     }
529                 }
530             } catch (InterruptedException e) {
531                 logger.warn("Processor for thing {} was interrupted: {}", thingID(), e.getMessage());
532                 Thread.currentThread().interrupt();
533             }
534
535             connectionManager.cancelConnectionMonitorJob();
536             connectionManager.disconnect();
537             connectionManager = null;
538             logger.debug("Command processor TERMINATING for thing {} at IP {}", thingID(), getIP());
539         }
540
541         /*
542          * Write the command to the device.
543          */
544         private void writeCommandToDevice(RequestMessage requestMessage) throws IOException {
545             logger.trace("Processor for thing {} writing command to device", thingID());
546
547             if (connectionManager.getCommandOut() == null) {
548                 logger.debug("Error writing to device because output stream object is null");
549                 return;
550             }
551
552             byte[] deviceCommand = (requestMessage.getDeviceCommand() + '\r').getBytes();
553             connectionManager.getCommandOut().write(deviceCommand);
554             connectionManager.getCommandOut().flush();
555         }
556
557         /*
558          * Read command reply from the device, then remove the CR at the end of the line.
559          */
560         private String readReplyFromDevice(RequestMessage requestMessage) throws IOException {
561             logger.trace("Processor for thing {} reading reply from device", thingID());
562
563             if (connectionManager.getCommandIn() == null) {
564                 logger.debug("Error reading from device because input stream object is null");
565                 return "ERROR: BufferedReader is null!";
566             }
567
568             logger.trace("Processor for thing {} reading response from device", thingID());
569             return connectionManager.getCommandIn().readLine().trim();
570         }
571
572         /*
573          * Write a serial command to the device
574          */
575         private void writeSerialToDevice(RequestMessage requestMessage) throws IOException {
576             DataOutputStream out = connectionManager.getSerialOut(requestMessage.getCommandType());
577             if (out == null) {
578                 logger.warn("Can't send serial command; output stream is null!");
579                 return;
580             }
581
582             byte[] deviceCommand;
583             deviceCommand = URLDecoder.decode(requestMessage.getDeviceCommand(), CHARSET).getBytes(CHARSET);
584
585             logger.debug("Writing decoded deviceCommand byte array: {}", getAsHexString(deviceCommand));
586             out.write(deviceCommand);
587         }
588     }
589
590     /*
591      * The {@link ConnectionManager} class is responsible for managing the state of the connections to the
592      * command port and the serial port(s) of the device.
593      *
594      * @author Mark Hilbush - Initial contribution
595      */
596     private class ConnectionManager {
597         private Logger logger = LoggerFactory.getLogger(ConnectionManager.class);
598
599         private DeviceConnection commandConnection;
600         private DeviceConnection serialPort1Connection;
601         private DeviceConnection serialPort2Connection;
602
603         private SerialPortReader serialReaderPort1;
604         private SerialPortReader serialReaderPort2;
605
606         private boolean deviceIsConnected;
607
608         private final String COMMAND_NAME = "command";
609         private final String SERIAL1_NAME = "serial-1";
610         private final String SERIAL2_NAME = "serial-2";
611
612         private final int COMMAND_PORT = 4998;
613         private final int SERIAL1_PORT = 4999;
614         private final int SERIAL2_PORT = 5000;
615
616         private final int SOCKET_CONNECT_TIMEOUT = 1500;
617
618         private ScheduledFuture<?> connectionMonitorJob;
619         private final int CONNECTION_MONITOR_FREQUENCY = 60;
620         private final int CONNECTION_MONITOR_START_DELAY = 15;
621
622         private Runnable connectionMonitorRunnable = () -> {
623             logger.trace("Performing connection check for thing {} at IP {}", thingID(), commandConnection.getIP());
624             checkConnection();
625         };
626
627         public ConnectionManager() {
628             commandConnection = new DeviceConnection(COMMAND_NAME, COMMAND_PORT);
629             serialPort1Connection = new DeviceConnection(SERIAL1_NAME, SERIAL1_PORT);
630             serialPort2Connection = new DeviceConnection(SERIAL2_NAME, SERIAL2_PORT);
631
632             commandConnection.setIP(getIPAddress());
633             serialPort1Connection.setIP(getIPAddress());
634             serialPort2Connection.setIP(getIPAddress());
635
636             deviceIsConnected = false;
637         }
638
639         private String getIPAddress() {
640             String ipAddress = ((GlobalCacheHandler) thing.getHandler()).getIP();
641             if (StringUtils.isEmpty(ipAddress)) {
642                 logger.debug("Handler for thing {} could not get IP address from config", thingID());
643                 markThingOfflineWithError(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "IP address not set");
644             }
645             return ipAddress;
646         }
647
648         /*
649          * Connect to the command and serial port(s) on the device. The serial connections are established only for
650          * devices that support serial.
651          */
652         protected void connect() {
653             if (isConnected()) {
654                 return;
655             }
656
657             // Get a connection to the command port
658             if (!commandConnect(commandConnection)) {
659                 return;
660             }
661
662             // Get a connection to serial port 1
663             if (deviceSupportsSerialPort1()) {
664                 if (!serialConnect(serialPort1Connection)) {
665                     commandDisconnect(commandConnection);
666                     return;
667                 }
668             }
669
670             // Get a connection to serial port 2
671             if (deviceSupportsSerialPort2()) {
672                 if (!serialConnect(serialPort2Connection)) {
673                     commandDisconnect(commandConnection);
674                     serialDisconnect(serialPort1Connection);
675                     return;
676                 }
677             }
678
679             /*
680              * All connections opened successfully, so we can mark the thing online
681              * and start the serial port readers
682              */
683             markThingOnline();
684             deviceIsConnected = true;
685             startSerialPortReaders();
686         }
687
688         private boolean commandConnect(DeviceConnection conn) {
689             logger.debug("Connecting to {} port for thing {} at IP {}", conn.getName(), thingID(), conn.getIP());
690             if (!openSocket(conn)) {
691                 return false;
692             }
693             // create streams
694             try {
695                 conn.setCommandIn(new BufferedReader(new InputStreamReader(conn.getSocket().getInputStream())));
696                 conn.setCommandOut(new DataOutputStream(conn.getSocket().getOutputStream()));
697             } catch (IOException e) {
698                 logger.debug("Error getting streams to {} port for thing {} at {}, exception={}", conn.getName(),
699                         thingID(), conn.getIP(), e.getMessage());
700                 markThingOfflineWithError(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
701                 closeSocket(conn);
702                 return false;
703             }
704             logger.info("Got a connection to {} port for thing {} at {}", conn.getName(), thingID(), conn.getIP());
705
706             return true;
707         }
708
709         private boolean serialConnect(DeviceConnection conn) {
710             logger.debug("Connecting to {} port for thing {} at {}", conn.getName(), thingID(), conn.getIP());
711             if (!openSocket(conn)) {
712                 return false;
713             }
714             // create streams
715             try {
716                 conn.setSerialIn(new BufferedInputStream(conn.getSocket().getInputStream()));
717                 conn.setSerialOut(new DataOutputStream(conn.getSocket().getOutputStream()));
718             } catch (IOException e) {
719                 logger.debug("Failed to get streams on {} port for thing {} at {}", conn.getName(), thingID(),
720                         conn.getIP());
721                 closeSocket(conn);
722                 return false;
723             }
724             logger.info("Got a connection to {} port for thing {} at {}", conn.getName(), thingID(), conn.getIP());
725
726             return true;
727         }
728
729         private boolean openSocket(DeviceConnection conn) {
730             try {
731                 conn.setSocket(new Socket());
732                 conn.getSocket().bind(new InetSocketAddress(ifAddress, 0));
733                 conn.getSocket().connect(new InetSocketAddress(conn.getIP(), conn.getPort()), SOCKET_CONNECT_TIMEOUT);
734             } catch (IOException e) {
735                 logger.debug("Failed to get socket on {} port for thing {} at {}", conn.getName(), thingID(),
736                         conn.getIP());
737                 return false;
738             }
739             return true;
740         }
741
742         private void closeSocket(DeviceConnection conn) {
743             if (conn.getSocket() != null) {
744                 try {
745                     conn.getSocket().close();
746                 } catch (IOException e) {
747                     logger.debug("Failed to close socket on {} port for thing {} at {}", conn.getName(), thingID(),
748                             conn.getIP());
749                 }
750             }
751         }
752
753         /*
754          * Disconnect from the command and serial port(s) on the device. Only disconnect the serial port
755          * connections if the devices have serial ports.
756          */
757         protected void disconnect() {
758             if (!isConnected()) {
759                 return;
760             }
761             commandDisconnect(commandConnection);
762
763             stopSerialPortReaders();
764             if (deviceSupportsSerialPort1()) {
765                 serialDisconnect(serialPort1Connection);
766             }
767             if (deviceSupportsSerialPort2()) {
768                 serialDisconnect(serialPort2Connection);
769             }
770
771             markThingOffline();
772             deviceIsConnected = false;
773         }
774
775         private void commandDisconnect(DeviceConnection conn) {
776             deviceDisconnect(conn);
777         }
778
779         private void serialDisconnect(DeviceConnection conn) {
780             deviceDisconnect(conn);
781         }
782
783         private void deviceDisconnect(DeviceConnection conn) {
784             logger.debug("Disconnecting from {} port for thing {} at IP {}", conn.getName(), thingID(), conn.getIP());
785
786             try {
787                 if (conn.getSerialOut() != null) {
788                     conn.getSerialOut().close();
789                 }
790                 if (conn.getSerialIn() != null) {
791                     conn.getSerialIn().close();
792                 }
793                 if (conn.getSocket() != null) {
794                     conn.getSocket().close();
795                 }
796             } catch (IOException e) {
797                 logger.debug("Error closing {} port for thing {} at IP {}: exception={}", conn.getName(), thingID(),
798                         conn.getIP(), e.getMessage());
799             }
800             conn.reset();
801         }
802
803         private boolean isConnected() {
804             return deviceIsConnected;
805         }
806
807         public void setCommError(String errorMessage) {
808             markThingOfflineWithError(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, errorMessage);
809         }
810
811         /*
812          * Retrieve the input/output streams for command and serial connections.
813          */
814         protected BufferedReader getCommandIn() {
815             return commandConnection.getCommandIn();
816         }
817
818         protected DataOutputStream getCommandOut() {
819             return commandConnection.getCommandOut();
820         }
821
822         protected BufferedInputStream getSerialIn(CommandType commandType) {
823             if (commandType != CommandType.SERIAL1 && commandType != CommandType.SERIAL2) {
824                 return null;
825             }
826             if (commandType == CommandType.SERIAL1) {
827                 return serialPort1Connection.getSerialIn();
828             } else {
829                 return serialPort2Connection.getSerialIn();
830             }
831         }
832
833         protected DataOutputStream getSerialOut(CommandType commandType) {
834             if (commandType != CommandType.SERIAL1 && commandType != CommandType.SERIAL2) {
835                 return null;
836             }
837             if (commandType == CommandType.SERIAL1) {
838                 return serialPort1Connection.getSerialOut();
839             } else {
840                 return serialPort2Connection.getSerialOut();
841             }
842         }
843
844         private boolean deviceSupportsSerialPort1() {
845             ThingTypeUID typeUID = thing.getThingTypeUID();
846
847             if (typeUID.equals(THING_TYPE_ITACH_SL)) {
848                 return true;
849             } else if (typeUID.equals(THING_TYPE_GC_100_06) || typeUID.equals(THING_TYPE_GC_100_12)) {
850                 return true;
851             } else if (typeUID.equals(THING_TYPE_ITACH_FLEX) && getFlexActiveCable().equals(ACTIVE_CABLE_SERIAL)) {
852                 return true;
853             }
854             return false;
855         }
856
857         private boolean deviceSupportsSerialPort2() {
858             if (thing.getThingTypeUID().equals(THING_TYPE_GC_100_12)) {
859                 return true;
860             }
861             return false;
862         }
863
864         /*
865          * Periodically validate the command connection to the device by executing a getversion command.
866          */
867         private void scheduleConnectionMonitorJob() {
868             logger.debug("Starting connection monitor job for thing {} at IP {}", thingID(), commandConnection.getIP());
869             connectionMonitorJob = scheduler.scheduleWithFixedDelay(connectionMonitorRunnable,
870                     CONNECTION_MONITOR_START_DELAY, CONNECTION_MONITOR_FREQUENCY, TimeUnit.SECONDS);
871         }
872
873         private void cancelConnectionMonitorJob() {
874             if (connectionMonitorJob != null) {
875                 logger.debug("Canceling connection monitor job for thing {} at IP {}", thingID(),
876                         commandConnection.getIP());
877                 connectionMonitorJob.cancel(true);
878                 connectionMonitorJob = null;
879             }
880         }
881
882         private void checkConnection() {
883             CommandGetversion getversion = new CommandGetversion(thing, sendQueue);
884             getversion.executeQuiet();
885
886             if (getversion.isSuccessful()) {
887                 logger.trace("Connection check successful for thing {} at IP {}", thingID(), commandConnection.getIP());
888                 markThingOnline();
889                 deviceIsConnected = true;
890             } else {
891                 logger.debug("Connection check failed for thing {} at IP {}", thingID(), commandConnection.getIP());
892                 disconnect();
893             }
894         }
895
896         private void startSerialPortReaders() {
897             if (deviceSupportsSerialPort1()) {
898                 serialReaderPort1 = startSerialPortReader(CommandType.SERIAL1, CONFIG_ENABLE_TWO_WAY_PORT_1,
899                         CONFIG_END_OF_MESSAGE_DELIMITER_PORT_1);
900             }
901             if (deviceSupportsSerialPort2()) {
902                 serialReaderPort2 = startSerialPortReader(CommandType.SERIAL2, CONFIG_ENABLE_TWO_WAY_PORT_2,
903                         CONFIG_END_OF_MESSAGE_DELIMITER_PORT_2);
904             }
905         }
906
907         private SerialPortReader startSerialPortReader(CommandType serialDevice, String enableTwoWayConfig,
908                 String endOfMessageDelimiterConfig) {
909             Boolean enableTwoWay = (Boolean) thing.getConfiguration().get(enableTwoWayConfig);
910             logger.debug("Enable two-way is {} for thing {} {}", enableTwoWay, thingID(), serialDevice);
911
912             if (Boolean.TRUE.equals(enableTwoWay)) {
913                 // Get the end of message delimiter from the config, URL decode it, and convert it to a byte array
914                 String endOfMessageString = (String) thing.getConfiguration().get(endOfMessageDelimiterConfig);
915                 if (StringUtils.isNotEmpty(endOfMessageString)) {
916                     logger.debug("End of message is {} for thing {} {}", endOfMessageString, thingID(), serialDevice);
917                     byte[] endOfMessage;
918                     try {
919                         endOfMessage = URLDecoder.decode(endOfMessageString, CHARSET).getBytes(CHARSET);
920                     } catch (UnsupportedEncodingException e) {
921                         logger.info("Unable to decode end of message delimiter {} for thing {} {}", endOfMessageString,
922                                 thingID(), serialDevice);
923                         return null;
924                     }
925
926                     // Start the serial reader using the above end-of-message delimiter
927                     SerialPortReader serialPortReader = new SerialPortReader(serialDevice, getSerialIn(serialDevice),
928                             endOfMessage);
929                     serialPortReader.start();
930                     return serialPortReader;
931                 } else {
932                     logger.warn("End of message delimiter is not defined in configuration of thing {}", thingID());
933                 }
934             }
935             return null;
936         }
937
938         private void stopSerialPortReaders() {
939             if (deviceSupportsSerialPort1() && serialReaderPort1 != null) {
940                 logger.debug("Stopping serial port 1 reader for thing {} at IP {}", thingID(),
941                         commandConnection.getIP());
942                 serialReaderPort1.stop();
943                 serialReaderPort1 = null;
944             }
945             if (deviceSupportsSerialPort2() && serialReaderPort2 != null) {
946                 logger.debug("Stopping serial port 2 reader for thing {} at IP {}", thingID(),
947                         commandConnection.getIP());
948                 serialReaderPort2.stop();
949                 serialReaderPort2 = null;
950             }
951         }
952     }
953
954     /*
955      * The {@link SerialReader} class reads data from the serial connection. When data is
956      * received, the receive channel is updated with the data. Data is read up to the
957      * end-of-message delimiter defined in the Thing configuration.
958      *
959      * @author Mark Hilbush - Initial contribution
960      */
961     private class SerialPortReader {
962         private Logger logger = LoggerFactory.getLogger(SerialPortReader.class);
963
964         private CommandType serialPort;
965         private BufferedInputStream serialPortIn;
966         private ScheduledFuture<?> serialPortReaderJob;
967         private boolean terminateSerialPortReader;
968
969         private byte[] endOfMessage;
970
971         SerialPortReader(CommandType serialPort, BufferedInputStream serialIn, byte[] endOfMessage) {
972             if (serialIn == null) {
973                 throw new IllegalArgumentException("Serial input stream is not set");
974             }
975             this.serialPort = serialPort;
976             this.serialPortIn = serialIn;
977             this.endOfMessage = endOfMessage;
978             serialPortReaderJob = null;
979             terminateSerialPortReader = false;
980         }
981
982         public void start() {
983             serialPortReaderJob = scheduledExecutorService.schedule(this::serialPortReader, 0, TimeUnit.SECONDS);
984         }
985
986         public void stop() {
987             if (serialPortReaderJob != null) {
988                 terminateSerialPortReader = true;
989                 serialPortReaderJob.cancel(true);
990                 serialPortReaderJob = null;
991             }
992         }
993
994         private void serialPortReader() {
995             logger.info("Serial reader RUNNING for {} on {}:{}", thingID(), getIP(), serialPort);
996
997             while (!terminateSerialPortReader) {
998                 byte[] buffer;
999                 try {
1000                     buffer = readUntilEndOfMessage(endOfMessage);
1001                     if (buffer == null) {
1002                         logger.debug("Received end-of-stream from {} on {}", getIP(), serialPort);
1003                         continue;
1004                     }
1005                     logger.debug("Rcv data from {} at {}:{}: {}", thingID(), getIP(), serialPort,
1006                             getAsHexString(buffer));
1007                     updateFeedbackChannel(buffer);
1008                 } catch (UnsupportedEncodingException e) {
1009                     logger.info("Unsupported encoding exception: {}", e.getMessage(), e);
1010                     continue;
1011                 } catch (IOException e) {
1012                     logger.debug("Serial Reader got IOException: {}", e.getMessage());
1013                     break;
1014                 } catch (InterruptedException e) {
1015                     logger.debug("Serial Reader got InterruptedException: {}", e.getMessage());
1016                     break;
1017                 }
1018             }
1019             logger.debug("Serial reader STOPPING for {} on {}:{}", thingID(), getIP(), serialPort);
1020         }
1021
1022         private byte[] readUntilEndOfMessage(byte[] endOfMessageDelimiter) throws IOException, InterruptedException {
1023             logger.debug("Serial reader waiting for available data");
1024
1025             int val;
1026             ByteArrayOutputStream buf = new ByteArrayOutputStream();
1027
1028             // Read from the serial input stream until the endOfMessage delimiter is found
1029             while (true) {
1030                 val = serialPortIn.read();
1031                 if (val == -1) {
1032                     logger.debug("Serial reader got unexpected end of input stream");
1033                     throw new IOException("Unexpected end of stream");
1034                 }
1035
1036                 buf.write(val);
1037                 if (findEndOfMessage(buf.toByteArray(), endOfMessageDelimiter)) {
1038                     // Found the end-of-message delimiter in the serial input stream
1039                     break;
1040                 }
1041             }
1042             logger.debug("Serial reader returning a message");
1043             return buf.toByteArray();
1044         }
1045
1046         private boolean findEndOfMessage(byte[] buf, byte[] endOfMessage) {
1047             int lengthEOM = endOfMessage.length;
1048             int lengthBuf = buf.length;
1049
1050             // Look for the end-of-message delimiter at the end of the buffer
1051             while (lengthEOM > 0) {
1052                 lengthEOM--;
1053                 lengthBuf--;
1054                 if (lengthBuf < 0 || endOfMessage[lengthEOM] != buf[lengthBuf]) {
1055                     // No match on end of message
1056                     return false;
1057                 }
1058             }
1059             logger.debug("Serial reader found the end-of-message delimiter in the input buffer");
1060             return true;
1061         }
1062
1063         private void updateFeedbackChannel(byte[] buffer) {
1064             String channelId;
1065             if (serialPort.equals(CommandType.SERIAL1)) {
1066                 channelId = CHANNEL_SL_M1_RECEIVE;
1067             } else if (serialPort.equals(CommandType.SERIAL2)) {
1068                 channelId = CHANNEL_SL_M2_RECEIVE;
1069             } else {
1070                 logger.warn("Unknown serial port; can't update feedback channel: {}", serialPort);
1071                 return;
1072             }
1073             Channel channel = getThing().getChannel(channelId);
1074             if (channel != null && isLinked(channelId)) {
1075                 logger.debug("Updating feedback channel for port {}", serialPort);
1076                 try {
1077                     String encodedReply = URLEncoder.encode(new String(buffer, CHARSET), CHARSET);
1078                     logger.debug("encodedReply='{}'", encodedReply);
1079                     updateState(channel.getUID(), new StringType(encodedReply));
1080                 } catch (UnsupportedEncodingException e) {
1081                     logger.warn("Exception while encoding data read from serial device: {}", e.getMessage());
1082                 }
1083             }
1084         }
1085     }
1086
1087     /*
1088      * The {@link DeviceConnection} class stores information about the connection to a globalcache device.
1089      * There can be two types of connections, command and serial. The command connection is used to
1090      * send all but the serial strings to the device. The serial connection is used exclusively to
1091      * send serial messages. These serial connections are applicable only to iTach SL and GC-100 devices.
1092      *
1093      * @author Mark Hilbush - Initial contribution
1094      */
1095     private class DeviceConnection {
1096         private String connectionName;
1097         private int port;
1098         private String ipAddress;
1099         private Socket socket;
1100         private BufferedReader commandIn;
1101         private DataOutputStream commandOut;
1102         private BufferedInputStream serialIn;
1103         private DataOutputStream serialOut;
1104
1105         DeviceConnection(String connectionName, int port) {
1106             setName(connectionName);
1107             setPort(port);
1108             setIP(null);
1109             setSocket(null);
1110             setCommandIn(null);
1111             setCommandOut(null);
1112             setSerialIn(null);
1113             setSerialOut(null);
1114         }
1115
1116         public void reset() {
1117             setSocket(null);
1118             setCommandIn(null);
1119             setCommandOut(null);
1120             setSerialIn(null);
1121             setSerialOut(null);
1122         }
1123
1124         public String getName() {
1125             return connectionName;
1126         }
1127
1128         public void setName(String connectionName) {
1129             this.connectionName = connectionName;
1130         }
1131
1132         public int getPort() {
1133             return port;
1134         }
1135
1136         public void setPort(int port) {
1137             this.port = port;
1138         }
1139
1140         public String getIP() {
1141             return ipAddress;
1142         }
1143
1144         public void setIP(String ipAddress) {
1145             this.ipAddress = ipAddress;
1146         }
1147
1148         public Socket getSocket() {
1149             return socket;
1150         }
1151
1152         public void setSocket(Socket socket) {
1153             this.socket = socket;
1154         }
1155
1156         public BufferedReader getCommandIn() {
1157             return commandIn;
1158         }
1159
1160         public void setCommandIn(BufferedReader commandIn) {
1161             this.commandIn = commandIn;
1162         }
1163
1164         public DataOutputStream getCommandOut() {
1165             return commandOut;
1166         }
1167
1168         public void setCommandOut(DataOutputStream commandOut) {
1169             this.commandOut = commandOut;
1170         }
1171
1172         public BufferedInputStream getSerialIn() {
1173             return serialIn;
1174         }
1175
1176         public void setSerialIn(BufferedInputStream serialIn) {
1177             this.serialIn = serialIn;
1178         }
1179
1180         public DataOutputStream getSerialOut() {
1181             return serialOut;
1182         }
1183
1184         public void setSerialOut(DataOutputStream serialOut) {
1185             this.serialOut = serialOut;
1186         }
1187     }
1188 }