org.openhab.binding.zonky.internal.ZonkyBinding.java Source code

Java tutorial

Introduction

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

import com.google.gson.Gson;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.zonky.ZonkyBindingProvider;
import org.openhab.binding.zonky.internal.model.*;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.library.types.DecimalType;
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.HttpsURLConnection;
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.util.Calendar;
import java.util.Date;
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 ZonkyBinding extends AbstractActiveBinding<ZonkyBindingProvider> {

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

    //constants
    private final String ZONKY_URL = "https://api.zonky.cz/";
    private final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36";

    /**
     * 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 Zonky
     * server (optional, defaults to 60000ms)
     */
    private long refreshInterval = 240000;

    private String userName = "";
    private String password = "";
    private String token = "";
    private String refreshToken = "";

    private int lastHours;

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

    public ZonkyBinding() {
    }

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

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

    /**
     * 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

        readConfiguration(configuration);
        setProperlyConfigured(!userName.isEmpty() && !password.isEmpty());
    }

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

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

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

    }

    /**
     * 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
        if (configuration != null) {
            readConfiguration(configuration);
            setProperlyConfigured(!userName.isEmpty() && !password.isEmpty());
        }
    }

    /**
     * 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
        if (!token.isEmpty()) {
            logout();
        }
    }

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

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

    /**
     * @{inheritDoc}
     */
    @Override
    protected void execute() {
        // the frequently executed code (polling) goes here ...
        if (!bindingsExist()) {
            return;
        }
        //first call
        if (token.isEmpty()) {
            login();
        } else {
            // relogin every hour
            Calendar cal = Calendar.getInstance();
            cal.setTime(new Date());
            int hours = cal.get(Calendar.HOUR_OF_DAY);
            if (lastHours >= 0 && lastHours != hours) {
                if (!refreshToken()) {
                    token = "";
                    login();
                }
            }
        }

        if (token.isEmpty())
            return;

        String wallet = getWallet();
        String statistics = getStatistics();
        String weekly = getWeeklyStatistics();
        if (wallet == null || statistics == null || weekly == null) {
            return;
        }

        ZonkyWalletResponse walletResponse = gson.fromJson(wallet, ZonkyWalletResponse.class);
        ZonkyStatResponse statResponse = gson.fromJson(statistics, ZonkyStatResponse.class);
        ZonkyWeeklyStatResponse weeklyResponse = gson.fromJson(weekly, ZonkyWeeklyStatResponse.class);
        State oldValue = null;

        for (final ZonkyBindingProvider provider : providers) {
            for (String itemName : provider.getItemNames()) {
                String type = provider.getItemType(itemName);
                State newValue = null;
                try {
                    oldValue = itemRegistry.getItem(itemName).getState();
                    if (isWalletType(type)) {
                        Number value = getWalletValue(walletResponse, type);
                        newValue = new DecimalType(value.doubleValue());
                    } else if (isWeeklyStatiscticsType(type)) {
                        type = type.replace("weekly.", "");
                        Number value = getWeeklyStatValue(weeklyResponse, type);
                        newValue = new DecimalType(value.doubleValue());
                    } else {
                        if (isPercentType(type)) {
                            Double value = type.equals("currentProfitability")
                                    ? statResponse.getCurrentProfitability().doubleValue()
                                    : statResponse.getExpectedProfitability().doubleValue();
                            newValue = new DecimalType(value * 100);
                        }
                        if (newValue == null) {
                            ZonkyCurrentOverview currentOverview = statResponse.getCurrentOverview();
                            ZonkyOverallOverview overallOverview = statResponse.getOverallOverview();

                            String subType;
                            if (type.startsWith("currentOverview.")) {
                                subType = type.replace("currentOverview.", "");
                                Number value = GetStatCurrentOverviewValue(currentOverview, subType);
                                newValue = new DecimalType(value.doubleValue());
                            } else if (type.startsWith("overallOverview.")) {
                                subType = type.replace("overallOverview.", "");
                                Number value = GetStatOverallOverviewValue(overallOverview, subType);
                                newValue = new DecimalType(value.doubleValue());
                            }
                        }
                    }
                    if (oldValue != null && newValue != null && !oldValue.equals(newValue)) {
                        eventPublisher.postUpdate(itemName, newValue);
                    }
                } catch (ItemNotFoundException e) {
                    logger.error("Unknown item", e);
                }
            }
        }
    }

    private Number getWeeklyStatValue(ZonkyWeeklyStatResponse weekly, String type) {
        switch (type) {
        case "newInvestments":
            return weekly.getNewInvestments();
        case "newInvestmentsAmount":
            return weekly.getNewInvestmentsAmount();
        case "paidInstalments":
            return weekly.getPaidInstalments();
        case "paidInstalmentsAmount":
            return weekly.getPaidInstalmentsAmount();
        case "soldInvestments":
            return weekly.getSoldInvestments();
        case "soldInvestmentsAmount":
            return weekly.getSoldInvestmentsAmount();
        case "boughtInvestments":
            return weekly.getBoughtInvestments();
        case "boughtInvestmentsAmount":
            return weekly.getBoughtInvestmentsAmount();
        }
        return 0;
    }

    /*
    private boolean isIntegerType(String type) {
    return type.equals("newInvestments") || type.equals("paidInstalments") || type.equals("soldInvestments") || type.equals("boughtInvestments");
    }*/

    private Number GetStatOverallOverviewValue(ZonkyOverallOverview overallOverview, String subType) {
        switch (subType) {
        case "feesAmount":
            return overallOverview.getFeesAmount();
        case "interestPaid":
            return overallOverview.getInterestPaid();
        case "investmentCount":
            return overallOverview.getInvestmentCount();
        case "netIncome":
            return overallOverview.getNetIncome();
        case "principalLost":
            return overallOverview.getPrincipalLost();
        case "principalPaid":
            return overallOverview.getPrincipalPaid();
        case "totalInvestment":
            return overallOverview.getTotalInvestment();
        }
        return 0d;
    }

    private Number GetStatCurrentOverviewValue(ZonkyCurrentOverview currentOverview, String subType) {
        switch (subType) {
        case "interestLeft":
            return currentOverview.getInterestLeft();
        case "interestLeftDue":
            return currentOverview.getInterestLeftDue();
        case "interestLeftToPay":
            return currentOverview.getInterestLeftToPay();
        case "interestPaid":
            return currentOverview.getInterestPaid();
        case "interestPanned":
            return currentOverview.getInterestPlanned();
        case "investmentCount":
            return currentOverview.getInvestmentCount();
        case "principalLeft":
            return currentOverview.getPrincipalLeft();
        case "principalLeftDue":
            return currentOverview.getPrincipalLeftDue();
        case "principalLeftToPay":
            return currentOverview.getPrincipalLeftToPay();
        case "principalPaid":
            return currentOverview.getPrincipalPaid();
        case "totalInvestment":
            return currentOverview.getTotalInvestment();
        }

        return 0d;
    }

    private Number getWalletValue(ZonkyWalletResponse wallet, String type) {
        switch (type) {
        case "balance":
            return wallet.getBalance();
        case "availableBalance":
            return wallet.getAvailableBalance();
        case "blockedBalance":
            return wallet.getBlockedBalance();
        case "creditSum":
            return wallet.getCreditSum();
        case "debitSum":
            return wallet.getDebitSum();
        }
        return 0;
    }

    private boolean isPercentType(String type) {
        return type.equals("currentProfitability") || type.equals("expectedProfitability");
    }

    private boolean isWalletType(String type) {
        return type.equals("balance") || type.equals("availableBalance") || type.equals("blockedBalance")
                || type.equals("creditSum") || type.equals("debitSum");
    }

    private boolean isWeeklyStatiscticsType(String type) {
        return type.startsWith("weekly.");
    }

    private Boolean refreshToken() {
        String url = null;

        try {
            //login
            url = ZONKY_URL + "oauth/token";
            String urlParameters = "refresh_token=" + refreshToken
                    + "&grant_type=refresh_token&scope=SCOPE_APP_WEB";
            byte[] postData = urlParameters.getBytes(StandardCharsets.UTF_8);

            URL cookieUrl = new URL(url);
            HttpsURLConnection connection = (HttpsURLConnection) cookieUrl.openConnection();
            setupConnectionDefaults(connection);
            connection.setDoOutput(true);
            connection.setRequestMethod("POST");
            connection.setRequestProperty("Content-Length", Integer.toString(postData.length));
            connection.setRequestProperty("Authorization", "Basic d2ViOndlYg==");
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
            try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
                wr.write(postData);
            }
            String line = readResponse(connection);
            ZonkyTokenResponse response = gson.fromJson(line, ZonkyTokenResponse.class);
            token = response.getAccessToken();
            refreshToken = response.getRefreshToken();
            return true;

        } catch (MalformedURLException e) {
            logger.error("The URL '{}' is malformed", url, e);
        } catch (Exception e) {
            logger.error("Cannot get Zonky login token", e);
        }
        return false;
    }

    private String getWallet() {
        return sendJsonRequest("users/me/wallet");
    }

    private String getStatistics() {
        return sendJsonRequest("statistics/overview");
    }

    private String getWeeklyStatistics() {
        return sendJsonRequest("statistics/weekly-statistics");
    }

    private String sendJsonRequest(String uri) {
        String url = null;

        logger.debug("sending uri request: {}", uri);
        try {
            //login
            url = ZONKY_URL + uri;

            URL cookieUrl = new URL(url);
            HttpsURLConnection connection = (HttpsURLConnection) cookieUrl.openConnection();
            setupConnectionDefaults(connection);
            connection.setRequestProperty("Authorization", "Bearer " + token);

            if (connection.getResponseCode() != 200) {
                logger.error("Got response code: {}", connection.getResponseCode());
                return null;
            }

            return readResponse(connection);
        } catch (MalformedURLException e) {
            logger.error("The URL '{}' is malformed", url, e);
        } catch (Exception e) {
            logger.error("Cannot get Zonky wallet response", e);
        }
        return null;
    }

    private String logout() {
        String url = null;

        try {
            //login
            url = ZONKY_URL + "users/me/logout";

            URL cookieUrl = new URL(url);
            HttpsURLConnection connection = (HttpsURLConnection) cookieUrl.openConnection();
            setupConnectionDefaults(connection);
            connection.setRequestProperty("Authorization", "Bearer " + token);

            return readResponse(connection);
        } catch (MalformedURLException e) {
            logger.error("The URL '{}' is malformed", url, e);
        } catch (Exception e) {
            logger.error("Cannot get Zonky logout response", e);
        }
        return null;
    }

    private void login() {
        String url = null;

        try {
            //login
            url = ZONKY_URL + "oauth/token";
            String urlParameters = "username=" + userName + "&password=" + password
                    + "&grant_type=password&scope=SCOPE_APP_WEB";
            byte[] postData = urlParameters.getBytes(StandardCharsets.UTF_8);

            URL cookieUrl = new URL(url);
            HttpsURLConnection connection = (HttpsURLConnection) cookieUrl.openConnection();
            setupConnectionDefaults(connection);
            connection.setDoOutput(true);
            connection.setRequestMethod("POST");
            connection.setRequestProperty("Content-Length", Integer.toString(postData.length));
            connection.setRequestProperty("Authorization", "Basic d2ViOndlYg==");
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
            try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
                wr.write(postData);
            }
            String line = readResponse(connection);
            ZonkyTokenResponse response = gson.fromJson(line, ZonkyTokenResponse.class);
            token = response.getAccessToken();
            refreshToken = response.getRefreshToken();
            if (!token.isEmpty()) {
                logger.info("Successfully logged in to Zonky!");
            }
        } catch (MalformedURLException e) {
            logger.error("The URL '{}' is malformed", url, e);
        } catch (Exception e) {
            logger.error("Cannot get Zonky login token", e);
        }
    }

    private void setupConnectionDefaults(HttpsURLConnection connection) {
        connection.setRequestProperty("User-Agent", USER_AGENT);
        connection.setRequestProperty("Accept-Language", "cs-CZ");
        //connection.setRequestProperty("Accept-Encoding", "gzip, deflate");
        connection.setRequestProperty("Accept", "application/json, text/plain, */*");
        connection.setRequestProperty("Referer", "https://app.zonky.cz/");
        connection.setInstanceFollowRedirects(false);
        connection.setUseCaches(false);
    }

    private String readResponse(HttpsURLConnection connection) throws Exception {
        InputStream stream = connection.getInputStream();
        String line;
        StringBuilder body = new StringBuilder();
        BufferedReader reader = new BufferedReader(new InputStreamReader(stream));

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

    /**
     * @{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);
    }

    /**
     * @{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);
    }

}