de.zazaz.iot.bosch.indego.ifttt.IftttIndegoAdapter.java Source code

Java tutorial

Introduction

Here is the source code for de.zazaz.iot.bosch.indego.ifttt.IftttIndegoAdapter.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.zazaz.iot.bosch.indego.ifttt;

import java.io.IOException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;

import de.zazaz.iot.bosch.indego.DeviceCommand;
import de.zazaz.iot.bosch.indego.DeviceStateInformation;
import de.zazaz.iot.bosch.indego.DeviceStatus;
import de.zazaz.iot.bosch.indego.IndegoController;
import de.zazaz.iot.bosch.indego.IndegoException;

/**
 * This class connects to an Indego device and provides a simple server which can be used by the IFTTT maker
 * channel.
 */
public class IftttIndegoAdapter {

    /** the logger */
    private static final Logger LOG = LogManager.getLogger(IftttIndegoAdapter.class);

    private static final String URL_IFTTT = "https://maker.ifttt.com/trigger/%s/with/key/%s";

    /** the configuration to use */
    private IftttIndegoAdapterConfiguration configuration;

    /** this is used for indicating, that we are request to shutdown */
    private AtomicBoolean flagShutdown = new AtomicBoolean(false);

    /** semaphore for waking worker thread up */
    private Semaphore semThreadWaker;

    /** a reference to the worker thread */
    private Thread threadWorker;

    /** contains the last command */
    private AtomicReference<String> commandToExecute = new AtomicReference<>();

    /**
     * Initializes the IFTTT Adapter.
     * 
     * @param configuration_ the configuration to use
     */
    public IftttIndegoAdapter(IftttIndegoAdapterConfiguration configuration_) {
        // Get a clone, since we don't want to be manipulated during runtime
        configuration = (IftttIndegoAdapterConfiguration) configuration_.clone();
    }

    /**
     * This starts the adapter.
     */
    public synchronized void startup() {
        if (threadWorker != null) {
            throw new IllegalStateException("The adapter is already started");
        }
        flagShutdown.set(false);
        semThreadWaker = new Semaphore(0);
        threadWorker = new Thread(new Runnable() {

            @Override
            public void run() {
                runOuter();
            }
        });
        LOG.debug("Starting worker thread");
        try {
            threadWorker.start();
            LOG.debug("Worker thread started");
        } catch (RuntimeException ex) {
            LOG.error("Failed to start worker thread", ex);
            threadWorker = null;
            throw ex;
        }
    }

    /**
     * This shuts down the adapter.
     */
    public synchronized void shutdown() {
        if (threadWorker == null) {
            throw new IllegalStateException("The adapter is not started");
        }
        try {
            LOG.debug("Requesting worker thread to shut down");
            flagShutdown.set(true);
            semThreadWaker.release();
            LOG.debug("Waiting for worker thread");
            while (true) {
                try {
                    threadWorker.join();
                    break;
                } catch (InterruptedException ex) {
                    // Ignored
                }
            }
            LOG.debug("Worker thread terminated, shutdown complete");
        } finally {
            threadWorker = null;
            semThreadWaker = null;
            flagShutdown.set(false);
        }
    }

    /**
     * This is the initial entry point for the worker thread.
     */
    private void runOuter() {
        LOG.debug("Worker thread started");
        try {
            while (!flagShutdown.get()) {
                try {
                    runInternal();
                } catch (Exception ex) {
                    LOG.fatal("Unhandled exception thrown! Trying a restart...", ex);
                }
            }
        } finally {
            LOG.debug("Closing worker thread");
        }
    }

    /**
     * This is the inner run method, which does the actual work.
     */
    private void runInternal() {
        CloseableHttpClient httpClient = buildHttpClient();
        Server httpServer = buildHttpServer();
        IndegoController indegoController = null;
        Semaphore semWakeup = semThreadWaker;

        boolean firstRun = true;
        boolean lastOffline = false;
        DeviceCommand lastCommand = null;
        int lastErrorCode = 0;

        try {
            while (!flagShutdown.get()) {
                if (indegoController == null) {
                    LOG.info("No connection to Indego server. Creating connection.");
                    indegoController = connectIndego();
                    if (indegoController == null) {
                        LOG.warn("Was not able to connect to Indego server.");
                    }
                }

                DeviceStateInformation currentState = null;
                if (indegoController != null) {
                    try {
                        currentState = indegoController.getState();
                    } catch (Exception ex) {
                        LOG.error("Exception during fetching Indego state", ex);
                        disconnect(indegoController);
                        indegoController = null;
                        try {
                            semWakeup.tryAcquire(configuration.getPollingIntervalMs(), TimeUnit.MILLISECONDS);
                        } catch (InterruptedException ex2) {
                            // Ignored
                        }
                        continue;
                    }
                }

                if (indegoController != null) {
                    String command = commandToExecute.getAndSet(null);
                    if (command != null) {
                        LOG.info(String.format("Received command: %s", command));
                        DeviceCommand cmd = null;
                        try {
                            cmd = DeviceCommand.valueOf(command);
                        } catch (Exception ex) {
                            LOG.error(String.format("Received invalid command from IFTTT (%s)", command), ex);
                        }
                        try {
                            if (cmd != null) {
                                LOG.info(String.format("Sending command to Indego: %s", cmd));
                                indegoController.sendCommand(cmd);
                                LOG.info(String.format("Command '%s' was sent successfully", cmd));
                            }
                        } catch (Exception ex) {
                            LOG.error(String.format("Indego was not able to execute command (%s)", command), ex);
                        }
                    }
                }

                boolean currentOffline = indegoController == null;
                DeviceCommand currentCommand = null;
                int currentErrorCode = 0;
                if (currentState != null) {
                    currentCommand = DeviceStatus.decodeStatusCode(currentState.getState()).getAssociatedCommand();
                    currentErrorCode = currentState.getError();
                }

                boolean commitOffline = firstRun;
                boolean commitCommand = firstRun;
                boolean commitErrorCode = firstRun;

                if (!firstRun) {
                    String percentMowed = currentState != null ? Integer.toString(currentState.getMowed())
                            : "unknown";

                    if (currentOffline != lastOffline) {
                        if (currentOffline) {
                            commitOffline = sendIfftTrigger(httpClient, configuration.getIftttOfflineEventName(),
                                    "offline", "", percentMowed);
                        } else {
                            commitOffline = sendIfftTrigger(httpClient, configuration.getIftttOnlineEventName(),
                                    "online", "", percentMowed);
                        }
                    }

                    if (currentCommand != lastCommand) {
                        String message = currentCommand != null ? currentCommand.toString() : "UNKNOWN";
                        commitCommand = sendIfftTrigger(httpClient, configuration.getIftttStateChangeEventName(),
                                message, "", percentMowed);
                    }

                    if (currentErrorCode != lastErrorCode) {
                        if (currentErrorCode == 0) {
                            commitErrorCode = sendIfftTrigger(httpClient,
                                    configuration.getIftttErrorClearedEventName(),
                                    Integer.toString(currentErrorCode), "", percentMowed);
                        } else {
                            commitErrorCode = sendIfftTrigger(httpClient, configuration.getIftttErrorEventName(),
                                    Integer.toString(currentErrorCode), "Unknown error", percentMowed);
                        }
                    }
                }

                if (commitOffline) {
                    lastOffline = currentOffline;
                }
                if (commitCommand) {
                    lastCommand = currentCommand;
                }
                if (commitErrorCode) {
                    lastErrorCode = currentErrorCode;
                }
                firstRun = false;

                try {
                    semWakeup.tryAcquire(configuration.getPollingIntervalMs(), TimeUnit.MILLISECONDS);
                } catch (InterruptedException ex) {
                    // Ignored
                }
            }
        } finally {
            disconnect(indegoController);
            disconnect(httpServer);
            disconnect(httpClient);
        }
    }

    /**
     * This handles a command request
     * 
     * @param req_ the servlet request
     * @param resp_ the servlet response
     */
    private void handleIftttCommand(HttpServletRequest req_, HttpServletResponse resp_) {
        String commandStr = req_.getPathInfo();
        int idx = commandStr.lastIndexOf('/');
        if (idx > 0) {
            commandStr = commandStr.substring(idx + 1);
        }
        commandToExecute.set(commandStr);
        semThreadWaker.release();
    }

    private boolean sendIfftTrigger(CloseableHttpClient httpClient, String triggerName, String value1,
            String value2, String value3) {
        if (triggerName == null) {
            return true;
        }

        LOG.info(String.format("Sending IFTTT trigger ('%s', '%s', '%s', '%s')", triggerName, value1, value2,
                value3));

        String json = String.format("{ \"value1\" : \"%s\", \"value2\" : \"%s\", \"value3\" : \"%s\" }", value1,
                value2, value3);

        HttpPut httpRequest = new HttpPut(String.format(URL_IFTTT, triggerName, configuration.getIftttMakerKey()));
        httpRequest.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));

        CloseableHttpResponse response = null;
        try {
            response = httpClient.execute(httpRequest);
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                LOG.debug("IFTT Trigger sent successfully");
            } else {
                LOG.error(String.format("IFTTT trigger was not accepted ('%s', '%s', '%s', '%s') => %s",
                        triggerName, value1, value2, value3, response.getStatusLine()));
            }
        } catch (IOException ex) {
            LOG.error(String.format("Error while sending IFTTT trigger ('%s', '%s', '%s', '%s')", triggerName,
                    value1, value2, value3), ex);
            return false;
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
            } catch (IOException ex) {
                // Ignored
            }
        }

        return true;
    }

    /**
     * Connects to the Indego server.
     * 
     * @return a connected controller instance; null, if the connection was not successful.
     */
    private IndegoController connectIndego() {
        try {
            LOG.info("Connecting to Indego");
            IndegoController result = new IndegoController(configuration.getIndegoBaseUrl(),
                    configuration.getIndegoUsername(), configuration.getIndegoPassword());
            result.connect();
            LOG.info("Connection to Indego established");
            return result;
        } catch (IndegoException ex) {
            LOG.error("Connection to Indego failed", ex);
            return null;
        }
    }

    /**
     * This creates a HTTP client instance for connecting the IFTTT server.
     * 
     * @return the HTTP client instance
     */
    private CloseableHttpClient buildHttpClient() {
        if (configuration.isIftttIgnoreServerCertificate()) {
            try {
                SSLContextBuilder builder = new SSLContextBuilder();
                builder.loadTrustMaterial(new TrustStrategy() {
                    @Override
                    public boolean isTrusted(X509Certificate[] chain_, String authType_)
                            throws CertificateException {
                        return true;
                    }
                });
                SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(builder.build());
                return HttpClients.custom().setSSLSocketFactory(sslsf).build();
            } catch (Exception ex) {
                LOG.error(ex);
                // This should never happen, but we have to handle it
                throw new RuntimeException(ex);
            }
        } else {
            return HttpClients.createDefault();
        }
    }

    /**
     * This creates a HTTP server instance for receiving IFTTT commands.
     * 
     * @return the HTTP server instance
     */
    private Server buildHttpServer() {
        if (configuration.getIftttReceiverPort() == 0) {
            return null;
        }

        HttpServlet servlet = new HttpServlet() {

            private static final long serialVersionUID = 1L;

            @Override
            protected void doGet(HttpServletRequest req_, HttpServletResponse resp_)
                    throws ServletException, IOException {
                if (req_.getPathInfo()
                        .startsWith(String.format("/%s/command/", configuration.getIftttReceiverSecret()))) {
                    handleIftttCommand(req_, resp_);
                    resp_.setContentType("text/html");
                    resp_.setStatus(HttpStatus.SC_OK);
                } else {
                    super.doGet(req_, resp_);
                }
            }

        };

        Server result = new Server(configuration.getIftttReceiverPort());
        ServletHandler handler = new ServletHandler();
        result.setHandler(handler);
        handler.addServletWithMapping(new ServletHolder(servlet), "/*");
        try {
            result.start();
        } catch (Exception ex) {
            LOG.fatal("Was not able to start the command server", ex);
            return null;
        }

        return result;
    }

    /**
     * Disconnects a connected Indego controller.
     * 
     * @param indegoController the controller to disconnect
     */
    private void disconnect(IndegoController indegoController) {
        try {
            if (indegoController != null) {
                LOG.info("Disconnecting from Indego");
                indegoController.disconnect();
            }
        } catch (Exception ex) {
            LOG.warn("Something strange happened while disconnecting from Indego", ex);
        }
    }

    /**
     * Closes the HTTP client.
     * 
     * @param httpClient the HTTP client instance to close
     */
    private void disconnect(CloseableHttpClient httpClient) {
        try {
            if (httpClient != null) {
                LOG.info("Closing the HTTP client");
                httpClient.close();
            }
        } catch (Exception ex) {
            LOG.warn("Something strange happened while closing the HTTP client", ex);
        }
    }

    /**
     * Closes the HTTP server.
     * 
     * @param httpServer the HTTP server to close
     */
    private void disconnect(Server httpServer) {
        if (httpServer != null) {
            httpServer.destroy();
        }
    }
}