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