org.openhab.binding.unifi.internal.UnifiBinding.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.binding.unifi.internal.UnifiBinding.java

Source

/**
 * Copyright (c) 2010-2015, openHAB.org and others.
 * <p>
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.openhab.binding.unifi.internal;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.unifi.UnifiBindingProvider;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.*;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Map;

/**
 * Implement this class if you are going create an actively polling service
 * like querying a Website/Device.
 *
 * @author Ondrej Pecta
 * @since 1.9.0
 */
public class UnifiBinding extends AbstractActiveBinding<UnifiBindingProvider> {

    private static final Logger logger = LoggerFactory.getLogger(UnifiBinding.class);
    private static final String LED = "led";
    private static final String REBOOT = "reboot";
    private static final String BLINK = "blink";
    private static final String DISABLE_AP = "disable_ap";
    private static final String ENABLE_WLAN = "enable_wlan";
    private static final String PASSWORD = "password";
    private static final String CHANGE_PASSWORD = "change_password";

    //generate password
    static final String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    static SecureRandom rnd = new SecureRandom();

    /**
     * The BundleContext. This is only valid when the bundle is ACTIVE. It is set in the activate()
     * method and must not be accessed anymore once the deactivate() method was called or before activate()
     * was called.
     */
    private BundleContext bundleContext;
    private ItemRegistry itemRegistry;

    /**
     * the refresh interval which is used to poll values from the Unifi
     * server (optional, defaults to 60000ms)
     */
    private long refreshInterval = 60000;
    private String controllerIP = "";
    private String controllerPort = "";
    private String username = "";
    private String password = "";

    private SSLContext sc;

    //Gson parser
    private JsonParser parser = new JsonParser();

    public UnifiBinding() {
    }

    ArrayList<String> cookies = new ArrayList<>();
    ArrayList<String> aps = new ArrayList<>();

    /**
     * Called by the SCR to activate the component with its configuration read from CAS
     *
     * @param bundleContext BundleContext of the Bundle that defines this component
     * @param configuration Configuration properties for this component obtained from the ConfigAdmin service
     */
    public void activate(final BundleContext bundleContext, final Map<String, Object> configuration) {
        this.bundleContext = bundleContext;

        // the configuration is guaranteed not to be null, because the component definition has the
        // configuration-policy set to require. If set to 'optional' then the configuration may be null

        // to override the default refresh interval one has to add a
        // parameter to openhab.cfg like <bindingName>:refresh=<intervalInMs>
        readConfiguration(configuration);

        try {
            sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new java.security.SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

            // Create all-trusting host name verifier
            HostnameVerifier allHostsValid = new HostnameVerifier() {
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            };

            // Install the all-trusting host verifier
            HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
        } catch (Exception e) {
            logger.error("Cannot initialize SSL Context!" + e.toString());
            setProperlyConfigured(false);
            return;
        }

        setProperlyConfigured(true);
    }

    private void readConfiguration(Map<String, Object> configuration) {
        String refreshIntervalString = (String) configuration.get("refresh");
        if (StringUtils.isNotBlank(refreshIntervalString)) {
            refreshInterval = Long.parseLong(refreshIntervalString);
        }

        String controllerIPString = (String) configuration.get("controllerIP");
        if (StringUtils.isNotBlank(controllerIPString)) {
            controllerIP = controllerIPString;
        }

        String controllerPortString = (String) configuration.get("controllerPort");
        if (StringUtils.isNotBlank(controllerPortString)) {
            controllerPort = controllerPortString;
        }

        String usernameString = (String) configuration.get("username");
        if (StringUtils.isNotBlank(usernameString)) {
            username = usernameString;
        }

        String passwordString = (String) configuration.get("password");
        if (StringUtils.isNotBlank(passwordString)) {
            password = passwordString;
        }
    }

    private void enableWlan(String wlanId, boolean enable) {
        String url = getControllerUrl("api/s/default/rest/wlanconf/" + wlanId);
        String response = sendToController(url, "{'enabled': " + enable + "}", "PUT");
        logger.debug(response);
    }

    private void changePassword(String wlanId, String newPassword) {
        String url = getControllerUrl("api/s/default/rest/wlanconf/" + wlanId);
        String response = sendToController(url, "{'x_passphrase': '" + newPassword + "'}", "PUT");
        logger.debug(response);
    }

    private void discoverAPs() {
        aps.clear();
        String url = getControllerUrl("api/s/default/stat/device");
        String response = sendToController(url, "");

        if (response.equals(""))
            return;
        logger.debug(response);

        JsonObject jobject = parser.parse(response).getAsJsonObject();
        String res = jobject.get("meta").getAsJsonObject().get("rc").getAsString();
        if (res.equals("ok")) {
            JsonArray jarray = jobject.get("data").getAsJsonArray();
            logger.info("Detected " + jarray.size() + " unifi APs");
            for (JsonElement je : jarray) {
                jobject = je.getAsJsonObject();
                String _id = jobject.get("_id").getAsString();
                String mac = jobject.get("mac").getAsString();
                StringBuilder sb = new StringBuilder();
                sb.append("Unifi AP with id: " + _id + " MAC: " + mac);
                JsonArray vaparray = jobject.get("vap_table").getAsJsonArray();
                sb.append(" has " + vaparray.size() + " wifi networks:");
                for (JsonElement vapel : vaparray) {
                    String name = vapel.getAsJsonObject().get("name").getAsString();
                    String ssid = vapel.getAsJsonObject().get("essid").getAsString();
                    String id = vapel.getAsJsonObject().get("id").getAsString();
                    String radio = vapel.getAsJsonObject().get("radio").getAsString();

                    boolean guest = vapel.getAsJsonObject().get("is_guest").getAsBoolean();
                    sb.append("\n\t SSID: " + ssid);
                    sb.append(" name: " + name);
                    sb.append(" id: " + id);
                    sb.append(" radio: " + radio);
                    if (guest) {
                        sb.append(" (GUEST)");
                    }
                    aps.add(id);
                }
                logger.info(sb.toString());
            }
        }
    }

    private boolean login() {
        String url = null;

        try {
            url = getControllerUrl("api/login");
            String urlParameters = "{'username':'" + username + "','password':'" + password + "'}";
            byte[] postData = urlParameters.getBytes(StandardCharsets.UTF_8);

            URL cookieUrl = new URL(url);
            HttpsURLConnection connection = (HttpsURLConnection) cookieUrl.openConnection();
            connection.setDoOutput(true);
            connection.setInstanceFollowRedirects(true);
            connection.setRequestMethod("POST");
            connection.setRequestProperty("Referer", getControllerUrl("login"));
            connection.setRequestProperty("Content-Length", Integer.toString(postData.length));
            connection.setUseCaches(false);

            try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
                wr.write(postData);
            }

            //get cookie
            cookies.clear();
            String headerName;
            for (int i = 1; (headerName = connection.getHeaderFieldKey(i)) != null; i++) {
                if (headerName.equals("Set-Cookie")) {
                    cookies.add(connection.getHeaderField(i));
                }
            }

            InputStream response = connection.getInputStream();
            String line = readResponse(response);
            logger.debug("Unifi response: " + line);
            return checkResponse(line);

        } catch (MalformedURLException e) {
            logger.error("The URL '" + url + "' is malformed: " + e.toString());
        } catch (Exception e) {
            logger.error("Cannot get Ubiquiti Unifi login cookie: " + e.toString());
        }
        return false;
    }

    private boolean checkResponse(String line) {

        try {
            JsonObject jobject = parser.parse(line).getAsJsonObject();
            if (jobject != null) {
                jobject = jobject.get("meta").getAsJsonObject();
                return jobject.get("rc").getAsString().equals("ok");
            } else {
                return false;
            }
        } catch (Exception ex) {
            return false;
        }
    }

    TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
        }

        public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
        }
    } };

    private String readResponse(InputStream response) throws Exception {
        String line;
        StringBuilder body = new StringBuilder();
        BufferedReader reader = new BufferedReader(new InputStreamReader(response));

        while ((line = reader.readLine()) != null) {
            body.append(line).append("\n");
        }
        line = body.toString();
        logger.debug(line);
        return line;
    }

    private String getControllerUrl(String site) {
        return "https://" + controllerIP + ":" + controllerPort + "/" + site;
    }

    /**
     * Called by the SCR when the configuration of a binding has been changed through the ConfigAdmin service.
     *
     * @param configuration Updated configuration properties
     */
    public void modified(final Map<String, Object> configuration) {
        // update the internal configuration accordingly
    }

    /**
     * Called by the SCR to deactivate the component when either the configuration is removed or
     * mandatory references are no longer satisfied or the component has simply been stopped.
     *
     * @param reason Reason code for the deactivation:<br>
     *               <ul>
     *               <li> 0  Unspecified
     *               <li> 1  The component was disabled
     *               <li> 2  A reference became unsatisfied
     *               <li> 3  A configuration was changed
     *               <li> 4  A configuration was deleted
     *               <li> 5  The component was disposed
     *               <li> 6  The bundle was stopped
     *               </ul>
     */
    public void deactivate(final int reason) {
        this.bundleContext = null;
        // deallocate resources here that are no longer needed and
        // should be reset when activating this binding again

        logout();
        cookies.clear();
    }

    private void logout() {
        URL url = null;
        if (cookies.size() == 0)
            return;

        try {
            url = new URL(getControllerUrl("api/logout"));
            HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
            connection.setInstanceFollowRedirects(true);
            connection.setRequestMethod("POST");
            connection.setRequestProperty("Cookie", cookies.get(0) + "; " + cookies.get(1));
            connection.getInputStream();
        } catch (MalformedURLException e) {
            logger.error("The URL '" + url + "' is malformed: " + e.toString());
        } catch (Exception e) {
            logger.error("Cannot do logout. Exception: " + e.toString());
        }
    }

    public void setItemRegistry(ItemRegistry itemRegistry) {
        this.itemRegistry = itemRegistry;
    }

    public void unsetItemRegistry(ItemRegistry itemRegistry) {
        this.itemRegistry = null;
    }

    /**
     * @{inheritDoc}
     */
    @Override
    protected long getRefreshInterval() {
        return refreshInterval;
    }

    /**
     * @{inheritDoc}
     */
    @Override
    protected String getName() {
        return "Unifi Refresh Service";
    }

    /**
     * @{inheritDoc}
     */
    @Override
    protected void execute() {
        // the frequently executed code (polling) goes here ...
        logger.debug("execute() method is called!");

        if (!bindingsExist())
            return;

        synchronized (cookies) {
            logout();
            login();
        }

        if (aps.size() == 0) {
            discoverAPs();
        }

        for (final UnifiBindingProvider provider : providers) {
            for (final String itemName : provider.getItemNames()) {
                if (provider.getItemType(itemName).equals(LED)) {
                    readLedStatus(itemName);
                }
                if (provider.getItemType(itemName).equals(ENABLE_WLAN)) {
                    readWlanStatus(itemName, provider.getItemId(itemName));
                }
                if (provider.getItemType(itemName).equals(PASSWORD)) {
                    readWlanPassword(itemName, provider.getItemId(itemName));
                }
            }

        }
    }

    private void readWlanStatus(String itemName, String id) {
        String url = getControllerUrl("api/s/default/rest/wlanconf/" + id);
        String response = sendToController(url, "", "GET");
        logger.debug(response);
        if (checkResponse(response)) {
            JsonObject jobject = parser.parse(response).getAsJsonObject();
            if (jobject != null) {
                JsonArray jarray = jobject.getAsJsonArray("data");
                if (jarray.size() == 0) {
                    logger.error("Cannot read wlan status for id: " + id);
                    return;
                }
                jobject = jarray.get(0).getAsJsonObject();
                boolean enabled = jobject.get("enabled").getAsBoolean();
                State newVal = enabled ? OnOffType.ON : OnOffType.OFF;
                State oldVal;
                try {
                    oldVal = itemRegistry.getItem(itemName).getState();
                    if (!newVal.equals(oldVal))
                        eventPublisher.postUpdate(itemName, newVal);
                } catch (ItemNotFoundException e) {
                    logger.error("Cannot find item " + itemName + " in item registry!");
                }
            } else {
                logger.error("Cannot parse JSON response!");
            }
        }
    }

    private void readWlanPassword(String itemName, String id) {
        String url = getControllerUrl("api/s/default/rest/wlanconf/" + id);
        String response = sendToController(url, "", "GET");
        logger.debug(response);
        if (checkResponse(response)) {
            JsonObject jobject = parser.parse(response).getAsJsonObject();
            if (jobject != null) {
                JsonArray jarray = jobject.getAsJsonArray("data");
                if (jarray.size() == 0) {
                    logger.error("Cannot read wlan password for id: " + id);
                    return;
                }
                jobject = jarray.get(0).getAsJsonObject();
                String password = jobject.get("x_passphrase").getAsString();
                State newVal = new StringType(password);
                State oldVal;
                try {
                    oldVal = itemRegistry.getItem(itemName).getState();
                    if (!newVal.equals(oldVal))
                        eventPublisher.postUpdate(itemName, newVal);
                } catch (ItemNotFoundException e) {
                    logger.error("Cannot find item " + itemName + " in item registry!");
                }
            } else {
                logger.error("Cannot parse JSON response!");
            }
        }
    }

    private void readLedStatus(String itemName) {
        String url = getControllerUrl("api/s/default/set/setting/mgmt");
        String response = sendToController(url, "");
        logger.debug(response);
        if (checkResponse(response)) {
            JsonObject jobject = parser.parse(response).getAsJsonObject();
            if (jobject != null) {
                JsonArray jarray = jobject.getAsJsonArray("data");
                if (jarray.size() == 0) {
                    logger.error("Cannot read led status for item: " + itemName);
                    return;
                }
                jobject = jarray.get(0).getAsJsonObject();
                boolean enabled = jobject.get("led_enabled").getAsBoolean();
                State newVal = enabled ? OnOffType.ON : OnOffType.OFF;
                State oldVal;
                try {
                    oldVal = itemRegistry.getItem(itemName).getState();
                    if (!newVal.equals(oldVal))
                        eventPublisher.postUpdate(itemName, newVal);
                } catch (ItemNotFoundException e) {
                    logger.error("Cannot find item " + itemName + " in item registry!");
                }
            } else {
                logger.error("Cannot parse JSON response!");
            }
        }

    }

    /**
     * @{inheritDoc}
     */
    @Override
    protected void internalReceiveCommand(String itemName, Command command) {
        // the code being executed when a command was sent on the openHAB
        // event bus goes here. This method is only called if one of the
        // BindingProviders provide a binding for the given 'itemName'.
        logger.debug("internalReceiveCommand({},{}) is called!", itemName, command);
        UnifiGenericBindingProvider.UnifiBindingConfig config = getUnifiConfig(itemName);
        if (config == null)
            return;

        String type = config.getType();
        switch (type) {
        case LED:
            switchLed(command.equals(OnOffType.ON));
            break;
        case REBOOT:
            if (command.equals(OnOffType.ON))
                rebootAP(config.getId());
            break;
        case BLINK:
            blinkLed(config.getId(), command.equals(OnOffType.ON));
            break;
        case DISABLE_AP:
            disableAP(config.getId(), command.equals(OnOffType.ON));
            break;
        case ENABLE_WLAN:
            enableWlan(config.getId(), command.equals(OnOffType.ON));
            break;
        case CHANGE_PASSWORD:
            if (command.equals(OnOffType.ON)) {
                changePassword(config.getId(), generatePassword(10));
            }
            break;
        default:
            logger.error("Unknown Unifi type: " + type + " for item " + itemName);
        }

        /*
        if (type.equals(LED)) {
        switchLed(command.toString().equals("ON"));
        }
        else if (type.equals(REBOOT)) {
        if( command.toString().equals("ON") )
            reboot(config.getId());
        } else
        //
        if (type.equals(BLINK)) {
            blinkLed(config.getId(), command.toString().equals("ON"));
        } else {
            logger.error("Unknown Unifi type: " + type + " for item " + itemName);
        }
        */
    }

    private String generatePassword(int len) {
        StringBuilder sb = new StringBuilder(len);
        for (int i = 0; i < len; i++)
            sb.append(AB.charAt(rnd.nextInt(AB.length())));
        return sb.toString();
    }

    private void disableAP(String id, boolean disable) {
        String url = getControllerUrl("api/s/default/rest/device/" + id);
        String urlParameters = "{'disabled':" + disable + "}";
        sendToController(url, urlParameters, "PUT");
    }

    private void rebootAP(String mac) {
        String url = getControllerUrl("api/s/default/cmd/devmgr");
        String urlParameters = "json={'cmd':'restart', 'mac':'" + mac + "'}";
        sendToController(url, urlParameters);
    }

    private void blinkLed(String mac, boolean value) {
        String url = getControllerUrl("api/s/default/cmd/devmgr");
        String urlParameters = "json={'cmd':'" + (value ? "" : "un") + "set-locate', 'mac':'" + mac + "'}";
        sendToController(url, urlParameters);
    }

    private void switchLed(boolean value) {
        String url = getControllerUrl("api/s/default/set/setting/mgmt");
        String urlParameters = "json={'led_enabled':" + value + "}";
        sendToController(url, urlParameters);
    }

    private String sendToController(String url, String urlParameters) {
        return sendToController(url, urlParameters, "POST");
    }

    private String sendToController(String url, String urlParameters, String method) {
        try {
            synchronized (cookies) {
                byte[] postData = urlParameters.getBytes(StandardCharsets.UTF_8);

                URL cookieUrl = new URL(url);
                HttpsURLConnection connection = (HttpsURLConnection) cookieUrl.openConnection();

                connection.setInstanceFollowRedirects(true);
                connection.setRequestMethod(method);
                //for(String cookie : cookies) {
                connection.setRequestProperty("Cookie", cookies.get(0) + "; " + cookies.get(1));
                //}

                if (urlParameters.length() > 0) {
                    connection.setDoOutput(true);
                    connection.setRequestProperty("Content-Length", Integer.toString(postData.length));
                    connection.setUseCaches(false);

                    try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
                        wr.write(postData);
                    }
                }

                InputStream response = connection.getInputStream();
                String line = readResponse(response);
                if (!checkResponse(line)) {
                    logger.error("Unifi response: " + line);

                }
                return line;
            }
        } catch (MalformedURLException e) {
            logger.error("The URL '" + url + "' is malformed: " + e.toString());
        } catch (Exception e) {
            logger.error("Cannot send data " + urlParameters + " to url " + url + ". Exception: " + e.toString());
        }
        return "";
    }

    private UnifiGenericBindingProvider.UnifiBindingConfig getUnifiConfig(String itemName) {
        for (final UnifiBindingProvider provider : providers) {
            for (final String name : provider.getItemNames()) {
                if (itemName.equals(name)) {
                    return (UnifiGenericBindingProvider.UnifiBindingConfig) provider.getItemConfig(itemName);
                }
            }
        }
        return null;
    }

    /**
     * @{inheritDoc}
     */
    @Override
    protected void internalReceiveUpdate(String itemName, State newState) {
        // the code being executed when a state was sent on the openHAB
        // event bus goes here. This method is only called if one of the
        // BindingProviders provide a binding for the given 'itemName'.
        logger.debug("internalReceiveUpdate({},{}) is called!", itemName, newState);
    }

}