org.openhab.binding.amazonechocontrol.handler.AccountHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.binding.amazonechocontrol.handler.AccountHandler.java

Source

/**
 * Copyright (c) 2010-2018 by the respective copyright holders.
 *
 * 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.amazonechocontrol.handler;

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.core.storage.Storage;
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.eclipse.smarthome.core.types.RefreshType;
import org.openhab.binding.amazonechocontrol.internal.AccountConfiguration;
import org.openhab.binding.amazonechocontrol.internal.AccountServlet;
import org.openhab.binding.amazonechocontrol.internal.Connection;
import org.openhab.binding.amazonechocontrol.internal.ConnectionException;
import org.openhab.binding.amazonechocontrol.internal.HttpException;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonBluetoothStates.BluetoothState;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonDevices.Device;
import org.openhab.binding.amazonechocontrol.internal.jsons.JsonFeed;
import org.osgi.service.http.HttpService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;

/**
 * Handles the connection to the amazon server.
 *
 * @author Michael Geramb - Initial Contribution
 */
@NonNullByDefault
public class AccountHandler extends BaseBridgeHandler {

    private final Logger logger = LoggerFactory.getLogger(AccountHandler.class);
    private Storage<String> stateStorage;
    private @Nullable Connection connection;
    private final Set<EchoHandler> echoHandlers = new HashSet<>();
    private final Set<FlashBriefingProfileHandler> flashBriefingProfileHandlers = new HashSet<>();
    private final Object synchronizeConnection = new Object();
    private Map<String, Device> jsonSerialNumberDeviceMapping = new HashMap<>();
    private @Nullable ScheduledFuture<?> refreshJob;
    private @Nullable ScheduledFuture<?> refreshLogin;
    private String currentFlashBriefingJson = "";
    private final HttpService httpService;
    private @Nullable AccountServlet accountServlet;
    private final Gson gson = new Gson();

    public AccountHandler(Bridge bridge, HttpService httpService, Storage<String> stateStorage) {
        super(bridge);
        this.httpService = httpService;
        this.stateStorage = stateStorage;
    }

    @Override
    public void initialize() {
        logger.debug("amazon account bridge starting...");

        AccountConfiguration config = getConfigAs(AccountConfiguration.class);

        String amazonSite = config.amazonSite;
        if (StringUtils.isEmpty(amazonSite)) {
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Amazon site not configured");
            return;
        }
        String email = config.email;
        if (StringUtils.isEmpty(email)) {
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
                    "Account email not configured");
            return;
        }
        String password = config.password;
        if (StringUtils.isEmpty(password)) {
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
                    "Account password not configured");
            return;
        }
        Integer pollingIntervalInSeconds = config.pollingIntervalInSeconds;
        if (pollingIntervalInSeconds == null) {
            pollingIntervalInSeconds = 30;
        }
        if (pollingIntervalInSeconds < 10) {
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
                    "Polling interval less than 10 seconds not allowed");
            return;
        }
        synchronized (synchronizeConnection) {
            Connection connection = this.connection;
            if (connection == null || !connection.getEmail().equals(email)
                    || !connection.getPassword().equals(password)
                    || !connection.getAmazonSite().equals(amazonSite)) {
                this.connection = new Connection(email, password, amazonSite, this.getThing().getUID().getId());
            }
        }
        if (this.accountServlet == null) {
            this.accountServlet = new AccountServlet(httpService, this.getThing().getUID().getId(), this, config);
        }

        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Wait for login");

        refreshLogin = scheduler.scheduleWithFixedDelay(this::checkLogin, 0, 60, TimeUnit.SECONDS);
        refreshJob = scheduler.scheduleWithFixedDelay(this::refreshData, 4, pollingIntervalInSeconds,
                TimeUnit.SECONDS);

        logger.debug("amazon account bridge handler started.");
    }

    @Override
    public void handleCommand(ChannelUID channelUID, Command command) {
        logger.trace("Command '{}' received for channel '{}'", command, channelUID);
        if (command instanceof RefreshType) {
            refreshData();
        }
    }

    public List<FlashBriefingProfileHandler> getFlashBriefingProfileHandlers() {
        return new ArrayList<>(this.flashBriefingProfileHandlers);
    }

    public List<Device> getLastKnownDevices() {
        return new ArrayList<>(jsonSerialNumberDeviceMapping.values());
    }

    public void addEchoHandler(EchoHandler echoHandler) {
        synchronized (echoHandlers) {
            echoHandlers.add(echoHandler);
        }
        Connection connection = this.connection;
        if (connection != null) {
            initializeEchoHandler(echoHandler, connection);
        }
    }

    public @Nullable Thing findThingBySerialNumber(@Nullable String deviceSerialNumber) {
        synchronized (echoHandlers) {
            for (EchoHandler echoHandler : echoHandlers) {
                if (StringUtils.equals(echoHandler.findSerialNumber(), deviceSerialNumber)) {
                    return echoHandler.getThing();
                }
            }
        }
        return null;
    }

    public void addFlashBriefingProfileHandler(FlashBriefingProfileHandler flashBriefingProfileHandler) {
        synchronized (flashBriefingProfileHandlers) {
            flashBriefingProfileHandlers.add(flashBriefingProfileHandler);
        }
        Connection connection = this.connection;
        if (connection != null) {
            if (currentFlashBriefingJson.isEmpty()) {
                updateFlashBriefingProfiles(connection);
            }
            flashBriefingProfileHandler.initialize(this, currentFlashBriefingJson);
        }
    }

    private void initializeEchoHandler(EchoHandler echoHandler, Connection connection) {
        intializeChildDevice(connection, echoHandler);

        @Nullable
        Device device = findDeviceJson(echoHandler);

        JsonBluetoothStates states = null;
        if (connection.getIsLoggedIn()) {
            states = connection.getBluetoothConnectionStates();
        }

        BluetoothState state = null;
        if (states != null) {
            state = states.findStateByDevice(device);
        }
        echoHandler.updateState(device, state);
    }

    private void intializeChildDevice(Connection connection, EchoHandler child) {
        Device deviceJson = this.findDeviceJson(child);
        if (deviceJson != null) {
            child.intialize(connection, deviceJson);
        }
    }

    @Override
    public void handleRemoval() {
        cleanup();
        super.handleRemoval();
    }

    @Override
    public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
        // check for echo handler
        if (childHandler instanceof EchoHandler) {
            synchronized (echoHandlers) {
                echoHandlers.remove(childHandler);
            }
        }
        // check for flash briefing profile handler
        if (childHandler instanceof FlashBriefingProfileHandler) {
            synchronized (flashBriefingProfileHandlers) {
                flashBriefingProfileHandlers.remove(childHandler);
            }
        }
        super.childHandlerDisposed(childHandler, childThing);
    }

    @Override
    public void dispose() {
        AccountServlet accountServlet = this.accountServlet;
        if (accountServlet != null) {
            accountServlet.dispose();
        }
        this.accountServlet = null;
        cleanup();
        super.dispose();
    }

    private void cleanup() {
        logger.debug("cleanup {}", getThing().getUID().getAsString());
        @Nullable
        ScheduledFuture<?> refreshJob = this.refreshJob;
        if (refreshJob != null) {
            refreshJob.cancel(true);
            this.refreshJob = null;
        }
        @Nullable
        ScheduledFuture<?> refreshLogin = this.refreshLogin;
        if (refreshLogin != null) {
            refreshLogin.cancel(true);
            this.refreshLogin = null;
        }
        Connection connection = this.connection;
        if (connection != null) {
            connection.logout();
            this.connection = null;
        }
    }

    private void checkLogin() {
        try {
            logger.debug("check login {}", getThing().getUID().getAsString());

            synchronized (synchronizeConnection) {
                Connection currentConnection = this.connection;
                if (currentConnection == null) {
                    return;
                }
                Date verifyTime = currentConnection.tryGetVerifyTime();
                Date currentDate = new Date();
                long currentTime = currentDate.getTime();
                if (verifyTime != null && currentTime - verifyTime.getTime() > 3600000) // Every one hour
                {
                    try {
                        if (!currentConnection.verifyLogin()) {
                            currentConnection.logout();
                        }
                    } catch (IOException | URISyntaxException e) {
                        logger.info("logout failed: {}", e.getMessage());
                        currentConnection.logout();
                    }
                }
                Date loginTime = currentConnection.tryGetLoginTime();
                if (loginTime != null && currentTime - loginTime.getTime() > 86400000 * 5) // 5 days
                {
                    // Recreate session
                    this.stateStorage.put("sessionStorage", "");
                    currentConnection = new Connection(currentConnection.getEmail(),
                            currentConnection.getPassword(), currentConnection.getAmazonSite(),
                            this.getThing().getUID().getId());
                }
                boolean loginIsValid = true;
                if (!currentConnection.getIsLoggedIn()) {
                    try {

                        // read session data from property
                        String sessionStore = this.stateStorage.get("sessionStorage");

                        // try use the session data
                        if (!currentConnection.tryRestoreLogin(sessionStore)) {
                            // session data not valid -> login
                            int retry = 0;
                            while (true) {
                                try {
                                    currentConnection.makeLogin();
                                    break;
                                } catch (ConnectionException e) {
                                    // Up to 2 retries for login
                                    retry++;
                                    if (retry >= 2) {
                                        currentConnection.logout();
                                        throw e;
                                    }
                                    // give amazon some time
                                    try {
                                        Thread.sleep(2000);
                                    } catch (InterruptedException exception) {
                                        // throw the original exception
                                        throw e;
                                    }
                                }
                            }
                            // store session data in property
                            String serializedStorage = currentConnection.serializeLoginData();
                            this.stateStorage.put("sessionStorage", serializedStorage);
                        }
                        this.connection = currentConnection;
                    } catch (ConnectionException e) {
                        loginIsValid = false;
                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
                    } catch (UnknownHostException e) {
                        loginIsValid = false;
                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                                "Unknown host name '" + e.getMessage()
                                        + "'. Maybe your internet connection is offline");
                    } catch (IOException e) {
                        loginIsValid = false;
                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                                e.getLocalizedMessage());
                    } catch (URISyntaxException e) {
                        loginIsValid = false;
                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                                e.getLocalizedMessage());
                    }
                    if (loginIsValid) {
                        handleValidLogin();
                    }
                }
            }
        } catch (HttpException | JsonSyntaxException | ConnectionException e) {
            logger.debug("check login fails {}", e);
        } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
            logger.error("check login fails with unexpected error {}", e);
        }
    }

    private void handleValidLogin() {
        updateDeviceList();
        updateFlashBriefingHandlers();
        updateStatus(ThingStatus.ONLINE);
    }

    // used to set a valid connection from the web proxy login
    public void setConnection(Connection connection) {
        this.connection = connection;
        String serializedStorage = connection.serializeLoginData();
        this.stateStorage.put("sessionStorage", serializedStorage);
        handleValidLogin();
    }

    private void refreshData() {
        try {
            logger.debug("refreshing data {}", getThing().getUID().getAsString());

            // check if logged in
            Connection currentConnection = null;
            synchronized (synchronizeConnection) {
                currentConnection = connection;
                if (currentConnection != null) {
                    if (!currentConnection.getIsLoggedIn()) {
                        return;
                    }
                }
            }
            if (currentConnection == null) {
                return;
            }

            // get all devices registered in the account
            updateDeviceList();
            updateFlashBriefingHandlers();

            // update bluetooth states
            JsonBluetoothStates states = null;
            if (currentConnection.getIsLoggedIn()) {
                states = currentConnection.getBluetoothConnectionStates();
            }

            // forward device information to echo handler
            for (EchoHandler child : echoHandlers) {
                Device device = findDeviceJson(child);
                BluetoothState state = null;
                if (states != null) {
                    state = states.findStateByDevice(device);
                }
                child.updateState(device, state);
            }

            // update account state
            updateStatus(ThingStatus.ONLINE);

            logger.debug("refresh data {} finished", getThing().getUID().getAsString());
        } catch (HttpException | JsonSyntaxException | ConnectionException e) {
            logger.debug("refresh data fails {}", e);
        } catch (Exception e) { // this handler can be removed later, if we know that nothing else can fail.
            logger.error("refresh data fails with unexpected error {}", e);
        }
    }

    public @Nullable Device findDeviceJson(EchoHandler echoHandler) {
        String serialNumber = echoHandler.findSerialNumber();
        return findDeviceJson(serialNumber);
    }

    public @Nullable Device findDeviceJson(@Nullable String serialNumber) {
        Device result = null;
        if (StringUtils.isNotEmpty(serialNumber)) {
            Map<String, Device> jsonSerialNumberDeviceMapping = this.jsonSerialNumberDeviceMapping;
            result = jsonSerialNumberDeviceMapping.get(serialNumber);
        }
        return result;
    }

    public @Nullable Device findDeviceJsonBySerialOrName(@Nullable String serialOrName) {
        if (StringUtils.isNotEmpty(serialOrName)) {
            Map<String, Device> currentJsonSerialNumberDeviceMapping = this.jsonSerialNumberDeviceMapping;
            for (Device device : currentJsonSerialNumberDeviceMapping.values()) {
                if (StringUtils.equalsIgnoreCase(device.serialNumber, serialOrName)) {
                    return device;
                }
            }
            for (Device device : currentJsonSerialNumberDeviceMapping.values()) {
                if (StringUtils.equalsIgnoreCase(device.accountName, serialOrName)) {
                    return device;
                }
            }
        }
        return null;
    }

    public List<Device> updateDeviceList() {

        Connection currentConnection = connection;
        if (currentConnection == null) {
            return new ArrayList<Device>();
        }

        List<Device> devices = null;
        try {
            if (currentConnection.getIsLoggedIn()) {
                devices = currentConnection.getDeviceList();
            }
        } catch (IOException | URISyntaxException e) {
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
        }
        if (devices != null) {
            Map<String, Device> newJsonSerialDeviceMapping = new HashMap<>();
            for (Device device : devices) {
                String serialNumber = device.serialNumber;
                if (serialNumber != null) {
                    newJsonSerialDeviceMapping.put(serialNumber, device);
                }
            }
            jsonSerialNumberDeviceMapping = newJsonSerialDeviceMapping;
        }
        synchronized (echoHandlers) {
            for (EchoHandler child : echoHandlers) {
                initializeEchoHandler(child, currentConnection);
            }
        }
        if (devices != null) {
            return devices;
        }
        return new ArrayList<Device>();
    }

    public void setEnabledFlashBriefingsJson(String flashBriefingJson) {
        Connection currentConnection = connection;
        JsonFeed[] feeds = gson.fromJson(flashBriefingJson, JsonFeed[].class);
        if (currentConnection != null) {
            try {
                currentConnection.setEnabledFlashBriefings(feeds);
            } catch (IOException | URISyntaxException e) {
                logger.warn("Set flashbriefing profile failed {}", e);
            }
        }
        updateFlashBriefingHandlers();
    }

    public String getNewCurrentFlashbriefingConfiguration() {
        return updateFlashBriefingHandlers();
    }

    public String updateFlashBriefingHandlers() {
        Connection currentConnection = connection;
        if (currentConnection != null) {
            return updateFlashBriefingHandlers(currentConnection);
        }
        return "";
    }

    private String updateFlashBriefingHandlers(Connection currentConnection) {
        synchronized (flashBriefingProfileHandlers) {
            if (!flashBriefingProfileHandlers.isEmpty() || currentFlashBriefingJson.isEmpty()) {
                updateFlashBriefingProfiles(currentConnection);
            }
            boolean flashBriefingProfileFound = false;
            for (FlashBriefingProfileHandler child : flashBriefingProfileHandlers) {
                flashBriefingProfileFound |= child.initialize(this, currentFlashBriefingJson);
            }
            if (flashBriefingProfileFound) {
                return "";
            }
            return this.currentFlashBriefingJson;
        }
    }

    public @Nullable Connection findConnection() {
        return this.connection;
    }

    public String getEnabledFlashBriefingsJson() {
        Connection currentConnection = this.connection;
        if (currentConnection == null) {
            return "";
        }
        updateFlashBriefingProfiles(currentConnection);
        return this.currentFlashBriefingJson;
    }

    private void updateFlashBriefingProfiles(Connection currentConnection) {
        try {
            JsonFeed[] feeds = currentConnection.getEnabledFlashBriefings();
            // Make a copy and remove changeable parts
            JsonFeed[] forSerializer = new JsonFeed[feeds.length];
            for (int i = 0; i < feeds.length; i++) {
                JsonFeed source = feeds[i];
                JsonFeed copy = new JsonFeed();
                copy.feedId = source.feedId;
                copy.skillId = source.skillId;
                // Do not copy imageUrl here, because it will change
                forSerializer[i] = copy;
            }
            this.currentFlashBriefingJson = gson.toJson(forSerializer);
        } catch (HttpException | JsonSyntaxException | IOException | URISyntaxException | ConnectionException e) {
            logger.warn("get flash briefing profiles fails {}", e);
        }

    }

}