text: /**
   * Copyright (c) 2010-2020 Contributors to the openHAB project
   *
   * See the NOTICE file(s) distributed with this work for additional
   * information.
   *
   * This program and the accompanying materials are made available under the
   * terms of the Eclipse Public License 2.0 which is available at
   * http://www.eclipse.org/legal/epl-2.0
   *
   * SPDX-License-Identifier: EPL-2.0
   */
  package org.openhab.binding.alarmdecoder.internal.handler;
  
  import java.io.BufferedReader;
  import java.io.BufferedWriter;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.io.OutputStreamWriter;
  import java.net.Socket;
  import java.net.UnknownHostException;
  import java.util.Date;
  import java.util.concurrent.ScheduledFuture;
  import java.util.concurrent.TimeUnit;
  
  import org.eclipse.jdt.annotation.NonNullByDefault;
  import org.eclipse.jdt.annotation.Nullable;
  import org.openhab.binding.alarmdecoder.internal.config.IPBridgeConfig;
  import org.openhab.core.thing.Bridge;
  import org.openhab.core.thing.ThingStatus;
  import org.openhab.core.thing.ThingStatusDetail;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /**
   * Handler responsible for communicating via TCP with the Nu Tech Alarm Decoder device.
   * Based on and including code from the original OH1 alarmdecoder binding.
   *
   * @author Bernd Pfrommer - Initial contribution (OH1 version)
   * @author Bob Adair - Re-factored into OH2 binding
   */
  @NonNullByDefault
  public class IPBridgeHandler extends ADBridgeHandler {
  
      private final Logger logger = LoggerFactory.getLogger(IPBridgeHandler.class);
  
      private IPBridgeConfig config = new IPBridgeConfig();
  
      private @Nullable Socket socket = null;
  
      public IPBridgeHandler(Bridge bridge) {
          super(bridge);
      }
  
      @Override
      public void initialize() {
          logger.debug("Initializing IP bridge handler");
          config = getConfigAs(IPBridgeConfig.class);
          discovery = config.discovery;
  
          if (config.hostname == null) {
              updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "hostname not configured");
              return;
          }
          if (config.tcpPort <= 0 || config.tcpPort > 65535) {
              updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "invalid port number configured");
              return;
          }
  
          // set the thing status to UNKNOWN temporarily and let the background connect task decide the real status.
          updateStatus(ThingStatus.UNKNOWN);
  
          scheduler.submit(this::connect); // start the async connect task
      }
  
      @Override
      protected synchronized void connect() {
          disconnect(); // make sure we are disconnected
          writeException = false;
          try {
              Socket socket = new Socket(config.hostname, config.tcpPort);
              this.socket = socket;
              reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), AD_CHARSET));
              writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), AD_CHARSET));
              logger.debug("connected to {}:{}", config.hostname, config.tcpPort);
              panelReadyReceived = false;
              startMsgReader();
              updateStatus(ThingStatus.ONLINE);
  
              // Start connection check job
              logger.debug("Scheduling connection check job with interval {} minutes.", config.reconnect);
              lastReceivedTime = new Date();
              connectionCheckJob = scheduler.scheduleWithFixedDelay(this::connectionCheck, config.reconnect,
                      config.reconnect, TimeUnit.MINUTES);
          } catch (UnknownHostException e) {
              updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "unknown host");
              disconnect();
          } catch (IOException e) {
              updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
              disconnect();
              scheduleConnectRetry(config.reconnect); // Possibly a retryable error. Try again later.
          }
      }
  
      protected synchronized void connectionCheck() {
          logger.trace("Connection check job running");
  
          Thread mrThread = msgReaderThread;
          if (mrThread != null && !mrThread.isAlive()) {
              logger.debug("Reader thread has exited abnormally. Restarting.");
              scheduler.submit(this::connect);
          } else if (writeException) {
              logger.debug("Write exception encountered. Resetting connection.");
              scheduler.submit(this::connect);
          } else {
              Date now = new Date();
              Date last = lastReceivedTime;
              if (last != null && config.timeout > 0
                      && ((last.getTime() + (config.timeout * 60 * 1000)) < now.getTime())) {
                  logger.warn("Last valid message received at {}. Resetting connection.", last);
                  scheduler.submit(this::connect);
              }
          }
      }
  
      @Override
      protected synchronized void disconnect() {
          logger.trace("Disconnecting");
          // stop scheduled connection check and retry jobs
          ScheduledFuture<?> crJob = connectRetryJob;
          if (crJob != null) {
              // use cancel(false) so we don't kill ourselves when connect retry job calls disconnect()
              crJob.cancel(false);
              connectRetryJob = null;
          }
          ScheduledFuture<?> ccJob = connectionCheckJob;
          if (ccJob != null) {
              // use cancel(false) so we don't kill ourselves when reconnect job calls disconnect()
              ccJob.cancel(false);
              connectionCheckJob = null;
          }
  
          // Must close the socket first so the message reader thread will exit properly.
          // The BufferedReader.readLine() call used in readerThread() is not interruptable.
          Socket s = socket;
          if (s != null) {
              try {
                  s.close();
              } catch (IOException e) {
                  logger.debug("error closing socket: {}", e.getMessage());
              }
          }
          socket = null;
  
          stopMsgReader();
  
          try {
              BufferedWriter bw = writer;
              if (bw != null) {
                  bw.close();
              }
              BufferedReader br = reader;
              if (br != null) {
                  br.close();
              }
          } catch (IOException e) {
              logger.debug("error closing reader/writer: {}", e.getMessage());
          }
          writer = null;
          reader = null;
      }
  }
  