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