]> git.basschouten.com Git - openhab-addons.git/blob
c95577db53aec604fc4cd1edc9f3a6c5569ecd08
[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 final String TERMINATE_COMMAND = "terminate";
447
448         private final int SEND_QUEUE_MAX_DEPTH = 10;
449         private 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 final String COMMAND_NAME = "command";
598         private final String SERIAL1_NAME = "serial-1";
599         private final String SERIAL2_NAME = "serial-2";
600
601         private final int COMMAND_PORT = 4998;
602         private final int SERIAL1_PORT = 4999;
603         private final int SERIAL2_PORT = 5000;
604
605         private final int SOCKET_CONNECT_TIMEOUT = 1500;
606
607         private ScheduledFuture<?> connectionMonitorJob;
608         private final int CONNECTION_MONITOR_FREQUENCY = 60;
609         private 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             if (thing.getThingTypeUID().equals(THING_TYPE_GC_100_12)) {
848                 return true;
849             }
850             return false;
851         }
852
853         /*
854          * Periodically validate the command connection to the device by executing a getversion command.
855          */
856         private void scheduleConnectionMonitorJob() {
857             logger.debug("Starting connection monitor job for thing {} at IP {}", thingID(), commandConnection.getIP());
858             connectionMonitorJob = scheduler.scheduleWithFixedDelay(connectionMonitorRunnable,
859                     CONNECTION_MONITOR_START_DELAY, CONNECTION_MONITOR_FREQUENCY, TimeUnit.SECONDS);
860         }
861
862         private void cancelConnectionMonitorJob() {
863             if (connectionMonitorJob != null) {
864                 logger.debug("Canceling connection monitor job for thing {} at IP {}", thingID(),
865                         commandConnection.getIP());
866                 connectionMonitorJob.cancel(true);
867                 connectionMonitorJob = null;
868             }
869         }
870
871         private void checkConnection() {
872             CommandGetversion getversion = new CommandGetversion(thing, sendQueue);
873             getversion.executeQuiet();
874
875             if (getversion.isSuccessful()) {
876                 logger.trace("Connection check successful for thing {} at IP {}", thingID(), commandConnection.getIP());
877                 markThingOnline();
878                 deviceIsConnected = true;
879             } else {
880                 logger.debug("Connection check failed for thing {} at IP {}", thingID(), commandConnection.getIP());
881                 disconnect();
882             }
883         }
884
885         private void startSerialPortReaders() {
886             if (deviceSupportsSerialPort1()) {
887                 serialReaderPort1 = startSerialPortReader(CommandType.SERIAL1, CONFIG_ENABLE_TWO_WAY_PORT_1,
888                         CONFIG_END_OF_MESSAGE_DELIMITER_PORT_1);
889             }
890             if (deviceSupportsSerialPort2()) {
891                 serialReaderPort2 = startSerialPortReader(CommandType.SERIAL2, CONFIG_ENABLE_TWO_WAY_PORT_2,
892                         CONFIG_END_OF_MESSAGE_DELIMITER_PORT_2);
893             }
894         }
895
896         private SerialPortReader startSerialPortReader(CommandType serialDevice, String enableTwoWayConfig,
897                 String endOfMessageDelimiterConfig) {
898             Boolean enableTwoWay = (Boolean) thing.getConfiguration().get(enableTwoWayConfig);
899             logger.debug("Enable two-way is {} for thing {} {}", enableTwoWay, thingID(), serialDevice);
900
901             if (Boolean.TRUE.equals(enableTwoWay)) {
902                 // Get the end of message delimiter from the config, URL decode it, and convert it to a byte array
903                 String endOfMessageString = (String) thing.getConfiguration().get(endOfMessageDelimiterConfig);
904                 if (endOfMessageString != null && !endOfMessageString.isEmpty()) {
905                     logger.debug("End of message is {} for thing {} {}", endOfMessageString, thingID(), serialDevice);
906                     byte[] endOfMessage = URLDecoder.decode(endOfMessageString, StandardCharsets.ISO_8859_1)
907                             .getBytes(StandardCharsets.ISO_8859_1);
908
909                     // Start the serial reader using the above end-of-message delimiter
910                     SerialPortReader serialPortReader = new SerialPortReader(serialDevice, getSerialIn(serialDevice),
911                             endOfMessage);
912                     serialPortReader.start();
913                     return serialPortReader;
914                 } else {
915                     logger.warn("End of message delimiter is not defined in configuration of thing {}", thingID());
916                 }
917             }
918             return null;
919         }
920
921         private void stopSerialPortReaders() {
922             if (deviceSupportsSerialPort1() && serialReaderPort1 != null) {
923                 logger.debug("Stopping serial port 1 reader for thing {} at IP {}", thingID(),
924                         commandConnection.getIP());
925                 serialReaderPort1.stop();
926                 serialReaderPort1 = null;
927             }
928             if (deviceSupportsSerialPort2() && serialReaderPort2 != null) {
929                 logger.debug("Stopping serial port 2 reader for thing {} at IP {}", thingID(),
930                         commandConnection.getIP());
931                 serialReaderPort2.stop();
932                 serialReaderPort2 = null;
933             }
934         }
935     }
936
937     /*
938      * The {@link SerialReader} class reads data from the serial connection. When data is
939      * received, the receive channel is updated with the data. Data is read up to the
940      * end-of-message delimiter defined in the Thing configuration.
941      *
942      * @author Mark Hilbush - Initial contribution
943      */
944     private class SerialPortReader {
945         private Logger logger = LoggerFactory.getLogger(SerialPortReader.class);
946
947         private CommandType serialPort;
948         private BufferedInputStream serialPortIn;
949         private ScheduledFuture<?> serialPortReaderJob;
950         private boolean terminateSerialPortReader;
951
952         private byte[] endOfMessage;
953
954         SerialPortReader(CommandType serialPort, BufferedInputStream serialIn, byte[] endOfMessage) {
955             if (serialIn == null) {
956                 throw new IllegalArgumentException("Serial input stream is not set");
957             }
958             this.serialPort = serialPort;
959             this.serialPortIn = serialIn;
960             this.endOfMessage = endOfMessage;
961             serialPortReaderJob = null;
962             terminateSerialPortReader = false;
963         }
964
965         public void start() {
966             serialPortReaderJob = scheduledExecutorService.schedule(this::serialPortReader, 0, TimeUnit.SECONDS);
967         }
968
969         public void stop() {
970             if (serialPortReaderJob != null) {
971                 terminateSerialPortReader = true;
972                 serialPortReaderJob.cancel(true);
973                 serialPortReaderJob = null;
974             }
975         }
976
977         private void serialPortReader() {
978             logger.info("Serial reader RUNNING for {} on {}:{}", thingID(), getIP(), serialPort);
979
980             while (!terminateSerialPortReader) {
981                 byte[] buffer;
982                 try {
983                     buffer = readUntilEndOfMessage(endOfMessage);
984                     if (buffer == null) {
985                         logger.debug("Received end-of-stream from {} on {}", getIP(), serialPort);
986                         continue;
987                     }
988                     logger.debug("Rcv data from {} at {}:{}: {}", thingID(), getIP(), serialPort,
989                             getAsHexString(buffer));
990                     updateFeedbackChannel(buffer);
991                 } catch (IOException e) {
992                     logger.debug("Serial Reader got IOException: {}", e.getMessage());
993                     break;
994                 } catch (InterruptedException e) {
995                     logger.debug("Serial Reader got InterruptedException: {}", e.getMessage());
996                     break;
997                 }
998             }
999             logger.debug("Serial reader STOPPING for {} on {}:{}", thingID(), getIP(), serialPort);
1000         }
1001
1002         private byte[] readUntilEndOfMessage(byte[] endOfMessageDelimiter) throws IOException, InterruptedException {
1003             logger.debug("Serial reader waiting for available data");
1004
1005             int val;
1006             ByteArrayOutputStream buf = new ByteArrayOutputStream();
1007
1008             // Read from the serial input stream until the endOfMessage delimiter is found
1009             while (true) {
1010                 val = serialPortIn.read();
1011                 if (val == -1) {
1012                     logger.debug("Serial reader got unexpected end of input stream");
1013                     throw new IOException("Unexpected end of stream");
1014                 }
1015
1016                 buf.write(val);
1017                 if (findEndOfMessage(buf.toByteArray(), endOfMessageDelimiter)) {
1018                     // Found the end-of-message delimiter in the serial input stream
1019                     break;
1020                 }
1021             }
1022             logger.debug("Serial reader returning a message");
1023             return buf.toByteArray();
1024         }
1025
1026         private boolean findEndOfMessage(byte[] buf, byte[] endOfMessage) {
1027             int lengthEOM = endOfMessage.length;
1028             int lengthBuf = buf.length;
1029
1030             // Look for the end-of-message delimiter at the end of the buffer
1031             while (lengthEOM > 0) {
1032                 lengthEOM--;
1033                 lengthBuf--;
1034                 if (lengthBuf < 0 || endOfMessage[lengthEOM] != buf[lengthBuf]) {
1035                     // No match on end of message
1036                     return false;
1037                 }
1038             }
1039             logger.debug("Serial reader found the end-of-message delimiter in the input buffer");
1040             return true;
1041         }
1042
1043         private void updateFeedbackChannel(byte[] buffer) {
1044             String channelId;
1045             if (serialPort.equals(CommandType.SERIAL1)) {
1046                 channelId = CHANNEL_SL_M1_RECEIVE;
1047             } else if (serialPort.equals(CommandType.SERIAL2)) {
1048                 channelId = CHANNEL_SL_M2_RECEIVE;
1049             } else {
1050                 logger.warn("Unknown serial port; can't update feedback channel: {}", serialPort);
1051                 return;
1052             }
1053             Channel channel = getThing().getChannel(channelId);
1054             if (channel != null && isLinked(channelId)) {
1055                 logger.debug("Updating feedback channel for port {}", serialPort);
1056                 String encodedReply = URLEncoder.encode(new String(buffer, StandardCharsets.ISO_8859_1),
1057                         StandardCharsets.ISO_8859_1);
1058                 logger.debug("encodedReply='{}'", encodedReply);
1059                 updateState(channel.getUID(), new StringType(encodedReply));
1060             }
1061         }
1062     }
1063
1064     /*
1065      * The {@link DeviceConnection} class stores information about the connection to a globalcache device.
1066      * There can be two types of connections, command and serial. The command connection is used to
1067      * send all but the serial strings to the device. The serial connection is used exclusively to
1068      * send serial messages. These serial connections are applicable only to iTach SL and GC-100 devices.
1069      *
1070      * @author Mark Hilbush - Initial contribution
1071      */
1072     private class DeviceConnection {
1073         private String connectionName;
1074         private int port;
1075         private String ipAddress;
1076         private Socket socket;
1077         private BufferedReader commandIn;
1078         private DataOutputStream commandOut;
1079         private BufferedInputStream serialIn;
1080         private DataOutputStream serialOut;
1081
1082         DeviceConnection(String connectionName, int port) {
1083             setName(connectionName);
1084             setPort(port);
1085             setIP(null);
1086             setSocket(null);
1087             setCommandIn(null);
1088             setCommandOut(null);
1089             setSerialIn(null);
1090             setSerialOut(null);
1091         }
1092
1093         public void reset() {
1094             setSocket(null);
1095             setCommandIn(null);
1096             setCommandOut(null);
1097             setSerialIn(null);
1098             setSerialOut(null);
1099         }
1100
1101         public String getName() {
1102             return connectionName;
1103         }
1104
1105         public void setName(String connectionName) {
1106             this.connectionName = connectionName;
1107         }
1108
1109         public int getPort() {
1110             return port;
1111         }
1112
1113         public void setPort(int port) {
1114             this.port = port;
1115         }
1116
1117         public String getIP() {
1118             return ipAddress;
1119         }
1120
1121         public void setIP(String ipAddress) {
1122             this.ipAddress = ipAddress;
1123         }
1124
1125         public Socket getSocket() {
1126             return socket;
1127         }
1128
1129         public void setSocket(Socket socket) {
1130             this.socket = socket;
1131         }
1132
1133         public BufferedReader getCommandIn() {
1134             return commandIn;
1135         }
1136
1137         public void setCommandIn(BufferedReader commandIn) {
1138             this.commandIn = commandIn;
1139         }
1140
1141         public DataOutputStream getCommandOut() {
1142             return commandOut;
1143         }
1144
1145         public void setCommandOut(DataOutputStream commandOut) {
1146             this.commandOut = commandOut;
1147         }
1148
1149         public BufferedInputStream getSerialIn() {
1150             return serialIn;
1151         }
1152
1153         public void setSerialIn(BufferedInputStream serialIn) {
1154             this.serialIn = serialIn;
1155         }
1156
1157         public DataOutputStream getSerialOut() {
1158             return serialOut;
1159         }
1160
1161         public void setSerialOut(DataOutputStream serialOut) {
1162             this.serialOut = serialOut;
1163         }
1164     }
1165 }