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