org.openhab.binding.neeo.internal.handler.NeeoBrainHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.binding.neeo.internal.handler.NeeoBrainHandler.java

Source

/**
 * Copyright (c) 2010-2019 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.neeo.internal.handler;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URL;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.servlet.ServletException;

import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.core.net.NetworkAddressService;
import org.eclipse.smarthome.core.thing.Bridge;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingStatus;
import org.eclipse.smarthome.core.thing.ThingStatusDetail;
import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler;
import org.eclipse.smarthome.core.thing.binding.ThingHandler;
import org.eclipse.smarthome.core.types.Command;
import org.openhab.binding.neeo.internal.NeeoBrainApi;
import org.openhab.binding.neeo.internal.NeeoBrainConfig;
import org.openhab.binding.neeo.internal.NeeoConstants;
import org.openhab.binding.neeo.internal.NeeoUtil;
import org.openhab.binding.neeo.internal.models.NeeoAction;
import org.openhab.binding.neeo.internal.models.NeeoBrain;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;

/**
 * A subclass of {@link BaseBridgeHandler} is responsible for handling commands and discovery for a
 * {@link NeeoBrain}
 *
 * @author Tim Roberts - Initial contribution
 */
@NonNullByDefault
public class NeeoBrainHandler extends BaseBridgeHandler {

    /** The logger */
    private final Logger logger = LoggerFactory.getLogger(NeeoBrainHandler.class);

    /** The {@link HttpService} to register callbacks */
    private final HttpService httpService;

    /** The {@link NetworkAddressService} to use */
    private final NetworkAddressService networkAddressService;

    /** GSON implementation - only used to deserialize {@link NeeoAction} */
    private final Gson gson = new Gson();

    /** The port the HTTP service is listening on */
    private final int servicePort;

    /**
     * The initialization task (null until set by {@link #initializeTask()} and set back to null in {@link #dispose()}
     */
    private final AtomicReference<@Nullable Future<?>> initializationTask = new AtomicReference<>();

    /** The check status task (not-null when connecting, null otherwise) */
    private final AtomicReference<@Nullable Future<?>> checkStatus = new AtomicReference<>();

    /** The lock that protected multi-threaded access to the state variables */
    private final ReadWriteLock stateLock = new ReentrantReadWriteLock();

    /** The {@link NeeoBrainApi} (null until set by {@link #initializationTask}) */
    @Nullable
    private NeeoBrainApi neeoBrainApi;

    /** The path to the forward action servlet - will be null if not enabled */
    @Nullable
    private String servletPath;

    /** The servlet for forward actions - will be null if not enabled */
    @Nullable
    private NeeoForwardActionsServlet forwardActionServlet;

    /**
     * Instantiates a new neeo brain handler from the {@link Bridge}, service port, {@link HttpService} and
     * {@link NetworkAddressService}.
     *
     * @param bridge the non-null {@link Bridge}
     * @param servicePort the service port the http service is listening on
     * @param httpService the non-null {@link HttpService}
     * @param networkAddressService the non-null {@link NetworkAddressService}
     */
    NeeoBrainHandler(Bridge bridge, int servicePort, HttpService httpService,
            NetworkAddressService networkAddressService) {
        super(bridge);

        Objects.requireNonNull(bridge, "bridge cannot be null");
        Objects.requireNonNull(httpService, "httpService cannot be null");
        Objects.requireNonNull(networkAddressService, "networkAddressService cannot be null");

        this.servicePort = servicePort;
        this.httpService = httpService;
        this.networkAddressService = networkAddressService;
    }

    /**
     * Handles any {@Commands} sent - this bridge has no commands and does nothing
     *
     * @see
     *      org.eclipse.smarthome.core.thing.binding.ThingHandler#handleCommand(org.eclipse.smarthome.core.thing.ChannelUID,
     *      org.eclipse.smarthome.core.types.Command)
     */
    @Override
    public void handleCommand(ChannelUID channelUID, Command command) {
    }

    /**
     * Simply cancels any existing initialization tasks and schedules a new task
     *
     * @see org.eclipse.smarthome.core.thing.binding.BaseThingHandler#initialize()
     */
    @Override
    public void initialize() {
        NeeoUtil.cancel(initializationTask.getAndSet(scheduler.submit(() -> {
            initializeTask();
        })));
    }

    /**
     * Initializes the bridge by connecting to the configuration ip address and parsing the results. Properties will be
     * set and the thing will go online.
     */
    private void initializeTask() {
        final Lock writerLock = stateLock.writeLock();
        writerLock.lock();
        try {
            NeeoUtil.checkInterrupt();

            final NeeoBrainConfig config = getBrainConfig();
            final String ipAddress = config.getIpAddress();
            if (ipAddress == null || StringUtils.isEmpty(ipAddress)) {
                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
                        "Brain IP Address must be specified");
                return;
            }
            final NeeoBrainApi api = new NeeoBrainApi(ipAddress);
            final NeeoBrain brain = api.getBrain();
            final String brainId = getNeeoBrainId();

            NeeoUtil.checkInterrupt();
            neeoBrainApi = api;

            final Map<String, String> properties = new HashMap<>();
            addProperty(properties, "Name", brain.getName());
            addProperty(properties, "Version", brain.getVersion());
            addProperty(properties, "Label", brain.getLabel());
            addProperty(properties, "Is Configured", String.valueOf(brain.isConfigured()));
            addProperty(properties, "Key", brain.getKey());
            addProperty(properties, "AirKey", brain.getAirkey());
            addProperty(properties, "Last Change", String.valueOf(brain.getLastChange()));
            updateProperties(properties);

            if (config.isEnableForwardActions()) {
                NeeoUtil.checkInterrupt();

                forwardActionServlet = new NeeoForwardActionsServlet(scheduler,
                        new NeeoForwardActionsServlet.Callback() {
                            @Override
                            public void post(String json) {
                                triggerChannel(NeeoConstants.CHANNEL_BRAIN_FOWARDACTIONS, json);

                                final NeeoAction action = gson.fromJson(json, NeeoAction.class);

                                for (final Thing child : getThing().getThings()) {
                                    final ThingHandler th = child.getHandler();
                                    if (th instanceof NeeoRoomHandler) {
                                        ((NeeoRoomHandler) th).processAction(action);
                                    }
                                }
                            }

                        }, config.getForwardChain());

                NeeoUtil.checkInterrupt();
                try {
                    servletPath = NeeoConstants.WEBAPP_FORWARDACTIONS.replace("{brainid}", brainId);

                    httpService.registerServlet(servletPath, forwardActionServlet, new Hashtable<>(),
                            httpService.createDefaultHttpContext());

                    final URL callbackURL = createCallbackUrl(brainId, config);
                    if (callbackURL == null) {
                        logger.debug(
                                "Unable to create a callback URL because there is no primary address specified (please set the primary address in the configuration)");
                    } else {
                        final URL url = new URL(callbackURL, servletPath);
                        api.registerForwardActions(url);
                    }
                } catch (NamespaceException | ServletException e) {
                    logger.debug("Error registering forward actions to {}: {}", servletPath, e.getMessage(), e);
                }
            }

            NeeoUtil.checkInterrupt();
            updateStatus(ThingStatus.ONLINE);
            NeeoUtil.checkInterrupt();
            if (config.getCheckStatusInterval() > 0) {
                NeeoUtil.cancel(checkStatus.getAndSet(scheduler.scheduleWithFixedDelay(() -> {
                    try {
                        NeeoUtil.checkInterrupt();
                        checkStatus(ipAddress);
                    } catch (InterruptedException e) {
                        // do nothing - we were interrupted and should stop
                    }
                }, config.getCheckStatusInterval(), config.getCheckStatusInterval(), TimeUnit.SECONDS)));
            }
        } catch (IOException e) {
            logger.debug("Exception occurred connecting to brain: {}", e.getMessage(), e);
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                    "Exception occurred connecting to brain: " + e.getMessage());
        } catch (InterruptedException e) {
            logger.debug("Initializtion was interrupted", e);
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
                    "Initialization was interrupted");
        } finally {
            writerLock.unlock();
        }
    }

    /**
     * Helper method to add a property to the properties map if the value is not null
     *
     * @param properties a non-null properties map
     * @param key a non-null, non-empty key
     * @param value a possibly null, possibly empty key
     */
    private void addProperty(Map<String, String> properties, String key, @Nullable String value) {
        Objects.requireNonNull(properties, "properties cannot be null");
        NeeoUtil.requireNotEmpty(key, "key cannot be empty");
        if (value != null && StringUtils.isNotEmpty(value)) {
            properties.put(key, value);
        }
    }

    /**
     * Gets the {@link NeeoBrainApi} used by this bridge
     *
     * @return a possibly null {@link NeeoBrainApi}
     */
    @Nullable
    public NeeoBrainApi getNeeoBrainApi() {
        final Lock readerLock = stateLock.readLock();
        readerLock.lock();
        try {
            return neeoBrainApi;
        } finally {
            readerLock.unlock();
        }
    }

    /**
     * Gets the brain id used by this bridge
     *
     * @return a non-null, non-empty brain id
     */
    public String getNeeoBrainId() {
        return getThing().getUID().getId();
    }

    /**
     * Helper method to get the {@link NeeoBrainConfig}
     *
     * @return the {@link NeeoBrainConfig}
     */
    private NeeoBrainConfig getBrainConfig() {
        return getConfigAs(NeeoBrainConfig.class);
    }

    /**
     * Checks the status of the brain via a quick socket connection. If the status is unavailable and we are
     * {@link ThingStatus#ONLINE}, then we go {@link ThingStatus#OFFLINE}. If the status is available and we are
     * {@link ThingStatus#OFFLINE}, we go {@link ThingStatus#ONLINE}.
     *
     * @param ipAddress a non-null, non-empty IP address
     */
    private void checkStatus(String ipAddress) {
        NeeoUtil.requireNotEmpty(ipAddress, "ipAddress cannot be empty");

        try {
            try (Socket soc = new Socket()) {
                soc.connect(new InetSocketAddress(ipAddress, NeeoConstants.DEFAULT_BRAIN_PORT), 5000);
            }
            logger.debug("Checking connectivity to {}:{} - successful", ipAddress,
                    NeeoConstants.DEFAULT_BRAIN_PORT);

            if (getThing().getStatus() != ThingStatus.ONLINE) {
                updateStatus(ThingStatus.ONLINE);
            }
        } catch (IOException e) {
            if (getThing().getStatus() == ThingStatus.ONLINE) {
                logger.debug("Checking connectivity to {}:{} - unsuccessful - going offline: {}", ipAddress,
                        NeeoConstants.DEFAULT_BRAIN_PORT, e.getMessage(), e);
                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                        "Exception occurred connecting to brain: " + e.getMessage());
            } else {
                logger.debug("Checking connectivity to {}:{} - unsuccessful - still offline", ipAddress,
                        NeeoConstants.DEFAULT_BRAIN_PORT);
            }
        }
    }

    /**
     * Disposes of the bridge by closing/removing the {@link #neeoBrainApi} and canceling/removing any pending
     * {@link #initializeTask()}
     */
    @Override
    public void dispose() {
        final Lock writerLock = stateLock.writeLock();
        writerLock.lock();
        try {
            final NeeoBrainApi api = neeoBrainApi;
            neeoBrainApi = null;

            NeeoUtil.cancel(initializationTask.getAndSet(null));
            NeeoUtil.cancel(checkStatus.getAndSet(null));

            if (forwardActionServlet != null) {
                forwardActionServlet = null;

                if (api != null) {
                    try {
                        api.deregisterForwardActions();
                    } catch (IOException e) {
                        logger.debug("IOException occurred deregistering the forward actions: {}", e.getMessage(),
                                e);
                    }
                }

                if (servletPath != null) {
                    httpService.unregister(servletPath);
                    servletPath = null;
                }
            }

            NeeoUtil.close(api);
        } finally {
            writerLock.unlock();
        }
    }

    /**
     * Creates the URL the brain should callback. Note: if there is multiple interfaces, we try to prefer the one on the
     * same subnet as the brain
     *
     * @param brainId the non-null, non-empty brain identifier
     * @param config the non-null brain configuration
     * @return the callback URL
     * @throws MalformedURLException if the URL is malformed
     */
    @Nullable
    private URL createCallbackUrl(String brainId, NeeoBrainConfig config) throws MalformedURLException {
        NeeoUtil.requireNotEmpty(brainId, "brainId cannot be empty");
        Objects.requireNonNull(config, "config cannot be null");

        final String ipAddress = networkAddressService.getPrimaryIpv4HostAddress();
        if (ipAddress == null) {
            logger.debug("No network interface could be found.");
            return null;
        }

        return new URL("http://" + ipAddress + ":" + servicePort);
    }
}