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