org.openhab.binding.tesla.internal.handler.TeslaHandler.java Source code

Java tutorial

Introduction

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

import static org.openhab.binding.tesla.internal.TeslaBindingConstants.*;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;

import org.apache.commons.lang.StringUtils;
import org.eclipse.smarthome.core.library.types.DecimalType;
import org.eclipse.smarthome.core.library.types.IncreaseDecreaseType;
import org.eclipse.smarthome.core.library.types.OnOffType;
import org.eclipse.smarthome.core.library.types.PercentType;
import org.eclipse.smarthome.core.library.types.StringType;
import org.eclipse.smarthome.core.storage.Storage;
import org.eclipse.smarthome.core.storage.StorageService;
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.BaseThingHandler;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.eclipse.smarthome.core.types.State;
import org.eclipse.smarthome.core.types.UnDefType;
import org.glassfish.jersey.client.ClientProperties;
import org.openhab.binding.tesla.internal.TeslaBindingConstants;
import org.openhab.binding.tesla.internal.TeslaBindingConstants.EventKeys;
import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy;
import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy.TeslaChannelSelector;
import org.openhab.binding.tesla.internal.protocol.ChargeState;
import org.openhab.binding.tesla.internal.protocol.ClimateState;
import org.openhab.binding.tesla.internal.protocol.DriveState;
import org.openhab.binding.tesla.internal.protocol.GUIState;
import org.openhab.binding.tesla.internal.protocol.TokenRequest;
import org.openhab.binding.tesla.internal.protocol.TokenRequestPassword;
import org.openhab.binding.tesla.internal.protocol.TokenRequestRefreshToken;
import org.openhab.binding.tesla.internal.protocol.TokenResponse;
import org.openhab.binding.tesla.internal.protocol.Vehicle;
import org.openhab.binding.tesla.internal.protocol.VehicleState;
import org.openhab.binding.tesla.internal.throttler.QueueChannelThrottler;
import org.openhab.binding.tesla.internal.throttler.Rate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

/**
 * The {@link TeslaHandler} is responsible for handling commands, which are sent
 * to one of the channels.
 *
 * @author Karel Goderis - Initial contribution
 * @author Nicolai Grdum - Adding token based auth
 */
public class TeslaHandler extends BaseThingHandler {

    private static final int EVENT_STREAM_CONNECT_TIMEOUT = 3000;
    private static final int EVENT_STREAM_READ_TIMEOUT = 200000;
    private static final int EVENT_TIMESTAMP_AGE_LIMIT = 3000;
    private static final int EVENT_TIMESTAMP_MAX_DELTA = 10000;
    private static final int FAST_STATUS_REFRESH_INTERVAL = 15000;
    private static final int SLOW_STATUS_REFRESH_INTERVAL = 60000;
    private static final int CONNECT_RETRY_INTERVAL = 15000;
    private static final int API_MAXIMUM_ERRORS_IN_INTERVAL = 2;
    private static final int API_ERROR_INTERVAL_SECONDS = 15;
    private static final int EVENT_MAXIMUM_ERRORS_IN_INTERVAL = 10;
    private static final int EVENT_ERROR_INTERVAL_SECONDS = 15;
    private static final int API_SLEEP_INTERVAL_MINUTES = 15;
    private static final int MOVE_THRESHOLD_INTERVAL_MINUTES = 5;

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

    // Vehicle state variables
    protected Vehicle vehicle;
    protected String vehicleJSON;
    protected DriveState driveState;
    protected GUIState guiState;
    protected VehicleState vehicleState;
    protected ChargeState chargeState;
    protected ClimateState climateState;

    // REST Client API variables
    protected final Client teslaClient = ClientBuilder.newClient();
    protected Client eventClient = ClientBuilder.newClient();
    public final WebTarget teslaTarget = teslaClient.target(URI_OWNERS);
    public final WebTarget tokenTarget = teslaTarget.path(URI_ACCESS_TOKEN);
    public final WebTarget vehiclesTarget = teslaTarget.path(API_VERSION).path(VEHICLES);
    public final WebTarget vehicleTarget = vehiclesTarget.path(PATH_VEHICLE_ID);
    public final WebTarget dataRequestTarget = vehicleTarget.path(PATH_DATA_REQUEST);
    public final WebTarget commandTarget = vehicleTarget.path(PATH_COMMAND);
    public final WebTarget wakeUpTarget = vehicleTarget.path(PATH_WAKE_UP);
    protected WebTarget eventTarget;

    // Threading and Job related variables
    protected ScheduledFuture<?> connectJob;
    protected Thread eventThread;
    protected ScheduledFuture<?> fastStateJob;
    protected ScheduledFuture<?> slowStateJob;
    protected QueueChannelThrottler stateThrottler;

    protected boolean allowWakeUp = false;
    protected long lastTimeStamp;
    protected long apiIntervalTimestamp;
    protected int apiIntervalErrors;
    protected long eventIntervalTimestamp;
    protected int eventIntervalErrors;
    protected ReentrantLock lock;

    protected double lastLongitude;
    protected double lastLatitude;
    protected long lastLocationChangeTimestamp;

    protected long lastStateTimestamp = System.currentTimeMillis();
    protected String lastState = "";
    protected boolean isInactive = false;

    private StorageService storageService;
    protected Gson gson = new Gson();
    protected TeslaChannelSelectorProxy teslaChannelSelectorProxy = new TeslaChannelSelectorProxy();
    private TokenResponse logonToken;

    public TeslaHandler(Thing thing, StorageService storageService) {
        super(thing);
        this.storageService = storageService;
    }

    @Override
    public void initialize() {
        logger.trace("Initializing the Tesla handler for {}", this.getStorageKey());

        updateStatus(ThingStatus.UNKNOWN);

        lock = new ReentrantLock();

        lock.lock();
        try {
            if (connectJob == null || connectJob.isCancelled()) {
                connectJob = scheduler.scheduleWithFixedDelay(connectRunnable, 0, CONNECT_RETRY_INTERVAL,
                        TimeUnit.MILLISECONDS);
            }

            Map<Object, Rate> channels = new HashMap<>();
            channels.put(DATA_THROTTLE, new Rate(1, 1, TimeUnit.SECONDS));
            channels.put(COMMAND_THROTTLE, new Rate(20, 1, TimeUnit.MINUTES));

            Rate firstRate = new Rate(20, 1, TimeUnit.MINUTES);
            Rate secondRate = new Rate(200, 10, TimeUnit.MINUTES);
            stateThrottler = new QueueChannelThrottler(firstRate, scheduler, channels);
            stateThrottler.addRate(secondRate);

            if (fastStateJob == null || fastStateJob.isCancelled()) {
                fastStateJob = scheduler.scheduleWithFixedDelay(fastStateRunnable, 0, FAST_STATUS_REFRESH_INTERVAL,
                        TimeUnit.MILLISECONDS);
            }

            if (slowStateJob == null || slowStateJob.isCancelled()) {
                slowStateJob = scheduler.scheduleWithFixedDelay(slowStateRunnable, 0, SLOW_STATUS_REFRESH_INTERVAL,
                        TimeUnit.MILLISECONDS);
            }
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void dispose() {
        logger.trace("Disposing the Tesla handler for {}", getThing().getUID());

        lock.lock();
        try {
            if (fastStateJob != null && !fastStateJob.isCancelled()) {
                fastStateJob.cancel(true);
                fastStateJob = null;
            }

            if (slowStateJob != null && !slowStateJob.isCancelled()) {
                slowStateJob.cancel(true);
                slowStateJob = null;
            }

            if (eventThread != null && !eventThread.isInterrupted()) {
                eventThread.interrupt();
                eventThread = null;
            }

            if (connectJob != null && !connectJob.isCancelled()) {
                connectJob.cancel(true);
                connectJob = null;
            }
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void handleCommand(ChannelUID channelUID, Command command) {
        String channelID = channelUID.getId();
        TeslaChannelSelector selector = TeslaChannelSelector.getValueSelectorFromChannelID(channelID);

        if (command instanceof RefreshType) {
            if (isAwake()) {
                // Request the state of all known variables. This is sub-optimal, but the requests get scheduled and
                // throttled so we are safe not to break the Tesla SLA
                requestAllData();
            }
        } else {
            if (selector != null) {
                try {
                    switch (selector) {
                    case CHARGE_LIMIT_SOC: {
                        if (command instanceof PercentType) {
                            setChargeLimit(((PercentType) command).intValue());
                        } else if (command instanceof OnOffType && command == OnOffType.ON) {
                            setChargeLimit(100);
                        } else if (command instanceof OnOffType && command == OnOffType.OFF) {
                            setChargeLimit(0);
                        } else if (command instanceof IncreaseDecreaseType
                                && command == IncreaseDecreaseType.INCREASE) {
                            setChargeLimit(Math.min(chargeState.charge_limit_soc + 1, 100));
                        } else if (command instanceof IncreaseDecreaseType
                                && command == IncreaseDecreaseType.DECREASE) {
                            setChargeLimit(Math.max(chargeState.charge_limit_soc - 1, 0));
                        }
                        break;
                    }
                    case TEMPERATURE: {
                        if (command instanceof DecimalType) {
                            if (getThing().getProperties().containsKey("temperatureunits")
                                    && getThing().getProperties().get("temperatureunits").equals("F")) {
                                float fTemp = ((DecimalType) command).floatValue();
                                float cTemp = ((fTemp - 32.0f) * 5.0f / 9.0f);
                                setTemperature(cTemp);
                            } else {
                                setTemperature(((DecimalType) command).floatValue());
                            }
                        }
                        break;
                    }
                    case SUN_ROOF_STATE: {
                        if (command instanceof StringType) {
                            setSunroof(command.toString());
                        }
                        break;
                    }
                    case SUN_ROOF: {
                        if (command instanceof PercentType) {
                            moveSunroof(((PercentType) command).intValue());
                        } else if (command instanceof OnOffType && command == OnOffType.ON) {
                            moveSunroof(100);
                        } else if (command instanceof OnOffType && command == OnOffType.OFF) {
                            moveSunroof(0);
                        } else if (command instanceof IncreaseDecreaseType
                                && command == IncreaseDecreaseType.INCREASE) {
                            moveSunroof(Math.min(vehicleState.sun_roof_percent_open + 1, 100));
                        } else if (command instanceof IncreaseDecreaseType
                                && command == IncreaseDecreaseType.DECREASE) {
                            moveSunroof(Math.max(vehicleState.sun_roof_percent_open - 1, 0));
                        }
                        break;
                    }
                    case CHARGE_TO_MAX: {
                        if (command instanceof OnOffType) {
                            if (((OnOffType) command) == OnOffType.ON) {
                                setMaxRangeCharging(true);
                            } else {
                                setMaxRangeCharging(false);
                            }
                        }
                        break;
                    }
                    case CHARGE: {
                        if (command instanceof OnOffType) {
                            if (((OnOffType) command) == OnOffType.ON) {
                                charge(true);
                            } else {
                                charge(false);
                            }
                        }
                        break;
                    }
                    case FLASH: {
                        if (command instanceof OnOffType) {
                            if (((OnOffType) command) == OnOffType.ON) {
                                flashLights();
                            }
                        }
                        break;
                    }
                    case HONK_HORN: {
                        if (command instanceof OnOffType) {
                            if (((OnOffType) command) == OnOffType.ON) {
                                honkHorn();
                            }
                        }
                        break;
                    }
                    case CHARGEPORT: {
                        if (command instanceof OnOffType) {
                            if (((OnOffType) command) == OnOffType.ON) {
                                openChargePort();
                            }
                        }
                        break;
                    }
                    case DOOR_LOCK: {
                        if (command instanceof OnOffType) {
                            if (((OnOffType) command) == OnOffType.ON) {
                                lockDoors(true);
                            } else {
                                lockDoors(false);
                            }
                        }
                        break;
                    }
                    case AUTO_COND: {
                        if (command instanceof OnOffType) {
                            if (((OnOffType) command) == OnOffType.ON) {
                                autoConditioning(true);
                            } else {
                                autoConditioning(false);
                            }
                        }
                        break;
                    }
                    case WAKEUP: {
                        if (command instanceof OnOffType) {
                            if (((OnOffType) command) == OnOffType.ON) {
                                wakeUp();
                            }
                        }
                        break;
                    }
                    case FORCE_REFRESH: {
                        if (command instanceof OnOffType) {
                            if (((OnOffType) command) == OnOffType.ON) {
                                if (!isOnline()) {
                                    wakeUp();
                                } else {
                                    setActive();
                                }
                            }
                        }
                        break;
                    }
                    case ALLOWWAKEUP: {
                        if (command instanceof OnOffType) {
                            allowWakeUp = (((OnOffType) command) == OnOffType.ON);
                        }
                        break;
                    }
                    case ENABLEEVENTS: {
                        if (command instanceof OnOffType) {
                            if (((OnOffType) command) == OnOffType.ON) {
                                if (eventThread == null) {
                                    eventThread = new Thread(eventRunnable,
                                            "openHAB-Tesla-Events-" + getThing().getUID());
                                    eventThread.start();
                                }
                            } else {
                                if (eventThread != null) {
                                    eventThread.interrupt();
                                    eventThread = null;
                                }
                            }
                        }
                        break;
                    }
                    case FT: {
                        if (command instanceof OnOffType) {
                            if (((OnOffType) command) == OnOffType.ON) {
                                openFrunk();
                            }
                        }
                        break;
                    }
                    case RT: {
                        if (command instanceof OnOffType) {
                            if (((OnOffType) command) == OnOffType.ON) {
                                if (vehicleState.rt == 0) {
                                    openTrunk();
                                }
                            } else {
                                if (vehicleState.rt == 1) {
                                    closeTrunk();
                                }
                            }
                        }
                        break;
                    }
                    case VALET_MODE: {
                        if (command instanceof OnOffType) {
                            int valetpin = ((BigDecimal) getConfig().get(VALETPIN)).intValue();
                            if (((OnOffType) command) == OnOffType.ON) {
                                setValetMode(true, valetpin);
                            } else {
                                setValetMode(false, valetpin);
                            }
                        }
                        break;
                    }
                    case RESET_VALET_PIN: {
                        if (command instanceof OnOffType) {
                            if (((OnOffType) command) == OnOffType.ON) {
                                resetValetPin();
                            }
                        }
                        break;
                    }
                    default:
                        break;
                    }
                    return;
                } catch (IllegalArgumentException e) {
                    logger.warn(
                            "An error occurred while trying to set the read-only variable associated with channel '{}' to '{}'",
                            channelID, command.toString());
                }
            }
        }
    }

    public void sendCommand(String command, String payLoad, WebTarget target) {
        Request request = new Request(command, payLoad, target);
        if (stateThrottler != null) {
            stateThrottler.submit(COMMAND_THROTTLE, request);
        }
    }

    public void sendCommand(String command) {
        sendCommand(command, "{}");
    }

    public void sendCommand(String command, String payLoad) {
        Request request = new Request(command, payLoad, commandTarget);
        if (stateThrottler != null) {
            stateThrottler.submit(COMMAND_THROTTLE, request);
        }
    }

    public void sendCommand(String command, WebTarget target) {
        Request request = new Request(command, "{}", target);
        if (stateThrottler != null) {
            stateThrottler.submit(COMMAND_THROTTLE, request);
        }
    }

    public void requestData(String command, String payLoad) {
        Request request = new Request(command, payLoad, dataRequestTarget);
        if (stateThrottler != null) {
            stateThrottler.submit(DATA_THROTTLE, request);
        }
    }

    public void requestData(String command) {
        requestData(command, null);
    }

    public void queryVehicle(String parameter) {
        WebTarget target = vehicleTarget.path(parameter);
        sendCommand(parameter, null, target);
    }

    public void requestAllData() {
        requestData(DRIVE_STATE);
        requestData(VEHICLE_STATE);
        requestData(CHARGE_STATE);
        requestData(CLIMATE_STATE);
        requestData(GUI_STATE);
    }

    protected String invokeAndParse(String command, String payLoad, WebTarget target) {
        logger.debug("Invoking: {}", command);

        if (vehicle.id != null) {
            Response response;

            if (payLoad != null) {
                if (command != null) {
                    response = target.resolveTemplate("cmd", command).resolveTemplate("vid", vehicle.id).request()
                            .header("Authorization", "Bearer " + logonToken.access_token)
                            .post(Entity.entity(payLoad, MediaType.APPLICATION_JSON_TYPE));
                } else {
                    response = target.resolveTemplate("vid", vehicle.id).request()
                            .header("Authorization", "Bearer " + logonToken.access_token)
                            .post(Entity.entity(payLoad, MediaType.APPLICATION_JSON_TYPE));
                }
            } else {
                if (command != null) {
                    response = target.resolveTemplate("cmd", command).resolveTemplate("vid", vehicle.id)
                            .request(MediaType.APPLICATION_JSON_TYPE)
                            .header("Authorization", "Bearer " + logonToken.access_token).get();
                } else {
                    response = target.resolveTemplate("vid", vehicle.id).request(MediaType.APPLICATION_JSON_TYPE)
                            .header("Authorization", "Bearer " + logonToken.access_token).get();
                }
            }

            JsonParser parser = new JsonParser();

            if (!checkResponse(response, false)) {
                logger.error("An error occurred while communicating with the vehicle during request {} : {}:{}",
                        new Object[] { command, (response != null) ? response.getStatus() : "",
                                (response != null) ? response.getStatusInfo() : "No Response" });
                return null;
            }

            try {
                JsonObject jsonObject = parser.parse(response.readEntity(String.class)).getAsJsonObject();
                logger.trace("Request : {}:{}:{} yields {}", new Object[] { command, payLoad, target.toString(),
                        jsonObject.get("response").toString() });
                return jsonObject.get("response").toString();
            } catch (Exception e) {
                logger.error("An exception occurred while invoking a REST request : '{}'", e.getMessage());
            }
        }

        return null;
    }

    public void parseAndUpdate(String request, String payLoad, String result) {
        final Double LOCATION_THRESHOLD = .0000001;

        JsonParser parser = new JsonParser();
        JsonObject jsonObject = null;

        try {
            if (request != null && result != null && !"null".equals(result)) {
                // first, update state objects
                switch (request) {
                case DRIVE_STATE: {
                    driveState = gson.fromJson(result, DriveState.class);

                    if (Math.abs(lastLatitude - driveState.latitude) > LOCATION_THRESHOLD
                            || Math.abs(lastLongitude - driveState.longitude) > LOCATION_THRESHOLD) {
                        logger.debug("Vehicle moved, resetting last location timestamp");

                        lastLatitude = driveState.latitude;
                        lastLongitude = driveState.longitude;
                        lastLocationChangeTimestamp = System.currentTimeMillis();
                    }

                    break;
                }
                case GUI_STATE: {
                    guiState = gson.fromJson(result, GUIState.class);
                    break;
                }
                case VEHICLE_STATE: {
                    vehicleState = gson.fromJson(result, VehicleState.class);
                    break;
                }
                case CHARGE_STATE: {
                    chargeState = gson.fromJson(result, ChargeState.class);
                    if (isCharging()) {
                        updateState(CHANNEL_CHARGE, OnOffType.ON);
                    } else {
                        updateState(CHANNEL_CHARGE, OnOffType.OFF);
                    }

                    break;
                }
                case CLIMATE_STATE: {
                    climateState = gson.fromJson(result, ClimateState.class);
                    break;
                }
                case "queryVehicle": {
                    if (vehicle != null && !lastState.equals(vehicle.state)) {
                        lastState = vehicle.state;

                        // in case vehicle changed to online, refresh all data
                        if (isOnline()) {
                            logger.debug("Vehicle is now online, updating all data");
                            lastLocationChangeTimestamp = System.currentTimeMillis();
                            requestAllData();
                        }

                        setActive();
                    }

                    // reset timestamp if elapsed and set inactive to false
                    if (isInactive && lastStateTimestamp + (API_SLEEP_INTERVAL_MINUTES * 60 * 1000) < System
                            .currentTimeMillis()) {
                        logger.debug("Vehicle did not fall asleep within sleep period, checking again");
                        setActive();
                    } else {
                        boolean wasInactive = isInactive;
                        isInactive = !isCharging() && !hasMovedInSleepInterval();

                        if (!wasInactive && isInactive) {
                            lastStateTimestamp = System.currentTimeMillis();
                            logger.debug("Vehicle is inactive");
                        }
                    }

                    break;
                }
                }

                // secondly, reformat the response string to a JSON compliant
                // object for some specific non-JSON compatible requests
                switch (request) {
                case MOBILE_ENABLED_STATE: {
                    jsonObject = new JsonObject();
                    jsonObject.addProperty(MOBILE_ENABLED_STATE, result);
                    break;
                }
                default: {
                    jsonObject = parser.parse(result).getAsJsonObject();
                    break;
                }
                }
            }

            // process the result
            if (jsonObject != null && result != null && !"null".equals(result)) {
                // deal with responses for "set" commands, which get confirmed
                // positively, or negatively, in which case a reason for failure
                // is provided
                if (jsonObject.get("reason") != null && jsonObject.get("reason").getAsString() != null) {
                    boolean requestResult = jsonObject.get("result").getAsBoolean();
                    logger.debug("The request ({}) execution was {}, and reported '{}'",
                            new Object[] { request, requestResult ? "successful" : "not successful",
                                    jsonObject.get("reason").getAsString() });
                } else {
                    Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();

                    long resultTimeStamp = 0;
                    for (Map.Entry<String, JsonElement> entry : entrySet) {
                        if ("timestamp".equals(entry.getKey())) {
                            resultTimeStamp = Long.valueOf(entry.getValue().getAsString());
                            if (logger.isTraceEnabled()) {
                                Date date = new Date(resultTimeStamp);
                                SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
                                logger.trace("The request result timestamp is {}", dateFormatter.format(date));
                            }
                            break;
                        }
                    }

                    try {
                        lock.lock();

                        boolean proceed = true;
                        if (resultTimeStamp < lastTimeStamp && request == DRIVE_STATE) {
                            proceed = false;
                        }

                        if (proceed) {
                            for (Map.Entry<String, JsonElement> entry : entrySet) {
                                try {
                                    TeslaChannelSelector selector = TeslaChannelSelector
                                            .getValueSelectorFromRESTID(entry.getKey());
                                    if (!selector.isProperty()) {
                                        if (!entry.getValue().isJsonNull()) {
                                            updateState(selector.getChannelID(), teslaChannelSelectorProxy.getState(
                                                    entry.getValue().getAsString(), selector, editProperties()));
                                            if (logger.isTraceEnabled()) {
                                                logger.trace(
                                                        "The variable/value pair '{}':'{}' is successfully processed",
                                                        entry.getKey(), entry.getValue());
                                            }
                                        } else {
                                            updateState(selector.getChannelID(), UnDefType.UNDEF);
                                        }
                                    } else {
                                        if (!entry.getValue().isJsonNull()) {
                                            Map<String, String> properties = editProperties();
                                            properties.put(selector.getChannelID(), entry.getValue().getAsString());
                                            updateProperties(properties);
                                            if (logger.isTraceEnabled()) {
                                                logger.trace(
                                                        "The variable/value pair '{}':'{}' is successfully used to set property '{}'",
                                                        entry.getKey(), entry.getValue(), selector.getChannelID());
                                            }
                                        }
                                    }
                                } catch (IllegalArgumentException e) {
                                    logger.trace("The variable/value pair '{}':'{}' is not (yet) supported",
                                            entry.getKey(), entry.getValue());
                                } catch (ClassCastException | IllegalStateException e) {
                                    logger.trace("An exception occurred while converting the JSON data : '{}'",
                                            e.getMessage(), e);
                                }
                            }
                        } else {
                            logger.warn("The result for request '{}' is discarded due to an out of sync timestamp",
                                    request);
                        }
                    } finally {
                        lock.unlock();
                    }
                }
            }
        } catch (Exception p) {
            logger.error("An exception occurred while parsing data received from the vehicle: '{}'",
                    p.getMessage());
        }
    }

    protected boolean isAwake() {
        return vehicle != null && "online".equals(vehicle.state) && vehicle.vehicle_id != null;
    }

    protected boolean isOnline() {
        return vehicle != null && "online".equals(vehicle.state) && vehicle.vehicle_id != null;
    }

    protected boolean isInMotion() {
        if (driveState != null) {
            if (driveState.speed != null && driveState.shift_state != null) {
                return !"Undefined".equals(driveState.speed)
                        && (!"P".equals(driveState.shift_state) || !"Undefined".equals(driveState.shift_state));
            }
        }
        return false;
    }

    protected boolean isInactive() {
        // vehicle is inactive in case
        // - it does not charge
        // - it has not moved in the observation period
        return isInactive && !isCharging() && !hasMovedInSleepInterval();
    }

    protected boolean isCharging() {
        return chargeState != null && "Charging".equals(chargeState.charging_state);
    }

    protected boolean hasMovedInSleepInterval() {
        return lastLocationChangeTimestamp > (System.currentTimeMillis()
                - (MOVE_THRESHOLD_INTERVAL_MINUTES * 60 * 1000));
    }

    protected boolean allowQuery() {
        return allowWakeUp || (isOnline() && !isInactive());
    }

    protected void setActive() {
        isInactive = false;
        lastLocationChangeTimestamp = System.currentTimeMillis();
        lastLatitude = 0;
        lastLongitude = 0;
    }

    protected boolean checkResponse(Response response, boolean immediatelyFail) {

        if (response != null && response.getStatus() == 200) {
            return true;
        } else {
            apiIntervalErrors++;
            if (immediatelyFail || apiIntervalErrors >= API_MAXIMUM_ERRORS_IN_INTERVAL) {
                if (immediatelyFail) {
                    logger.warn("Got an unsuccessful result, setting vehicle to offline and will try again");
                } else {
                    logger.warn("Reached the maximum number of errors ({}) for the current interval ({} seconds)",
                            API_MAXIMUM_ERRORS_IN_INTERVAL, API_ERROR_INTERVAL_SECONDS);
                }

                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
                eventClient.close();
            } else if ((System.currentTimeMillis() - apiIntervalTimestamp) > 1000 * API_ERROR_INTERVAL_SECONDS) {
                logger.trace("Resetting the error counter. ({} errors in the last interval)", apiIntervalErrors);
                apiIntervalTimestamp = System.currentTimeMillis();
                apiIntervalErrors = 0;
            }
        }

        return false;
    }

    public void setChargeLimit(int percent) {
        JsonObject payloadObject = new JsonObject();
        payloadObject.addProperty("percent", percent);
        sendCommand(COMMAND_SET_CHARGE_LIMIT, gson.toJson(payloadObject), commandTarget);
        requestData(CHARGE_STATE);
    }

    public void setSunroof(String state) {
        JsonObject payloadObject = new JsonObject();
        payloadObject.addProperty("state", state);
        sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), commandTarget);
        requestData(VEHICLE_STATE);
    }

    public void moveSunroof(int percent) {
        JsonObject payloadObject = new JsonObject();
        payloadObject.addProperty("state", "move");
        payloadObject.addProperty("percent", percent);
        sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), commandTarget);
        requestData(VEHICLE_STATE);
    }

    public void setTemperature(float temperature) {
        JsonObject payloadObject = new JsonObject();
        payloadObject.addProperty("driver_temp", temperature);
        payloadObject.addProperty("passenger_temp", temperature);
        sendCommand(COMMAND_SET_TEMP, gson.toJson(payloadObject), commandTarget);
        requestData(CLIMATE_STATE);
    }

    public void openFrunk() {
        JsonObject payloadObject = new JsonObject();
        payloadObject.addProperty("which_trunk", "front");
        sendCommand(COMMAND_TRUNK_OPEN, gson.toJson(payloadObject), commandTarget);
        requestData(VEHICLE_STATE);
    }

    public void openTrunk() {
        JsonObject payloadObject = new JsonObject();
        payloadObject.addProperty("which_trunk", "rear");
        sendCommand(COMMAND_TRUNK_OPEN, gson.toJson(payloadObject), commandTarget);
        requestData(VEHICLE_STATE);
    }

    public void closeTrunk() {
        openTrunk();
    }

    public void setValetMode(boolean b, Integer pin) {
        JsonObject payloadObject = new JsonObject();
        payloadObject.addProperty("on", b);
        if (pin != null) {
            payloadObject.addProperty("password", String.format("%04d", pin));
        }
        sendCommand(COMMAND_SET_VALET_MODE, gson.toJson(payloadObject), commandTarget);
        requestData(CLIMATE_STATE);
    }

    public void resetValetPin() {
        sendCommand(COMMAND_RESET_VALET_PIN, commandTarget);
        requestData(CLIMATE_STATE);
    }

    public void setMaxRangeCharging(boolean b) {
        if (b) {
            sendCommand(COMMAND_CHARGE_MAX, commandTarget);
        } else {
            sendCommand(COMMAND_CHARGE_STD, commandTarget);
        }
        requestData(CHARGE_STATE);
    }

    public void charge(boolean b) {
        if (b) {
            sendCommand(COMMAND_CHARGE_START, commandTarget);
        } else {
            sendCommand(COMMAND_CHARGE_STOP, commandTarget);
        }
        requestData(CHARGE_STATE);
    }

    public void flashLights() {
        sendCommand(COMMAND_FLASH_LIGHTS, commandTarget);
    }

    public void honkHorn() {
        sendCommand(COMMAND_HONK_HORN, commandTarget);
    }

    public void openChargePort() {
        sendCommand(COMMAND_OPEN_CHARGE_PORT, commandTarget);
        requestData(CHARGE_STATE);
    }

    public void lockDoors(boolean b) {
        if (b) {
            sendCommand(COMMAND_DOOR_LOCK, commandTarget);
        } else {
            sendCommand(COMMAND_DOOR_UNLOCK, commandTarget);
        }
        requestData(VEHICLE_STATE);
    }

    public void autoConditioning(boolean b) {
        if (b) {
            sendCommand(COMMAND_AUTO_COND_START, commandTarget);
        } else {
            sendCommand(COMMAND_AUTO_COND_STOP, commandTarget);
        }
        requestData(CLIMATE_STATE);
    }

    public void wakeUp() {
        sendCommand(COMMAND_WAKE_UP, wakeUpTarget);
    }

    protected Vehicle queryVehicle() {
        // get a list of vehicles
        Response response = vehiclesTarget.request(MediaType.APPLICATION_JSON_TYPE)
                .header("Authorization", "Bearer " + logonToken.access_token).get();

        logger.debug("Querying the vehicle : Response : {}:{}", response.getStatus(), response.getStatusInfo());

        if (!checkResponse(response, true)) {
            logger.error("An error occurred while querying the vehicle");
            return null;
        }

        JsonParser parser = new JsonParser();

        JsonObject jsonObject = parser.parse(response.readEntity(String.class)).getAsJsonObject();
        Vehicle[] vehicleArray = gson.fromJson(jsonObject.getAsJsonArray("response"), Vehicle[].class);

        for (int i = 0; i < vehicleArray.length; i++) {
            logger.debug("Querying the vehicle : VIN : {}", vehicleArray[i].vin);
            if (vehicleArray[i].vin.equals(getConfig().get(VIN))) {
                vehicleJSON = gson.toJson(vehicleArray[i]);
                parseAndUpdate("queryVehicle", null, vehicleJSON);
                if (logger.isTraceEnabled()) {
                    logger.trace("Vehicle is id {}/vehicle_id {}/tokens {}", vehicleArray[i].id,
                            vehicleArray[i].vehicle_id, vehicleArray[i].tokens);
                }
                return vehicleArray[i];
            }
        }

        return null;
    }

    protected void queryVehicleAndUpdate() {
        vehicle = queryVehicle();
        parseAndUpdate("queryVehicle", null, vehicleJSON);
    }

    private String getStorageKey() {
        return this.getThing().getUID().getId();
    }

    private ThingStatusDetail authenticate() {
        Storage<Object> storage = storageService.getStorage(TeslaBindingConstants.BINDING_ID);

        String storedToken = (String) storage.get(getStorageKey());
        TokenResponse token = storedToken == null ? null : gson.fromJson(storedToken, TokenResponse.class);
        SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");

        boolean hasExpired = true;

        if (token != null) {
            Calendar calendar = Calendar.getInstance();
            calendar.setTimeInMillis(token.created_at * 1000);
            logger.info("Found a request token created at {}", dateFormatter.format(calendar.getTime()));
            calendar.setTimeInMillis(token.created_at * 1000 + 60 * token.expires_in);

            Date now = new Date();

            if (calendar.getTime().before(now)) {
                logger.info("The token has expired at {}", dateFormatter.format(calendar.getTime()));
                hasExpired = true;
            } else {
                hasExpired = false;
            }
        }

        String username = (String) getConfig().get(USERNAME);

        if (!StringUtils.isEmpty(username) && hasExpired) {
            String password = (String) getConfig().get(PASSWORD);
            return authenticate(username, password);
        }

        if (token == null || StringUtils.isEmpty(token.refresh_token)) {
            return ThingStatusDetail.CONFIGURATION_ERROR;
        }

        TokenRequestRefreshToken tokenRequest = null;
        try {
            tokenRequest = new TokenRequestRefreshToken(token.refresh_token);
        } catch (GeneralSecurityException e) {
            logger.error("An exception occurred while requesting a new token : '{}'", e.getMessage(), e);
        }

        String payLoad = gson.toJson(tokenRequest);

        Response response = tokenTarget.request().post(Entity.entity(payLoad, MediaType.APPLICATION_JSON_TYPE));

        if (response == null) {
            logger.debug("Authenticating : Response was null");
        } else {
            logger.debug("Authenticating : Response : {}:{}", response.getStatus(), response.getStatusInfo());

            if (response.getStatus() == 200 && response.hasEntity()) {
                String responsePayLoad = response.readEntity(String.class);
                TokenResponse tokenResponse = gson.fromJson(responsePayLoad.trim(), TokenResponse.class);

                if (tokenResponse != null && !StringUtils.isEmpty(tokenResponse.access_token)) {
                    storage.put(getStorageKey(), gson.toJson(tokenResponse));
                    this.logonToken = tokenResponse;
                    if (logger.isTraceEnabled()) {
                        logger.trace("Access Token is {}", logonToken.access_token);
                    }
                    return ThingStatusDetail.NONE;
                }

                return ThingStatusDetail.NONE;
            } else if (response.getStatus() == 401) {
                if (!StringUtils.isEmpty(username)) {
                    String password = (String) getConfig().get(PASSWORD);
                    return authenticate(username, password);
                } else {
                    return ThingStatusDetail.CONFIGURATION_ERROR;
                }
            } else if (response.getStatus() == 503 || response.getStatus() == 502) {
                return ThingStatusDetail.COMMUNICATION_ERROR;
            }
        }
        return ThingStatusDetail.CONFIGURATION_ERROR;
    }

    private ThingStatusDetail authenticate(String username, String password) {
        TokenRequest token = null;
        try {
            token = new TokenRequestPassword(username, password);
        } catch (GeneralSecurityException e) {
            logger.error("An exception occurred while building a password request token : '{}'", e.getMessage(), e);
        }

        if (token != null) {
            String payLoad = gson.toJson(token);

            Response response = tokenTarget.request().post(Entity.entity(payLoad, MediaType.APPLICATION_JSON_TYPE));

            if (response != null) {
                logger.debug("Authenticating : Response : {}:{}", response.getStatus(), response.getStatusInfo());

                if (response.getStatus() == 200 && response.hasEntity()) {
                    String responsePayLoad = response.readEntity(String.class);
                    TokenResponse tokenResponse = gson.fromJson(responsePayLoad.trim(), TokenResponse.class);

                    if (StringUtils.isNotEmpty(tokenResponse.access_token)) {
                        Storage<Object> storage = storageService.getStorage(TeslaBindingConstants.BINDING_ID);
                        storage.put(getStorageKey(), gson.toJson(tokenResponse));
                        this.logonToken = tokenResponse;
                        return ThingStatusDetail.NONE;
                    }
                } else if (response.getStatus() == 401) {
                    return ThingStatusDetail.CONFIGURATION_ERROR;
                } else if (response.getStatus() == 503 || response.getStatus() == 502) {
                    return ThingStatusDetail.COMMUNICATION_ERROR;
                }
            }
        }
        return ThingStatusDetail.CONFIGURATION_ERROR;
    }

    protected Runnable fastStateRunnable = () -> {
        if (getThing().getStatus() == ThingStatus.ONLINE) {
            boolean allowQuery = allowQuery();

            if (allowQuery) {
                requestData(DRIVE_STATE);
                requestData(VEHICLE_STATE);
            } else {
                if (vehicle == null) {
                    vehicle = queryVehicle();
                } else if (allowWakeUp) {
                    wakeUp();
                } else {
                    queryVehicleAndUpdate();

                    if (isOnline()) {
                        logger.debug(
                                "Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
                    }
                }
            }
        }

        if (allowWakeUp) {
            updateState(CHANNEL_ALLOWWAKEUP, OnOffType.ON);
        } else {
            updateState(CHANNEL_ALLOWWAKEUP, OnOffType.OFF);
        }

        if (eventThread != null) {
            updateState(CHANNEL_ENABLEEVENTS, OnOffType.ON);
        } else {
            updateState(CHANNEL_ENABLEEVENTS, OnOffType.OFF);
        }
    };

    protected Runnable slowStateRunnable = () -> {
        if (getThing().getStatus() == ThingStatus.ONLINE) {
            boolean allowQuery = allowQuery();

            if (allowQuery) {
                requestData(CHARGE_STATE);
                requestData(CLIMATE_STATE);
                requestData(GUI_STATE);
                queryVehicle(MOBILE_ENABLED_STATE);
                parseAndUpdate("queryVehicle", null, vehicleJSON);
            } else {
                if (vehicle == null) {
                    vehicle = queryVehicle();
                } else if (allowWakeUp) {
                    wakeUp();
                } else {
                    queryVehicleAndUpdate();

                    if (isOnline()) {
                        logger.debug(
                                "Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
                    }
                }
            }
        }
    };

    protected Runnable connectRunnable = () -> {
        try {
            lock.lock();

            if (getThing().getStatus() != ThingStatus.ONLINE) {
                logger.debug("Setting up an authenticated connection to the Tesla back-end");

                ThingStatusDetail authenticationResult = authenticate();

                if (authenticationResult != ThingStatusDetail.NONE) {
                    updateStatus(ThingStatus.OFFLINE, authenticationResult);
                } else {
                    // get a list of vehicles
                    Response response = vehiclesTarget.request(MediaType.APPLICATION_JSON_TYPE)
                            .header("Authorization", "Bearer " + logonToken.access_token).get();

                    if (response != null && response.getStatus() == 200 && response.hasEntity()) {
                        if ((vehicle = queryVehicle()) != null) {
                            logger.debug("Found the vehicle with VIN '{}' in the list of vehicles you own",
                                    getConfig().get(VIN));
                            updateStatus(ThingStatus.ONLINE);
                            apiIntervalErrors = 0;
                            apiIntervalTimestamp = System.currentTimeMillis();
                        } else {
                            logger.warn("Unable to find the vehicle with VIN '{}' in the list of vehicles you own",
                                    getConfig().get(VIN));
                            updateStatus(ThingStatus.OFFLINE);
                        }
                    } else {
                        if (response != null) {
                            logger.error("Error fetching the list of vehicles : {}:{}", response.getStatus(),
                                    response.getStatusInfo());
                            updateStatus(ThingStatus.OFFLINE);
                        }
                    }
                }
            }
        } catch (Exception e) {
            logger.error("An exception occurred while connecting to the Tesla back-end: '{}'", e.getMessage());
        } finally {
            lock.unlock();
        }
    };

    protected Runnable eventRunnable = new Runnable() {
        Response eventResponse;
        BufferedReader eventBufferedReader;
        InputStreamReader eventInputStreamReader;
        boolean isEstablished = false;

        protected boolean establishEventStream() {
            try {
                if (!isEstablished) {
                    eventBufferedReader = null;

                    eventClient = ClientBuilder.newClient()
                            .property(ClientProperties.CONNECT_TIMEOUT, EVENT_STREAM_CONNECT_TIMEOUT)
                            .property(ClientProperties.READ_TIMEOUT, EVENT_STREAM_READ_TIMEOUT)
                            .register(new Authenticator((String) getConfig().get(USERNAME), vehicle.tokens[0]));
                    eventTarget = eventClient.target(URI_EVENT).path(vehicle.vehicle_id + "/").queryParam("values",
                            StringUtils.join(EventKeys.values(), ',', 1, EventKeys.values().length));
                    eventResponse = eventTarget.request(MediaType.TEXT_PLAIN_TYPE).get();

                    logger.debug("Event Stream : Establishing the event stream : Response : {}:{}",
                            eventResponse.getStatus(), eventResponse.getStatusInfo());

                    if (eventResponse.getStatus() == 200) {
                        InputStream dummy = (InputStream) eventResponse.getEntity();
                        eventInputStreamReader = new InputStreamReader(dummy);
                        eventBufferedReader = new BufferedReader(eventInputStreamReader);
                        isEstablished = true;
                    } else if (eventResponse.getStatus() == 401) {
                        isEstablished = false;
                    } else {
                        isEstablished = false;
                    }

                    if (!isEstablished) {
                        eventIntervalErrors++;
                        if (eventIntervalErrors >= EVENT_MAXIMUM_ERRORS_IN_INTERVAL) {
                            logger.warn(
                                    "Reached the maximum number of errors ({}) for the current interval ({} seconds)",
                                    EVENT_MAXIMUM_ERRORS_IN_INTERVAL, EVENT_ERROR_INTERVAL_SECONDS);
                            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
                            eventClient.close();
                        }

                        if ((System.currentTimeMillis() - eventIntervalTimestamp) > 1000
                                * EVENT_ERROR_INTERVAL_SECONDS) {
                            logger.trace("Resetting the error counter. ({} errors in the last interval)",
                                    eventIntervalErrors);
                            eventIntervalTimestamp = System.currentTimeMillis();
                            eventIntervalErrors = 0;
                        }
                    }
                }
            } catch (Exception e) {
                logger.error(
                        "Event Stream : An exception occurred while establishing the event stream for the vehicle: '{}'",
                        e.getMessage());
                isEstablished = false;
            }

            return isEstablished;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    if (getThing().getStatus() == ThingStatus.ONLINE) {
                        if (isAwake()) {
                            if (establishEventStream()) {
                                String line = eventBufferedReader.readLine();

                                while (line != null) {
                                    logger.debug("Event Stream : Received an event: '{}'", line);
                                    String vals[] = line.split(",");
                                    long currentTimeStamp = Long.valueOf(vals[0]);
                                    long systemTimeStamp = System.currentTimeMillis();
                                    if (logger.isDebugEnabled()) {
                                        SimpleDateFormat dateFormatter = new SimpleDateFormat(
                                                "yyyy-MM-dd'T'HH:mm:ss.SSS");
                                        logger.debug("STS {} CTS {} Delta {}",
                                                dateFormatter.format(new Date(systemTimeStamp)),
                                                dateFormatter.format(new Date(currentTimeStamp)),
                                                systemTimeStamp - currentTimeStamp);
                                    }
                                    if (systemTimeStamp - currentTimeStamp < EVENT_TIMESTAMP_AGE_LIMIT) {
                                        if (currentTimeStamp > lastTimeStamp) {
                                            lastTimeStamp = Long.valueOf(vals[0]);
                                            if (logger.isDebugEnabled()) {
                                                SimpleDateFormat dateFormatter = new SimpleDateFormat(
                                                        "yyyy-MM-dd'T'HH:mm:ss.SSS");
                                                logger.debug("Event Stream : Event stamp is {}",
                                                        dateFormatter.format(new Date(lastTimeStamp)));
                                            }
                                            for (int i = 0; i < EventKeys.values().length; i++) {
                                                TeslaChannelSelector selector = TeslaChannelSelector
                                                        .getValueSelectorFromRESTID(
                                                                (EventKeys.values()[i]).toString());
                                                if (!selector.isProperty()) {
                                                    State newState = teslaChannelSelectorProxy.getState(vals[i],
                                                            selector, editProperties());
                                                    if (newState != null && !"".equals(vals[i])) {
                                                        updateState(selector.getChannelID(), newState);
                                                    } else {
                                                        updateState(selector.getChannelID(), UnDefType.UNDEF);
                                                    }
                                                } else {
                                                    Map<String, String> properties = editProperties();
                                                    properties.put(selector.getChannelID(),
                                                            (selector.getState(vals[i])).toString());
                                                    updateProperties(properties);
                                                }
                                            }
                                        } else {
                                            if (logger.isDebugEnabled()) {
                                                SimpleDateFormat dateFormatter = new SimpleDateFormat(
                                                        "yyyy-MM-dd'T'HH:mm:ss.SSS");
                                                logger.debug(
                                                        "Event Stream : Discarding an event with an out of sync timestamp {} (last is {})",
                                                        dateFormatter.format(new Date(currentTimeStamp)),
                                                        dateFormatter.format(new Date(lastTimeStamp)));
                                            }
                                        }
                                    } else {
                                        if (logger.isDebugEnabled()) {
                                            SimpleDateFormat dateFormatter = new SimpleDateFormat(
                                                    "yyyy-MM-dd'T'HH:mm:ss.SSS");
                                            logger.debug(
                                                    "Event Stream : Discarding an event that differs {} ms from the system time: {} (system is {})",
                                                    systemTimeStamp - currentTimeStamp,
                                                    dateFormatter.format(currentTimeStamp),
                                                    dateFormatter.format(systemTimeStamp));
                                        }
                                        if (systemTimeStamp - currentTimeStamp > EVENT_TIMESTAMP_MAX_DELTA) {
                                            if (logger.isTraceEnabled()) {
                                                logger.trace("Event Stream : The event stream will be reset");
                                            }
                                            isEstablished = false;
                                        }
                                    }

                                    line = eventBufferedReader.readLine();
                                }

                                if (line == null) {
                                    if (logger.isTraceEnabled()) {
                                        logger.trace("Event Stream : The end of stream was reached");
                                    }
                                    isEstablished = false;
                                }
                            }
                        } else {
                            logger.debug("Event stream : The vehicle is not awake");
                            if (vehicle != null && allowWakeUp) {
                                // wake up the vehicle until streaming token <> 0
                                logger.debug("Event stream : Waking up the vehicle");
                                wakeUp();
                            } else {
                                logger.debug("Event stream : Querying the vehicle");
                                vehicle = queryVehicle();
                            }
                        }
                    } else {
                        Thread.sleep(250);
                    }
                } catch (IOException | NumberFormatException e) {
                    if (logger.isErrorEnabled()) {
                        logger.error("Event Stream : An exception occurred while reading events : '{}'",
                                e.getMessage());
                    }
                    isEstablished = false;
                } catch (InterruptedException e) {
                    isEstablished = false;
                }

                if (Thread.interrupted()) {
                    logger.debug("Event Stream : the Event Stream was interrupted");
                    return;
                }
            }
        }
    };

    protected class Request implements Runnable {

        private String request;
        private String payLoad;
        private WebTarget target;

        public Request(String request, String payLoad, WebTarget target) {
            this.request = request;
            this.payLoad = payLoad;
            this.target = target;
        }

        @Override
        public void run() {
            try {
                String result = "";

                if (getThing().getStatus() == ThingStatus.ONLINE) {
                    if (request.equals(COMMAND_WAKE_UP) || isAwake()) {
                        result = invokeAndParse(request, payLoad, target);
                    }
                }

                if (result != null && !"".equals(result)) {
                    parseAndUpdate(request, payLoad, result);
                }
            } catch (Exception e) {
                logger.error("An exception occurred while executing a request to the vehicle: '{}'",
                        e.getMessage());
            }
        }
    }

    protected class Authenticator implements ClientRequestFilter {
        private final String user;
        private final String password;

        public Authenticator(String user, String password) {
            this.user = user;
            this.password = password;
        }

        @Override
        public void filter(ClientRequestContext requestContext) throws IOException {
            MultivaluedMap<String, Object> headers = requestContext.getHeaders();
            final String basicAuthentication = getBasicAuthentication();
            headers.add("Authorization", basicAuthentication);
        }

        private String getBasicAuthentication() {
            String token = this.user + ":" + this.password;
            return "Basic " + Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8));
        }
    }
}