2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.globalcache.internal.handler;
15 import static org.openhab.binding.globalcache.internal.GlobalCacheBindingConstants.*;
17 import java.io.BufferedInputStream;
18 import java.io.BufferedReader;
19 import java.io.ByteArrayOutputStream;
20 import java.io.DataOutputStream;
21 import java.io.IOException;
22 import java.io.InputStreamReader;
23 import java.io.UnsupportedEncodingException;
24 import java.net.InetAddress;
25 import java.net.InetSocketAddress;
26 import java.net.NetworkInterface;
27 import java.net.Socket;
28 import java.net.SocketException;
29 import java.net.URLDecoder;
30 import java.net.URLEncoder;
31 import java.net.UnknownHostException;
32 import java.util.concurrent.LinkedBlockingQueue;
33 import java.util.concurrent.ScheduledExecutorService;
34 import java.util.concurrent.ScheduledFuture;
35 import java.util.concurrent.TimeUnit;
36 import java.util.concurrent.atomic.AtomicInteger;
37 import java.util.regex.Pattern;
39 import org.apache.commons.lang.StringUtils;
40 import org.eclipse.jdt.annotation.NonNull;
41 import org.openhab.binding.globalcache.internal.GlobalCacheBindingConstants.CommandType;
42 import org.openhab.binding.globalcache.internal.command.CommandGetstate;
43 import org.openhab.binding.globalcache.internal.command.CommandGetversion;
44 import org.openhab.binding.globalcache.internal.command.CommandSendir;
45 import org.openhab.binding.globalcache.internal.command.CommandSendserial;
46 import org.openhab.binding.globalcache.internal.command.CommandSetstate;
47 import org.openhab.binding.globalcache.internal.command.RequestMessage;
48 import org.openhab.binding.globalcache.internal.command.ResponseMessage;
49 import org.openhab.core.common.ThreadPoolManager;
50 import org.openhab.core.library.types.OnOffType;
51 import org.openhab.core.library.types.StringType;
52 import org.openhab.core.thing.Channel;
53 import org.openhab.core.thing.ChannelUID;
54 import org.openhab.core.thing.Thing;
55 import org.openhab.core.thing.ThingStatus;
56 import org.openhab.core.thing.ThingStatusDetail;
57 import org.openhab.core.thing.ThingTypeUID;
58 import org.openhab.core.thing.binding.BaseThingHandler;
59 import org.openhab.core.transform.TransformationException;
60 import org.openhab.core.transform.TransformationHelper;
61 import org.openhab.core.transform.TransformationService;
62 import org.openhab.core.types.Command;
63 import org.openhab.core.types.RefreshType;
64 import org.osgi.framework.BundleContext;
65 import org.osgi.framework.FrameworkUtil;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
70 * The {@link GlobalCacheHandler} is responsible for handling commands, which are
71 * sent to one of the channels.
73 * @author Mark Hilbush - Initial contribution
75 public class GlobalCacheHandler extends BaseThingHandler {
76 private Logger logger = LoggerFactory.getLogger(GlobalCacheHandler.class);
78 private final BundleContext bundleContext;
80 private static final String GLOBALCACHE_THREAD_POOL = "globalCacheHandler";
82 private InetAddress ifAddress;
83 private CommandProcessor commandProcessor;
84 private ScheduledExecutorService scheduledExecutorService = ThreadPoolManager
85 .getScheduledPool(GLOBALCACHE_THREAD_POOL + "-" + thingID());
86 private ScheduledFuture<?> scheduledFuture;
88 private LinkedBlockingQueue<RequestMessage> sendQueue = null;
90 private String ipv4Address;
92 // IR transaction counter
93 private AtomicInteger irCounter;
95 // Character set to use for URL encoding & decoding
96 private String CHARSET = "ISO-8859-1";
98 public GlobalCacheHandler(@NonNull Thing gcDevice, String ipv4Address) {
100 irCounter = new AtomicInteger(1);
101 commandProcessor = new CommandProcessor();
102 scheduledFuture = null;
103 this.ipv4Address = ipv4Address;
104 this.bundleContext = FrameworkUtil.getBundle(GlobalCacheHandler.class).getBundleContext();
108 public void initialize() {
109 logger.debug("Initializing thing {}", thingID());
111 ifAddress = InetAddress.getByName(ipv4Address);
112 logger.debug("Handler using address {} on network interface {}", ifAddress.getHostAddress(),
113 NetworkInterface.getByInetAddress(ifAddress).getName());
114 } catch (SocketException e) {
115 logger.error("Handler got Socket exception creating multicast socket: {}", e.getMessage());
116 markThingOfflineWithError(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No suitable network interface");
118 } catch (UnknownHostException e) {
119 logger.error("Handler got UnknownHostException getting local IPv4 network interface: {}", e.getMessage());
120 markThingOfflineWithError(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No suitable network interface");
123 scheduledFuture = scheduledExecutorService.schedule(commandProcessor, 2, TimeUnit.SECONDS);
127 public void dispose() {
128 logger.debug("Disposing thing {}", thingID());
129 commandProcessor.terminate();
130 if (scheduledFuture != null) {
131 scheduledFuture.cancel(false);
136 public void handleCommand(ChannelUID channelUID, Command command) {
137 if (command == null) {
138 logger.warn("Command passed to handler for thing {} is null", thingID());
142 // Don't try to send command if the device is not online
144 logger.debug("Can't handle command {} because handler for thing {} is not ONLINE", command, thingID());
148 Channel channel = thing.getChannel(channelUID.getId());
149 if (channel == null) {
150 logger.warn("Unknown channel {} for thing {}; is item defined correctly", channelUID.getId(), thingID());
154 // Get module and connector properties for this channel
155 String modNum = channel.getProperties().get(CHANNEL_PROPERTY_MODULE);
156 String conNum = channel.getProperties().get(CHANNEL_PROPERTY_CONNECTOR);
157 if (modNum == null || conNum == null) {
158 logger.error("Channel {} of thing {} has no module/connector property", channelUID.getId(), thingID());
162 if (command instanceof RefreshType) {
163 handleRefresh(modNum, conNum, channel);
167 switch (channel.getChannelTypeUID().getId()) {
168 case CHANNEL_TYPE_CC:
169 handleContactClosure(modNum, conNum, command, channelUID);
172 case CHANNEL_TYPE_IR:
173 handleInfrared(modNum, conNum, command, channelUID);
176 case CHANNEL_TYPE_SL:
177 handleSerial(modNum, conNum, command, channelUID);
180 case CHANNEL_TYPE_SL_DIRECT:
181 handleSerialDirect(modNum, conNum, command, channelUID);
185 logger.warn("Thing {} has unknown channel type {}", thingID(), channel.getChannelTypeUID().getId());
190 private void handleContactClosure(String modNum, String conNum, Command command, ChannelUID channelUID) {
191 logger.debug("Handling CC command {} on channel {} of thing {}", command, channelUID.getId(), thingID());
193 if (command instanceof OnOffType) {
194 CommandSetstate setstate = new CommandSetstate(thing, command, sendQueue, modNum, conNum);
199 private void handleInfrared(String modNum, String conNum, Command command, ChannelUID channelUID) {
200 logger.debug("Handling infrared command {} on channel {} of thing {}", command, channelUID.getId(), thingID());
202 String irCode = lookupCode(command);
203 if (irCode != null) {
204 CommandSendir sendir = new CommandSendir(thing, command, sendQueue, modNum, conNum, irCode, getCounter());
209 private void handleSerial(String modNum, String conNum, Command command, ChannelUID channelUID) {
210 logger.debug("Handle serial command {} on channel {} of thing {}", command, channelUID.getId(), thingID());
212 String slCode = lookupCode(command);
213 if (slCode != null) {
214 CommandSendserial sendserial = new CommandSendserial(thing, command, sendQueue, modNum, conNum, slCode);
215 sendserial.execute();
219 private void handleSerialDirect(String modNum, String conNum, Command command, ChannelUID channelUID) {
220 logger.debug("Handle serial command {} on channel {} of thing {}", command, channelUID.getId(), thingID());
222 CommandSendserial sendserial = new CommandSendserial(thing, command, sendQueue, modNum, conNum,
224 sendserial.execute();
227 private void handleRefresh(String modNum, String conNum, Channel channel) {
228 // REFRESH makes sense only for CC channels because we can query the device for the relay state
229 if (channel.getChannelTypeUID().getId().equals(CHANNEL_TYPE_CC)) {
230 logger.debug("Handle REFRESH command on channel {} for thing {}", channel.getUID().getId(), thingID());
232 CommandGetstate getstate = new CommandGetstate(thing, sendQueue, modNum, conNum);
234 if (getstate.isSuccessful()) {
235 updateState(channel.getUID(), getstate.state());
240 private int getCounter() {
241 return irCounter.getAndIncrement();
245 * Look up the IR or serial command code in the MAP file.
248 private String lookupCode(Command command) {
249 if (command.toString() == null) {
250 logger.warn("Unable to perform transform on null command string");
254 String mapFile = (String) thing.getConfiguration().get(THING_CONFIG_MAP_FILENAME);
255 if (StringUtils.isEmpty(mapFile)) {
256 logger.warn("MAP file is not defined in configuration of thing {}", thingID());
260 TransformationService transformService = TransformationHelper.getTransformationService(bundleContext, "MAP");
261 if (transformService == null) {
262 logger.error("Failed to get MAP transformation service for thing {}; is bundle installed?", thingID());
268 code = transformService.transform(mapFile, command.toString());
270 } catch (TransformationException e) {
271 logger.error("Failed to transform {} for thing {} using map file '{}', exception={}", command, thingID(),
272 mapFile, e.getMessage());
276 if (StringUtils.isEmpty(code)) {
277 logger.warn("No entry for {} in map file '{}' for thing {}", command, mapFile, thingID());
281 logger.debug("Transformed {} for thing {} with map file '{}'", command, thingID(), mapFile);
283 // Determine if the code is hex format. If so, convert to GC format
284 if (isHexCode(code)) {
285 logger.debug("Code is in hex format, convert to GC format");
287 code = convertHexToGC(code);
288 logger.debug("Converted hex code is: {}", code);
289 } catch (HexCodeConversionException e) {
290 logger.info("Failed to convert hex code to globalcache format: {}", e.getMessage());
298 * Check if the string looks like a hex code; if not then assume it's GC format
300 private boolean isHexCode(String code) {
301 Pattern pattern = Pattern.compile("0000( +[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f])+");
302 return pattern.matcher(code).find();
306 * Convert a hex code IR string to a Global Cache formatted IR string
308 private String convertHexToGC(String hexCode) throws HexCodeConversionException {
309 // Magic number for converting frequency to GC format
310 final int freqConversionFactor = 4145146;
311 final int repeat = 1;
316 String[] hexCodeArray = hexCode.trim().split(" ");
318 if (hexCodeArray.length < 5) {
319 throw new HexCodeConversionException("Hex code is too short");
322 if (!hexCodeArray[0].equals("0000")) {
323 throw new HexCodeConversionException("Illegal hex code element 0, should be 0000");
327 // Use magic number to get frequency
328 frequency = Math.round(freqConversionFactor / Integer.parseInt(hexCodeArray[1], 16));
329 } catch (Exception e) {
330 throw new HexCodeConversionException("Unable to convert frequency from element 1");
334 // Offset is derived from sequenceLength1
335 sequence1Length = Integer.parseInt(hexCodeArray[2], 16);
336 offset = (sequence1Length * 2) + 1;
337 } catch (Exception e) {
338 throw new HexCodeConversionException("Unable to convert offset from element 2");
341 // sequenceLength2 (hexCodeArray[3]) is not used
343 StringBuilder gcCode = new StringBuilder();
344 gcCode.append(frequency);
346 gcCode.append(repeat);
348 gcCode.append(offset);
351 // The remaining fields are just converted to decimal
352 for (int i = 4; i < hexCodeArray.length; i++) {
354 gcCode.append(Integer.parseInt(hexCodeArray[i], 16));
356 } catch (Exception e) {
357 throw new HexCodeConversionException("Unable to convert remaining hex code string");
360 return gcCode.toString();
363 public static String getAsHexString(byte[] b) {
364 StringBuilder sb = new StringBuilder();
366 for (int j = 0; j < b.length; j++) {
367 String s = String.format("%02x ", b[j] & 0xff);
370 return sb.toString();
373 public String getIP() {
374 return thing.getConfiguration().get(THING_PROPERTY_IP).toString();
377 public String getFlexActiveCable() {
378 return thing.getConfiguration().get(THING_CONFIG_ACTIVECABLE).toString();
381 private String thingID() {
382 // Return segments 2 & 3 only
383 String s = thing.getUID().getAsString();
384 return s.substring(s.indexOf(':') + 1);
388 * Manage the ONLINE/OFFLINE status of the thing
390 private void markThingOnline() {
392 logger.debug("Changing status of {} from {}({}) to ONLINE", thingID(), getStatus(), getDetail());
393 updateStatus(ThingStatus.ONLINE);
397 private void markThingOffline() {
399 logger.debug("Changing status of {} from {}({}) to OFFLINE", thingID(), getStatus(), getDetail());
400 updateStatus(ThingStatus.OFFLINE);
404 private void markThingOfflineWithError(ThingStatusDetail statusDetail, String statusMessage) {
405 // If it's offline with no detail or if it's not offline, mark it offline with detailed status
406 if ((isOffline() && getDetail().equals(ThingStatusDetail.NONE)) || !isOffline()) {
407 logger.debug("Changing status of {} from {}({}) to OFFLINE({})", thingID(), getStatus(), getDetail(),
409 updateStatus(ThingStatus.OFFLINE, statusDetail, statusMessage);
414 private boolean isOnline() {
415 return thing.getStatus().equals(ThingStatus.ONLINE);
418 private boolean isOffline() {
419 return thing.getStatus().equals(ThingStatus.OFFLINE);
422 private ThingStatus getStatus() {
423 return thing.getStatus();
426 private ThingStatusDetail getDetail() {
427 return thing.getStatusInfo().getStatusDetail();
431 * The {@link HexCodeConversionException} class is responsible for
433 * @author Mark Hilbush - Initial contribution
435 private class HexCodeConversionException extends Exception {
436 private static final long serialVersionUID = -4422352677677729196L;
438 public HexCodeConversionException(String message) {
444 * The {@link CommandProcessor} class is responsible for handling communication with the GlobalCache
445 * device. It waits for requests to arrive on a queue. When a request arrives, it sends the command to the
446 * GlobalCache device, waits for a response from the device, parses the response, then responds to the caller by
447 * placing a message in a response queue. Device response time is typically well below 100 ms, hence the reason
448 * fgor a relatively low timeout when reading the response queue.
450 * @author Mark Hilbush - Initial contribution
452 private class CommandProcessor extends Thread {
453 private Logger logger = LoggerFactory.getLogger(CommandProcessor.class);
455 private boolean terminate = false;
456 private final String TERMINATE_COMMAND = "terminate";
458 private final int SEND_QUEUE_MAX_DEPTH = 10;
459 private final int SEND_QUEUE_TIMEOUT = 2000;
461 private ConnectionManager connectionManager;
463 public CommandProcessor() {
464 super("GlobalCache Command Processor");
465 sendQueue = new LinkedBlockingQueue<>(SEND_QUEUE_MAX_DEPTH);
466 logger.debug("Processor for thing {} created request queue, depth={}", thingID(), SEND_QUEUE_MAX_DEPTH);
469 public void terminate() {
470 logger.debug("Processor for thing {} is being marked ready to terminate.", thingID());
473 // Send the command processor a terminate message
474 sendQueue.put(new RequestMessage(TERMINATE_COMMAND, null, null, null));
475 } catch (InterruptedException e) {
476 Thread.currentThread().interrupt();
483 logger.debug("Command processor STARTING for thing {} at IP {}", thingID(), getIP());
484 connectionManager = new ConnectionManager();
485 connectionManager.connect();
486 connectionManager.scheduleConnectionMonitorJob();
491 RequestMessage requestMessage;
493 requestMessage = sendQueue.poll(SEND_QUEUE_TIMEOUT, TimeUnit.MILLISECONDS);
494 if (requestMessage != null) {
495 if (requestMessage.getCommandName().equals(TERMINATE_COMMAND)) {
496 logger.debug("Processor for thing {} received terminate message", thingID());
501 connectionManager.connect();
502 if (connectionManager.isConnected()) {
504 long startTime = System.currentTimeMillis();
505 if (requestMessage.isCommand()) {
506 writeCommandToDevice(requestMessage);
507 deviceReply = readReplyFromDevice(requestMessage);
509 writeSerialToDevice(requestMessage);
510 deviceReply = "successful";
512 long endTime = System.currentTimeMillis();
513 logger.debug("Transaction '{}' for thing {} at {} took {} ms",
514 requestMessage.getCommandName(), thingID(), getIP(), endTime - startTime);
516 } catch (IOException e) {
517 logger.error("Comm error for thing {} at {}: {}", thingID(), getIP(), e.getMessage());
518 deviceReply = "ERROR: " + e.getMessage();
519 connectionManager.setCommError(deviceReply);
520 connectionManager.disconnect();
523 deviceReply = "ERROR: " + "No connection to device";
526 logger.trace("Processor for thing {} queuing response message: {}", thingID(), deviceReply);
527 requestMessage.getReceiveQueue().put(new ResponseMessage(deviceReply));
530 } catch (InterruptedException e) {
531 logger.warn("Processor for thing {} was interrupted: {}", thingID(), e.getMessage());
532 Thread.currentThread().interrupt();
535 connectionManager.cancelConnectionMonitorJob();
536 connectionManager.disconnect();
537 connectionManager = null;
538 logger.debug("Command processor TERMINATING for thing {} at IP {}", thingID(), getIP());
542 * Write the command to the device.
544 private void writeCommandToDevice(RequestMessage requestMessage) throws IOException {
545 logger.trace("Processor for thing {} writing command to device", thingID());
547 if (connectionManager.getCommandOut() == null) {
548 logger.debug("Error writing to device because output stream object is null");
552 byte[] deviceCommand = (requestMessage.getDeviceCommand() + '\r').getBytes();
553 connectionManager.getCommandOut().write(deviceCommand);
554 connectionManager.getCommandOut().flush();
558 * Read command reply from the device, then remove the CR at the end of the line.
560 private String readReplyFromDevice(RequestMessage requestMessage) throws IOException {
561 logger.trace("Processor for thing {} reading reply from device", thingID());
563 if (connectionManager.getCommandIn() == null) {
564 logger.debug("Error reading from device because input stream object is null");
565 return "ERROR: BufferedReader is null!";
568 logger.trace("Processor for thing {} reading response from device", thingID());
569 return connectionManager.getCommandIn().readLine().trim();
573 * Write a serial command to the device
575 private void writeSerialToDevice(RequestMessage requestMessage) throws IOException {
576 DataOutputStream out = connectionManager.getSerialOut(requestMessage.getCommandType());
578 logger.warn("Can't send serial command; output stream is null!");
582 byte[] deviceCommand;
583 deviceCommand = URLDecoder.decode(requestMessage.getDeviceCommand(), CHARSET).getBytes(CHARSET);
585 logger.debug("Writing decoded deviceCommand byte array: {}", getAsHexString(deviceCommand));
586 out.write(deviceCommand);
591 * The {@link ConnectionManager} class is responsible for managing the state of the connections to the
592 * command port and the serial port(s) of the device.
594 * @author Mark Hilbush - Initial contribution
596 private class ConnectionManager {
597 private Logger logger = LoggerFactory.getLogger(ConnectionManager.class);
599 private DeviceConnection commandConnection;
600 private DeviceConnection serialPort1Connection;
601 private DeviceConnection serialPort2Connection;
603 private SerialPortReader serialReaderPort1;
604 private SerialPortReader serialReaderPort2;
606 private boolean deviceIsConnected;
608 private final String COMMAND_NAME = "command";
609 private final String SERIAL1_NAME = "serial-1";
610 private final String SERIAL2_NAME = "serial-2";
612 private final int COMMAND_PORT = 4998;
613 private final int SERIAL1_PORT = 4999;
614 private final int SERIAL2_PORT = 5000;
616 private final int SOCKET_CONNECT_TIMEOUT = 1500;
618 private ScheduledFuture<?> connectionMonitorJob;
619 private final int CONNECTION_MONITOR_FREQUENCY = 60;
620 private final int CONNECTION_MONITOR_START_DELAY = 15;
622 private Runnable connectionMonitorRunnable = () -> {
623 logger.trace("Performing connection check for thing {} at IP {}", thingID(), commandConnection.getIP());
627 public ConnectionManager() {
628 commandConnection = new DeviceConnection(COMMAND_NAME, COMMAND_PORT);
629 serialPort1Connection = new DeviceConnection(SERIAL1_NAME, SERIAL1_PORT);
630 serialPort2Connection = new DeviceConnection(SERIAL2_NAME, SERIAL2_PORT);
632 commandConnection.setIP(getIPAddress());
633 serialPort1Connection.setIP(getIPAddress());
634 serialPort2Connection.setIP(getIPAddress());
636 deviceIsConnected = false;
639 private String getIPAddress() {
640 String ipAddress = ((GlobalCacheHandler) thing.getHandler()).getIP();
641 if (StringUtils.isEmpty(ipAddress)) {
642 logger.debug("Handler for thing {} could not get IP address from config", thingID());
643 markThingOfflineWithError(ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "IP address not set");
649 * Connect to the command and serial port(s) on the device. The serial connections are established only for
650 * devices that support serial.
652 protected void connect() {
657 // Get a connection to the command port
658 if (!commandConnect(commandConnection)) {
662 // Get a connection to serial port 1
663 if (deviceSupportsSerialPort1()) {
664 if (!serialConnect(serialPort1Connection)) {
665 commandDisconnect(commandConnection);
670 // Get a connection to serial port 2
671 if (deviceSupportsSerialPort2()) {
672 if (!serialConnect(serialPort2Connection)) {
673 commandDisconnect(commandConnection);
674 serialDisconnect(serialPort1Connection);
680 * All connections opened successfully, so we can mark the thing online
681 * and start the serial port readers
684 deviceIsConnected = true;
685 startSerialPortReaders();
688 private boolean commandConnect(DeviceConnection conn) {
689 logger.debug("Connecting to {} port for thing {} at IP {}", conn.getName(), thingID(), conn.getIP());
690 if (!openSocket(conn)) {
695 conn.setCommandIn(new BufferedReader(new InputStreamReader(conn.getSocket().getInputStream())));
696 conn.setCommandOut(new DataOutputStream(conn.getSocket().getOutputStream()));
697 } catch (IOException e) {
698 logger.debug("Error getting streams to {} port for thing {} at {}, exception={}", conn.getName(),
699 thingID(), conn.getIP(), e.getMessage());
700 markThingOfflineWithError(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
704 logger.info("Got a connection to {} port for thing {} at {}", conn.getName(), thingID(), conn.getIP());
709 private boolean serialConnect(DeviceConnection conn) {
710 logger.debug("Connecting to {} port for thing {} at {}", conn.getName(), thingID(), conn.getIP());
711 if (!openSocket(conn)) {
716 conn.setSerialIn(new BufferedInputStream(conn.getSocket().getInputStream()));
717 conn.setSerialOut(new DataOutputStream(conn.getSocket().getOutputStream()));
718 } catch (IOException e) {
719 logger.debug("Failed to get streams on {} port for thing {} at {}", conn.getName(), thingID(),
724 logger.info("Got a connection to {} port for thing {} at {}", conn.getName(), thingID(), conn.getIP());
729 private boolean openSocket(DeviceConnection conn) {
731 conn.setSocket(new Socket());
732 conn.getSocket().bind(new InetSocketAddress(ifAddress, 0));
733 conn.getSocket().connect(new InetSocketAddress(conn.getIP(), conn.getPort()), SOCKET_CONNECT_TIMEOUT);
734 } catch (IOException e) {
735 logger.debug("Failed to get socket on {} port for thing {} at {}", conn.getName(), thingID(),
742 private void closeSocket(DeviceConnection conn) {
743 if (conn.getSocket() != null) {
745 conn.getSocket().close();
746 } catch (IOException e) {
747 logger.debug("Failed to close socket on {} port for thing {} at {}", conn.getName(), thingID(),
754 * Disconnect from the command and serial port(s) on the device. Only disconnect the serial port
755 * connections if the devices have serial ports.
757 protected void disconnect() {
758 if (!isConnected()) {
761 commandDisconnect(commandConnection);
763 stopSerialPortReaders();
764 if (deviceSupportsSerialPort1()) {
765 serialDisconnect(serialPort1Connection);
767 if (deviceSupportsSerialPort2()) {
768 serialDisconnect(serialPort2Connection);
772 deviceIsConnected = false;
775 private void commandDisconnect(DeviceConnection conn) {
776 deviceDisconnect(conn);
779 private void serialDisconnect(DeviceConnection conn) {
780 deviceDisconnect(conn);
783 private void deviceDisconnect(DeviceConnection conn) {
784 logger.debug("Disconnecting from {} port for thing {} at IP {}", conn.getName(), thingID(), conn.getIP());
787 if (conn.getSerialOut() != null) {
788 conn.getSerialOut().close();
790 if (conn.getSerialIn() != null) {
791 conn.getSerialIn().close();
793 if (conn.getSocket() != null) {
794 conn.getSocket().close();
796 } catch (IOException e) {
797 logger.debug("Error closing {} port for thing {} at IP {}: exception={}", conn.getName(), thingID(),
798 conn.getIP(), e.getMessage());
803 private boolean isConnected() {
804 return deviceIsConnected;
807 public void setCommError(String errorMessage) {
808 markThingOfflineWithError(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, errorMessage);
812 * Retrieve the input/output streams for command and serial connections.
814 protected BufferedReader getCommandIn() {
815 return commandConnection.getCommandIn();
818 protected DataOutputStream getCommandOut() {
819 return commandConnection.getCommandOut();
822 protected BufferedInputStream getSerialIn(CommandType commandType) {
823 if (commandType != CommandType.SERIAL1 && commandType != CommandType.SERIAL2) {
826 if (commandType == CommandType.SERIAL1) {
827 return serialPort1Connection.getSerialIn();
829 return serialPort2Connection.getSerialIn();
833 protected DataOutputStream getSerialOut(CommandType commandType) {
834 if (commandType != CommandType.SERIAL1 && commandType != CommandType.SERIAL2) {
837 if (commandType == CommandType.SERIAL1) {
838 return serialPort1Connection.getSerialOut();
840 return serialPort2Connection.getSerialOut();
844 private boolean deviceSupportsSerialPort1() {
845 ThingTypeUID typeUID = thing.getThingTypeUID();
847 if (typeUID.equals(THING_TYPE_ITACH_SL)) {
849 } else if (typeUID.equals(THING_TYPE_GC_100_06) || typeUID.equals(THING_TYPE_GC_100_12)) {
851 } else if (typeUID.equals(THING_TYPE_ITACH_FLEX) && getFlexActiveCable().equals(ACTIVE_CABLE_SERIAL)) {
857 private boolean deviceSupportsSerialPort2() {
858 if (thing.getThingTypeUID().equals(THING_TYPE_GC_100_12)) {
865 * Periodically validate the command connection to the device by executing a getversion command.
867 private void scheduleConnectionMonitorJob() {
868 logger.debug("Starting connection monitor job for thing {} at IP {}", thingID(), commandConnection.getIP());
869 connectionMonitorJob = scheduler.scheduleWithFixedDelay(connectionMonitorRunnable,
870 CONNECTION_MONITOR_START_DELAY, CONNECTION_MONITOR_FREQUENCY, TimeUnit.SECONDS);
873 private void cancelConnectionMonitorJob() {
874 if (connectionMonitorJob != null) {
875 logger.debug("Canceling connection monitor job for thing {} at IP {}", thingID(),
876 commandConnection.getIP());
877 connectionMonitorJob.cancel(true);
878 connectionMonitorJob = null;
882 private void checkConnection() {
883 CommandGetversion getversion = new CommandGetversion(thing, sendQueue);
884 getversion.executeQuiet();
886 if (getversion.isSuccessful()) {
887 logger.trace("Connection check successful for thing {} at IP {}", thingID(), commandConnection.getIP());
889 deviceIsConnected = true;
891 logger.debug("Connection check failed for thing {} at IP {}", thingID(), commandConnection.getIP());
896 private void startSerialPortReaders() {
897 if (deviceSupportsSerialPort1()) {
898 serialReaderPort1 = startSerialPortReader(CommandType.SERIAL1, CONFIG_ENABLE_TWO_WAY_PORT_1,
899 CONFIG_END_OF_MESSAGE_DELIMITER_PORT_1);
901 if (deviceSupportsSerialPort2()) {
902 serialReaderPort2 = startSerialPortReader(CommandType.SERIAL2, CONFIG_ENABLE_TWO_WAY_PORT_2,
903 CONFIG_END_OF_MESSAGE_DELIMITER_PORT_2);
907 private SerialPortReader startSerialPortReader(CommandType serialDevice, String enableTwoWayConfig,
908 String endOfMessageDelimiterConfig) {
909 Boolean enableTwoWay = (Boolean) thing.getConfiguration().get(enableTwoWayConfig);
910 logger.debug("Enable two-way is {} for thing {} {}", enableTwoWay, thingID(), serialDevice);
912 if (Boolean.TRUE.equals(enableTwoWay)) {
913 // Get the end of message delimiter from the config, URL decode it, and convert it to a byte array
914 String endOfMessageString = (String) thing.getConfiguration().get(endOfMessageDelimiterConfig);
915 if (StringUtils.isNotEmpty(endOfMessageString)) {
916 logger.debug("End of message is {} for thing {} {}", endOfMessageString, thingID(), serialDevice);
919 endOfMessage = URLDecoder.decode(endOfMessageString, CHARSET).getBytes(CHARSET);
920 } catch (UnsupportedEncodingException e) {
921 logger.info("Unable to decode end of message delimiter {} for thing {} {}", endOfMessageString,
922 thingID(), serialDevice);
926 // Start the serial reader using the above end-of-message delimiter
927 SerialPortReader serialPortReader = new SerialPortReader(serialDevice, getSerialIn(serialDevice),
929 serialPortReader.start();
930 return serialPortReader;
932 logger.warn("End of message delimiter is not defined in configuration of thing {}", thingID());
938 private void stopSerialPortReaders() {
939 if (deviceSupportsSerialPort1() && serialReaderPort1 != null) {
940 logger.debug("Stopping serial port 1 reader for thing {} at IP {}", thingID(),
941 commandConnection.getIP());
942 serialReaderPort1.stop();
943 serialReaderPort1 = null;
945 if (deviceSupportsSerialPort2() && serialReaderPort2 != null) {
946 logger.debug("Stopping serial port 2 reader for thing {} at IP {}", thingID(),
947 commandConnection.getIP());
948 serialReaderPort2.stop();
949 serialReaderPort2 = null;
955 * The {@link SerialReader} class reads data from the serial connection. When data is
956 * received, the receive channel is updated with the data. Data is read up to the
957 * end-of-message delimiter defined in the Thing configuration.
959 * @author Mark Hilbush - Initial contribution
961 private class SerialPortReader {
962 private Logger logger = LoggerFactory.getLogger(SerialPortReader.class);
964 private CommandType serialPort;
965 private BufferedInputStream serialPortIn;
966 private ScheduledFuture<?> serialPortReaderJob;
967 private boolean terminateSerialPortReader;
969 private byte[] endOfMessage;
971 SerialPortReader(CommandType serialPort, BufferedInputStream serialIn, byte[] endOfMessage) {
972 if (serialIn == null) {
973 throw new IllegalArgumentException("Serial input stream is not set");
975 this.serialPort = serialPort;
976 this.serialPortIn = serialIn;
977 this.endOfMessage = endOfMessage;
978 serialPortReaderJob = null;
979 terminateSerialPortReader = false;
982 public void start() {
983 serialPortReaderJob = scheduledExecutorService.schedule(this::serialPortReader, 0, TimeUnit.SECONDS);
987 if (serialPortReaderJob != null) {
988 terminateSerialPortReader = true;
989 serialPortReaderJob.cancel(true);
990 serialPortReaderJob = null;
994 private void serialPortReader() {
995 logger.info("Serial reader RUNNING for {} on {}:{}", thingID(), getIP(), serialPort);
997 while (!terminateSerialPortReader) {
1000 buffer = readUntilEndOfMessage(endOfMessage);
1001 if (buffer == null) {
1002 logger.debug("Received end-of-stream from {} on {}", getIP(), serialPort);
1005 logger.debug("Rcv data from {} at {}:{}: {}", thingID(), getIP(), serialPort,
1006 getAsHexString(buffer));
1007 updateFeedbackChannel(buffer);
1008 } catch (UnsupportedEncodingException e) {
1009 logger.info("Unsupported encoding exception: {}", e.getMessage(), e);
1011 } catch (IOException e) {
1012 logger.debug("Serial Reader got IOException: {}", e.getMessage());
1014 } catch (InterruptedException e) {
1015 logger.debug("Serial Reader got InterruptedException: {}", e.getMessage());
1019 logger.debug("Serial reader STOPPING for {} on {}:{}", thingID(), getIP(), serialPort);
1022 private byte[] readUntilEndOfMessage(byte[] endOfMessageDelimiter) throws IOException, InterruptedException {
1023 logger.debug("Serial reader waiting for available data");
1026 ByteArrayOutputStream buf = new ByteArrayOutputStream();
1028 // Read from the serial input stream until the endOfMessage delimiter is found
1030 val = serialPortIn.read();
1032 logger.debug("Serial reader got unexpected end of input stream");
1033 throw new IOException("Unexpected end of stream");
1037 if (findEndOfMessage(buf.toByteArray(), endOfMessageDelimiter)) {
1038 // Found the end-of-message delimiter in the serial input stream
1042 logger.debug("Serial reader returning a message");
1043 return buf.toByteArray();
1046 private boolean findEndOfMessage(byte[] buf, byte[] endOfMessage) {
1047 int lengthEOM = endOfMessage.length;
1048 int lengthBuf = buf.length;
1050 // Look for the end-of-message delimiter at the end of the buffer
1051 while (lengthEOM > 0) {
1054 if (lengthBuf < 0 || endOfMessage[lengthEOM] != buf[lengthBuf]) {
1055 // No match on end of message
1059 logger.debug("Serial reader found the end-of-message delimiter in the input buffer");
1063 private void updateFeedbackChannel(byte[] buffer) {
1065 if (serialPort.equals(CommandType.SERIAL1)) {
1066 channelId = CHANNEL_SL_M1_RECEIVE;
1067 } else if (serialPort.equals(CommandType.SERIAL2)) {
1068 channelId = CHANNEL_SL_M2_RECEIVE;
1070 logger.warn("Unknown serial port; can't update feedback channel: {}", serialPort);
1073 Channel channel = getThing().getChannel(channelId);
1074 if (channel != null && isLinked(channelId)) {
1075 logger.debug("Updating feedback channel for port {}", serialPort);
1077 String encodedReply = URLEncoder.encode(new String(buffer, CHARSET), CHARSET);
1078 logger.debug("encodedReply='{}'", encodedReply);
1079 updateState(channel.getUID(), new StringType(encodedReply));
1080 } catch (UnsupportedEncodingException e) {
1081 logger.warn("Exception while encoding data read from serial device: {}", e.getMessage());
1088 * The {@link DeviceConnection} class stores information about the connection to a globalcache device.
1089 * There can be two types of connections, command and serial. The command connection is used to
1090 * send all but the serial strings to the device. The serial connection is used exclusively to
1091 * send serial messages. These serial connections are applicable only to iTach SL and GC-100 devices.
1093 * @author Mark Hilbush - Initial contribution
1095 private class DeviceConnection {
1096 private String connectionName;
1098 private String ipAddress;
1099 private Socket socket;
1100 private BufferedReader commandIn;
1101 private DataOutputStream commandOut;
1102 private BufferedInputStream serialIn;
1103 private DataOutputStream serialOut;
1105 DeviceConnection(String connectionName, int port) {
1106 setName(connectionName);
1111 setCommandOut(null);
1116 public void reset() {
1119 setCommandOut(null);
1124 public String getName() {
1125 return connectionName;
1128 public void setName(String connectionName) {
1129 this.connectionName = connectionName;
1132 public int getPort() {
1136 public void setPort(int port) {
1140 public String getIP() {
1144 public void setIP(String ipAddress) {
1145 this.ipAddress = ipAddress;
1148 public Socket getSocket() {
1152 public void setSocket(Socket socket) {
1153 this.socket = socket;
1156 public BufferedReader getCommandIn() {
1160 public void setCommandIn(BufferedReader commandIn) {
1161 this.commandIn = commandIn;
1164 public DataOutputStream getCommandOut() {
1168 public void setCommandOut(DataOutputStream commandOut) {
1169 this.commandOut = commandOut;
1172 public BufferedInputStream getSerialIn() {
1176 public void setSerialIn(BufferedInputStream serialIn) {
1177 this.serialIn = serialIn;
1180 public DataOutputStream getSerialOut() {
1184 public void setSerialOut(DataOutputStream serialOut) {
1185 this.serialOut = serialOut;