org.openhab.binding.unifi.internal.handler.UniFiControllerThingHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.binding.unifi.internal.handler.UniFiControllerThingHandler.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.unifi.internal.handler;

import static org.eclipse.smarthome.core.thing.ThingStatus.OFFLINE;
import static org.eclipse.smarthome.core.thing.ThingStatus.ONLINE;
import static org.eclipse.smarthome.core.thing.ThingStatusDetail.*;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.smarthome.core.thing.Bridge;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.ThingStatus;
import org.eclipse.smarthome.core.thing.ThingStatusDetail;
import org.eclipse.smarthome.core.thing.ThingStatusInfo;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler;
import org.eclipse.smarthome.core.thing.binding.builder.ThingStatusInfoBuilder;
import org.eclipse.smarthome.core.types.Command;
import org.openhab.binding.unifi.internal.UniFiBindingConstants;
import org.openhab.binding.unifi.internal.UniFiControllerThingConfig;
import org.openhab.binding.unifi.internal.api.UniFiCommunicationException;
import org.openhab.binding.unifi.internal.api.UniFiController;
import org.openhab.binding.unifi.internal.api.UniFiException;
import org.openhab.binding.unifi.internal.api.UniFiInvalidCredentialsException;
import org.openhab.binding.unifi.internal.api.UniFiInvalidHostException;
import org.openhab.binding.unifi.internal.api.UniFiSSLException;
import org.openhab.binding.unifi.internal.api.model.UniFiClient;
import org.openhab.binding.unifi.internal.api.model.UniFiDevice;
import org.openhab.binding.unifi.internal.api.model.UniFiSite;
import org.openhab.binding.unifi.internal.api.model.UniFiWirelessClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The {@link UniFiControllerThingHandler} is responsible for handling commands and status
 * updates for the UniFi Controller.
 *
 * @author Matthew Bowman - Initial contribution
 */
@NonNullByDefault
public class UniFiControllerThingHandler extends BaseBridgeHandler {

    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
            .of(UniFiBindingConstants.THING_TYPE_CONTROLLER).collect(Collectors.toSet());

    private static final String STATUS_DESCRIPTION_COMMUNICATION_ERROR = "Error communicating with the UniFi controller";

    private static final String STATUS_DESCRIPTION_SSL_ERROR = "Error establishing an SSL connection with the UniFi controller";

    private static final String STATUS_DESCRIPTION_INVALID_CREDENTIALS = "Invalid username and/or password - please double-check your configuration";

    private static final String STATUS_DESCRIPTION_INVALID_HOSTNAME = "Invalid hostname - please double-check your configuration";

    private static final String CACHE_KEY_PREFIX_MAC = "mac";

    private static final String CACHE_KEY_PREFIX_IP = "ip";

    private static final String CACHE_KEY_PREFIX_HOSTNAME = "hostname";

    private static final String CACHE_KEY_PREFIX_ALIAS = "alias";

    private static final List<String> CACHE_KEY_PREFIXES = Arrays.asList(CACHE_KEY_PREFIX_MAC, CACHE_KEY_PREFIX_IP,
            CACHE_KEY_PREFIX_HOSTNAME, CACHE_KEY_PREFIX_ALIAS);

    private static final String CACHE_KEY_SEPARATOR = ":";

    private final Logger logger = LoggerFactory.getLogger(UniFiControllerThingHandler.class);

    private @Nullable UniFiControllerThingConfig config;

    private @Nullable volatile UniFiController controller; /* mgb: volatile because accessed from multiple threads */

    private @Nullable ScheduledFuture<?> refreshJob;

    private Map<String, UniFiSite> sitesCache = Collections.emptyMap();

    private Map<String, UniFiDevice> devicesCache = Collections.emptyMap();

    private Map<String, UniFiClient> clientsCache = Collections.emptyMap();

    private Map<String, UniFiClient> insightsCache = Collections.emptyMap();

    private final HttpClient httpClient;

    public UniFiControllerThingHandler(Bridge bridge, HttpClient httpClient) {
        super(bridge);
        this.httpClient = httpClient;
    }

    // Public API

    @Override
    public void initialize() {
        // mgb: called when the config changes
        cancelRefreshJob();
        config = getConfig().as(UniFiControllerThingConfig.class);
        logger.debug("Initializing the UniFi Controller Handler with config = {}", config);
        try {
            controller = new UniFiController(httpClient, config.getHost(), config.getPort(), config.getUsername(),
                    config.getPassword());
            controller.start();
            updateStatus(ONLINE);
        } catch (UniFiInvalidHostException e) {
            updateStatus(OFFLINE, CONFIGURATION_ERROR, STATUS_DESCRIPTION_INVALID_HOSTNAME);
        } catch (UniFiCommunicationException e) {
            updateStatus(OFFLINE, COMMUNICATION_ERROR, STATUS_DESCRIPTION_COMMUNICATION_ERROR);
        } catch (UniFiSSLException e) {
            updateStatus(OFFLINE, CONFIGURATION_ERROR, STATUS_DESCRIPTION_SSL_ERROR);
        } catch (UniFiInvalidCredentialsException e) {
            updateStatus(OFFLINE, CONFIGURATION_ERROR, STATUS_DESCRIPTION_INVALID_CREDENTIALS);
        } catch (UniFiException e) {
            logger.error("Unknown error while configuring the UniFi Controller", e);
            updateStatus(OFFLINE, CONFIGURATION_ERROR, e.getMessage());
        }
    }

    @Override
    protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
        if (status == ONLINE || (status == OFFLINE && statusDetail == COMMUNICATION_ERROR)) {
            scheduleRefreshJob();
        } else if (status == OFFLINE && statusDetail == CONFIGURATION_ERROR) {
            cancelRefreshJob();
        }
        // mgb: update the status only if it's changed
        ThingStatusInfo statusInfo = ThingStatusInfoBuilder.create(status, statusDetail)
                .withDescription(description).build();
        if (!statusInfo.equals(getThing().getStatusInfo())) {
            super.updateStatus(status, statusDetail, description);
        }
    }

    @Override
    public void dispose() {
        cancelRefreshJob();
        if (controller != null) {
            try {
                controller.stop();
            } catch (UniFiException e) {
                // mgb: nop as we're in dispose
            }
            controller = null;
        }
    }

    @Override
    public void handleCommand(ChannelUID channelUID, Command command) {
        // nop - read-only binding
        logger.debug("Ignoring command = {} for channel = {} - the UniFi binding is read-only!", command,
                channelUID);
    }

    public int getRefreshInterval() {
        return config.getRefresh();
    }

    private void cachePut(Map<String, UniFiClient> cache, UniFiClient client) {
        synchronized (cache) {
            for (String prefix : CACHE_KEY_PREFIXES) {
                String suffix = null;
                switch (prefix) {
                case CACHE_KEY_PREFIX_MAC:
                    suffix = client.getMac();
                    break;
                case CACHE_KEY_PREFIX_IP:
                    suffix = client.getIp();
                    break;
                case CACHE_KEY_PREFIX_HOSTNAME:
                    suffix = client.getHostname();
                    break;
                case CACHE_KEY_PREFIX_ALIAS:
                    suffix = client.getAlias();
                    break;
                }
                if (StringUtils.isNotBlank(suffix)) {
                    String key = prefix + CACHE_KEY_SEPARATOR + suffix;
                    cache.put(key, client);
                }
            }
        }
    }

    private @Nullable UniFiClient cacheGet(Map<String, UniFiClient> cache, String cid) {
        UniFiClient client = null;
        synchronized (cache) {
            for (String prefix : CACHE_KEY_PREFIXES) {
                String key = prefix + CACHE_KEY_SEPARATOR + cid;
                if (cache.containsKey(key)) {
                    client = cache.get(key);
                    logger.debug("Found client '{}' = {}", key, client);
                    break;
                }
            }
        }
        return client;
    }

    public @Nullable UniFiClient getClient(String cid, String site) {
        // mgb: first check active clients and fallback to insights if not found
        UniFiClient client = null;

        // mgb: first check active clients and fallback to insights if not found
        client = cacheGet(clientsCache, cid);
        if (client == null) {
            client = cacheGet(insightsCache, cid);
        }

        // mgb: short circuit
        if (client == null || BooleanUtils.isNotTrue(client.isWireless()) || !belongsToSite(client, site)) {
            return null;
        }

        // mgb: instanceof check just for type / cast safety
        return (client instanceof UniFiWirelessClient ? (UniFiWirelessClient) client : null);
    }

    // Private API

    private void scheduleRefreshJob() {
        synchronized (this) {
            if (refreshJob == null) {
                logger.debug("Scheduling refresh job every {}s", config.getRefresh());
                refreshJob = scheduler.scheduleWithFixedDelay(this::run, 0, config.getRefresh(), TimeUnit.SECONDS);
            }
        }
    }

    private void cancelRefreshJob() {
        synchronized (this) {
            if (refreshJob != null) {
                logger.debug("Cancelling refresh job");
                refreshJob.cancel(true);
                refreshJob = null;
            }
        }
    }

    private void run() {
        try {
            logger.trace("Executing refresh job");
            refresh();
            updateStatus(ONLINE);
        } catch (UniFiCommunicationException e) {
            updateStatus(OFFLINE, COMMUNICATION_ERROR, STATUS_DESCRIPTION_COMMUNICATION_ERROR);
        } catch (UniFiInvalidCredentialsException e) {
            updateStatus(OFFLINE, CONFIGURATION_ERROR, STATUS_DESCRIPTION_INVALID_CREDENTIALS);
        } catch (Exception e) {
            logger.warn("Unhandled exception while refreshing the UniFi Controller {} - {}", getThing().getUID(),
                    e.getMessage());
            updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getMessage());
        }
    }

    private void refresh() throws UniFiException {
        if (controller != null) {
            logger.debug("Refreshing the UniFi Controller {}", getThing().getUID());
            // mgb: refresh the controller thing
            synchronized (this) {
                sitesCache = getSites();
                devicesCache = getDevices();
                clientsCache = getClients();
                insightsCache = getInsights();
            }
            // mgb: then refresh all the client things
            getThing().getThings().forEach((thing) -> {
                if (thing.getHandler() instanceof UniFiClientThingHandler) {
                    ((UniFiClientThingHandler) thing.getHandler()).refresh();
                }
            });
        }
    }

    private Map<String, UniFiSite> getSites() throws UniFiException {
        Map<String, UniFiSite> siteMap = new HashMap<>();
        UniFiSite[] sites = controller.getSites();
        logger.debug("Found {} UniFi Site(s): {}", sites.length, lazyFormatAsList(sites));
        for (UniFiSite site : sites) {
            siteMap.put(site.getId(), site);
        }
        return siteMap;
    }

    private Map<String, UniFiDevice> getDevices() throws UniFiException {
        Map<String, UniFiDevice> deviceMap = new HashMap<>();
        Collection<UniFiSite> sites = sitesCache.values();
        for (UniFiSite site : sites) {
            Map<String, UniFiDevice> devices = getDevices(site);
            deviceMap.putAll(devices);
        }
        return deviceMap;
    }

    private Map<String, UniFiDevice> getDevices(UniFiSite site) throws UniFiException {
        Map<String, UniFiDevice> deviceMap = new HashMap<>();
        UniFiDevice[] devices = controller.getDevices(site);
        logger.debug("Found {} UniFi Device(s): {}", devices.length, lazyFormatAsList(devices));
        for (UniFiDevice device : devices) {
            device.setSite(site);
            deviceMap.put(device.getMac(), device);
        }
        return deviceMap;
    }

    private Map<String, UniFiClient> getClients() throws UniFiException {
        Map<String, UniFiClient> clientMap = new HashMap<>();
        Collection<UniFiSite> sites = sitesCache.values();
        for (UniFiSite site : sites) {
            Map<String, UniFiClient> siteClientMap = getClients(site);
            clientMap.putAll(siteClientMap);
        }
        return clientMap;
    }

    private Map<String, UniFiClient> getClients(UniFiSite site) throws UniFiException {
        Map<String, UniFiClient> clientMap = new HashMap<>();
        UniFiClient[] clients = controller.getClients(site);
        logger.debug("Found {} UniFi Client(s): {}", clients.length, lazyFormatAsList(clients));
        for (UniFiClient client : clients) {
            client.setDevice(devicesCache.get(client.getDeviceMac()));
            cachePut(clientMap, client);
        }
        return clientMap;
    }

    private Map<String, UniFiClient> getInsights() throws UniFiException {
        Map<String, UniFiClient> insightsMap = new HashMap<>();
        Collection<UniFiSite> sites = sitesCache.values();
        for (UniFiSite site : sites) {
            Map<String, UniFiClient> siteInsightsMap = getInsights(site);
            insightsMap.putAll(siteInsightsMap);
        }
        return insightsMap;
    }

    private Map<String, UniFiClient> getInsights(UniFiSite site) throws UniFiException {
        Map<String, UniFiClient> insightsMap = new HashMap<>();
        UniFiClient[] clients = controller.getInsights(site);
        logger.debug("Found {} UniFi Insights(s): {}", clients.length, lazyFormatAsList(clients));
        for (UniFiClient client : clients) {
            cachePut(insightsMap, client);
        }
        return insightsMap;
    }

    private boolean belongsToSite(UniFiClient client, String siteName) {
        boolean result = true; // mgb: assume true = proof by contradiction
        if (StringUtils.isNotEmpty(siteName)) {
            UniFiSite site = sitesCache.get(client.getSiteId());
            // mgb: if the 'site' can't be found or the name doesn't match...
            if (site == null || !site.matchesName(siteName)) {
                // mgb: ... then the client doesn't belong to this thing's configured 'site' and we 'filter' it
                result = false;
            }
        }
        return result;
    }

    private static Object lazyFormatAsList(Object[] arr) {
        return new Object() {

            @Override
            public String toString() {
                String value = "";
                for (Object o : arr) {
                    value += "\n - " + o.toString();
                }
                return value;
            }

        };
    }

}