[tesla] Remove (broken) options to obtain refresh token through credentials (#12537)
* Remove (broken) option to obtain refresh token through credentials * Remove outdated event streaming code * Update README Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -112,6 +112,4 @@ public class TeslaBindingConstants {
|
||||
public static final String CONFIG_ALLOWWAKEUPFORCOMMANDS = "allowWakeupForCommands";
|
||||
public static final String CONFIG_ENABLEEVENTS = "enableEvents";
|
||||
public static final String CONFIG_REFRESHTOKEN = "refreshToken";
|
||||
public static final String CONFIG_USERNAME = "username";
|
||||
public static final String CONFIG_PASSWORD = "password";
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 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.command;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.client.ClientBuilder;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.tesla.internal.TeslaBindingConstants;
|
||||
import org.openhab.binding.tesla.internal.discovery.TeslaAccountDiscoveryService;
|
||||
import org.openhab.binding.tesla.internal.handler.TeslaSSOHandler;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.io.console.Console;
|
||||
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
|
||||
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.util.UIDUtils;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.component.annotations.ReferenceCardinality;
|
||||
|
||||
/**
|
||||
* Console commands for interacting with the Tesla integration
|
||||
*
|
||||
* @author Nicolai Grødum - Initial contribution
|
||||
* @author Kai Kreuzer - refactored to use Tesla account thing
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = ConsoleCommandExtension.class)
|
||||
public class TeslaCommandExtension extends AbstractConsoleCommandExtension {
|
||||
|
||||
private static final String CMD_LOGIN = "login";
|
||||
|
||||
@Reference(cardinality = ReferenceCardinality.OPTIONAL)
|
||||
private @Nullable ClientBuilder injectedClientBuilder;
|
||||
|
||||
private final TeslaAccountDiscoveryService teslaAccountDiscoveryService;
|
||||
private final HttpClientFactory httpClientFactory;
|
||||
|
||||
@Activate
|
||||
public TeslaCommandExtension(@Reference TeslaAccountDiscoveryService teslaAccountDiscoveryService,
|
||||
@Reference HttpClientFactory httpClientFactory) {
|
||||
super("tesla", "Interact with the Tesla integration.");
|
||||
this.teslaAccountDiscoveryService = teslaAccountDiscoveryService;
|
||||
this.httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(String[] args, Console console) {
|
||||
if (args.length > 0) {
|
||||
String subCommand = args[0];
|
||||
switch (subCommand) {
|
||||
case CMD_LOGIN:
|
||||
if (args.length == 1) {
|
||||
try {
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
|
||||
console.print("Username (email): ");
|
||||
String username = br.readLine();
|
||||
console.println(username);
|
||||
|
||||
console.print("Password: ");
|
||||
|
||||
String pwd = br.readLine();
|
||||
console.println("");
|
||||
console.println("Attempting login...");
|
||||
login(console, username, pwd);
|
||||
} catch (Exception e) {
|
||||
console.println(e.toString());
|
||||
}
|
||||
} else if (args.length == 3) {
|
||||
login(console, args[1], args[2]);
|
||||
} else {
|
||||
printUsage(console);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.println("Unknown command '" + subCommand + "'");
|
||||
printUsage(console);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
printUsage(console);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getUsages() {
|
||||
return Arrays.asList(buildCommandUsage(CMD_LOGIN + " [<user email>] [<password>]",
|
||||
"Authenticates the user and provides a refresh token."));
|
||||
}
|
||||
|
||||
private void login(Console console, String username, String password) {
|
||||
TeslaSSOHandler ssoHandler = new TeslaSSOHandler(httpClientFactory.getCommonHttpClient());
|
||||
|
||||
String refreshToken = ssoHandler.authenticate(username, password);
|
||||
if (refreshToken != null) {
|
||||
console.println("Refresh token: " + refreshToken);
|
||||
|
||||
ThingUID thingUID = new ThingUID(TeslaBindingConstants.THING_TYPE_ACCOUNT, UIDUtils.encode(username));
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel("Tesla Account")
|
||||
.withProperty(TeslaBindingConstants.CONFIG_REFRESHTOKEN, refreshToken)
|
||||
.withProperty(TeslaBindingConstants.CONFIG_USERNAME, username)
|
||||
.withRepresentationProperty(TeslaBindingConstants.CONFIG_USERNAME).build();
|
||||
teslaAccountDiscoveryService.thingDiscovered(result);
|
||||
} else {
|
||||
console.println("Failed to retrieve refresh token");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 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.discovery;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.tesla.internal.TeslaHandlerFactory;
|
||||
import org.openhab.binding.tesla.internal.command.TeslaCommandExtension;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* This is a discovery service, is used by the {@link TeslaCommandExtension} for
|
||||
* automatically creating Tesla accounts.
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution
|
||||
*
|
||||
*/
|
||||
@Component(service = { TeslaAccountDiscoveryService.class, DiscoveryService.class })
|
||||
@NonNullByDefault
|
||||
public class TeslaAccountDiscoveryService extends AbstractDiscoveryService {
|
||||
|
||||
public TeslaAccountDiscoveryService() throws IllegalArgumentException {
|
||||
super(TeslaHandlerFactory.SUPPORTED_THING_TYPES_UIDS, 10, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activate(@Nullable Map<String, Object> configProperties) {
|
||||
super.activate(configProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void thingDiscovered(DiscoveryResult discoveryResult) {
|
||||
super.thingDiscovered(discoveryResult);
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,9 @@ package org.openhab.binding.tesla.internal.handler;
|
||||
|
||||
import static org.openhab.binding.tesla.internal.TeslaBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@@ -30,20 +27,15 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.ws.rs.client.Client;
|
||||
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.openhab.binding.tesla.internal.TeslaBindingConstants;
|
||||
import org.openhab.binding.tesla.internal.discovery.TeslaVehicleDiscoveryService;
|
||||
import org.openhab.binding.tesla.internal.protocol.Vehicle;
|
||||
import org.openhab.binding.tesla.internal.protocol.VehicleConfig;
|
||||
import org.openhab.binding.tesla.internal.protocol.sso.TokenResponse;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
@@ -269,33 +261,11 @@ public class TeslaAccountHandler extends BaseBridgeHandler {
|
||||
}
|
||||
|
||||
if (hasExpired) {
|
||||
String username = (String) getConfig().get(CONFIG_USERNAME);
|
||||
String password = (String) getConfig().get(CONFIG_PASSWORD);
|
||||
String refreshToken = (String) getConfig().get(CONFIG_REFRESHTOKEN);
|
||||
|
||||
if (refreshToken == null || refreshToken.isEmpty()) {
|
||||
if (username != null && !username.isEmpty() && password != null && !password.isEmpty()) {
|
||||
try {
|
||||
refreshToken = ssoHandler.authenticate(username, password);
|
||||
} catch (Exception e) {
|
||||
logger.error("An exception occurred while obtaining refresh token with username/password: '{}'",
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
if (refreshToken != null) {
|
||||
// store refresh token from SSO endpoint in config, clear the password
|
||||
Configuration cfg = editConfiguration();
|
||||
cfg.put(TeslaBindingConstants.CONFIG_REFRESHTOKEN, refreshToken);
|
||||
cfg.remove(TeslaBindingConstants.CONFIG_PASSWORD);
|
||||
updateConfiguration(cfg);
|
||||
} else {
|
||||
return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Failed to obtain refresh token with username/password.");
|
||||
}
|
||||
} else {
|
||||
return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Neither a refresh token nor credentials are provided.");
|
||||
}
|
||||
return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"No refresh token is provided.");
|
||||
}
|
||||
|
||||
this.logonToken = ssoHandler.getAccessToken(refreshToken);
|
||||
@@ -432,28 +402,6 @@ public class TeslaAccountHandler extends BaseBridgeHandler {
|
||||
}
|
||||
};
|
||||
|
||||
public static 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));
|
||||
}
|
||||
}
|
||||
|
||||
protected class Request implements Runnable {
|
||||
|
||||
private static final int NO_OF_RETRIES = 3;
|
||||
|
||||
@@ -14,11 +14,6 @@ package org.openhab.binding.tesla.internal.handler;
|
||||
|
||||
import static org.openhab.binding.tesla.internal.TeslaBindingConstants.*;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.Iterator;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
@@ -27,17 +22,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.util.FormContentProvider;
|
||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.util.Fields;
|
||||
import org.eclipse.jetty.util.Fields.Field;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.openhab.binding.tesla.internal.protocol.sso.AuthorizationCodeExchangeRequest;
|
||||
import org.openhab.binding.tesla.internal.protocol.sso.AuthorizationCodeExchangeResponse;
|
||||
import org.openhab.binding.tesla.internal.protocol.sso.RefreshTokenRequest;
|
||||
import org.openhab.binding.tesla.internal.protocol.sso.TokenResponse;
|
||||
import org.slf4j.Logger;
|
||||
@@ -93,178 +80,6 @@ public class TeslaSSOHandler {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates using username/password against Tesla SSO endpoints.
|
||||
*
|
||||
* @param username Username
|
||||
* @param password Password
|
||||
* @return Refresh token for use with {@link getAccessToken}
|
||||
*/
|
||||
@Nullable
|
||||
public String authenticate(String username, String password) {
|
||||
String codeVerifier = generateRandomString(86);
|
||||
String codeChallenge = null;
|
||||
String state = generateRandomString(10);
|
||||
|
||||
try {
|
||||
codeChallenge = getCodeChallenge(codeVerifier);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
logger.error("An exception occurred while building login page request: '{}'", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
final org.eclipse.jetty.client.api.Request loginPageRequest = httpClient
|
||||
.newRequest(URI_SSO + "/" + PATH_AUTHORIZE);
|
||||
loginPageRequest.method(HttpMethod.GET);
|
||||
loginPageRequest.followRedirects(false);
|
||||
|
||||
addQueryParameters(loginPageRequest, codeChallenge, state);
|
||||
|
||||
ContentResponse loginPageResponse = executeHttpRequest(loginPageRequest);
|
||||
if (loginPageResponse == null
|
||||
|| (loginPageResponse.getStatus() != 200 && loginPageResponse.getStatus() != 302)) {
|
||||
logger.debug("Failed to obtain SSO login page, response status code: {}",
|
||||
(loginPageResponse != null ? loginPageResponse.getStatus() : "no response"));
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.debug("Obtained SSO login page");
|
||||
|
||||
String authorizationCode = null;
|
||||
|
||||
if (loginPageResponse.getStatus() == 302) {
|
||||
String redirectLocation = loginPageResponse.getHeaders().get(HttpHeader.LOCATION);
|
||||
if (isValidRedirectLocation(redirectLocation)) {
|
||||
authorizationCode = extractAuthorizationCodeFromUri(redirectLocation);
|
||||
} else {
|
||||
logger.debug("Unexpected redirect location received when fetching login page: {}", redirectLocation);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
Fields postData = new Fields();
|
||||
|
||||
try {
|
||||
Document doc = Jsoup.parse(loginPageResponse.getContentAsString());
|
||||
logger.trace("{}", doc.toString());
|
||||
Element loginForm = doc.getElementsByTag("form").first();
|
||||
|
||||
Iterator<Element> elIt = loginForm.getElementsByTag("input").iterator();
|
||||
while (elIt.hasNext()) {
|
||||
Element input = elIt.next();
|
||||
if (input.attr("type").equalsIgnoreCase("hidden")) {
|
||||
postData.add(input.attr("name"), input.attr("value"));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to parse login page: {}", e.getMessage());
|
||||
logger.debug("login page response {}", loginPageResponse.getContentAsString());
|
||||
return null;
|
||||
}
|
||||
|
||||
postData.add("identity", username);
|
||||
postData.add("credential", password);
|
||||
|
||||
final org.eclipse.jetty.client.api.Request formSubmitRequest = httpClient
|
||||
.newRequest(URI_SSO + "/" + PATH_AUTHORIZE);
|
||||
formSubmitRequest.method(HttpMethod.POST);
|
||||
formSubmitRequest.content(new FormContentProvider(postData));
|
||||
formSubmitRequest.followRedirects(false); // this should return a 302 ideally, but that location doesn't
|
||||
// exist
|
||||
addQueryParameters(formSubmitRequest, codeChallenge, state);
|
||||
|
||||
ContentResponse formSubmitResponse = executeHttpRequest(formSubmitRequest);
|
||||
if (formSubmitResponse == null || formSubmitResponse.getStatus() != 302) {
|
||||
logger.debug("Failed to obtain code from SSO login page when submitting form, response status code: {}",
|
||||
(formSubmitResponse != null ? formSubmitResponse.getStatus() : "no response"));
|
||||
return null;
|
||||
}
|
||||
|
||||
String redirectLocation = formSubmitResponse.getHeaders().get(HttpHeader.LOCATION);
|
||||
if (!isValidRedirectLocation(redirectLocation)) {
|
||||
logger.debug("Redirect location not set or doesn't match expected callback URI {}: {}", URI_CALLBACK,
|
||||
redirectLocation);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.debug("Obtained valid redirect location");
|
||||
authorizationCode = extractAuthorizationCodeFromUri(redirectLocation);
|
||||
}
|
||||
|
||||
if (authorizationCode == null) {
|
||||
logger.debug("Did not receive an authorization code");
|
||||
return null;
|
||||
}
|
||||
|
||||
// exchange authorization code for SSO access + refresh token
|
||||
AuthorizationCodeExchangeRequest request = new AuthorizationCodeExchangeRequest(authorizationCode,
|
||||
codeVerifier);
|
||||
String payload = gson.toJson(request);
|
||||
|
||||
final org.eclipse.jetty.client.api.Request tokenExchangeRequest = httpClient
|
||||
.newRequest(URI_SSO + "/" + PATH_TOKEN);
|
||||
tokenExchangeRequest.content(new StringContentProvider(payload));
|
||||
tokenExchangeRequest.header(HttpHeader.CONTENT_TYPE, "application/json");
|
||||
tokenExchangeRequest.method(HttpMethod.POST);
|
||||
|
||||
ContentResponse response = executeHttpRequest(tokenExchangeRequest);
|
||||
if (response != null && response.getStatus() == 200) {
|
||||
String responsePayload = response.getContentAsString();
|
||||
AuthorizationCodeExchangeResponse ssoTokenResponse = gson.fromJson(responsePayload.trim(),
|
||||
AuthorizationCodeExchangeResponse.class);
|
||||
if (ssoTokenResponse != null && ssoTokenResponse.token_type != null
|
||||
&& !ssoTokenResponse.access_token.isEmpty()) {
|
||||
logger.debug("Obtained valid SSO refresh token");
|
||||
return ssoTokenResponse.refresh_token;
|
||||
}
|
||||
} else {
|
||||
logger.debug("An error occurred while exchanging authorization code for SSO refresh token: {}",
|
||||
(response != null ? response.getStatus() : "no response"));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Boolean isValidRedirectLocation(@Nullable String redirectLocation) {
|
||||
return redirectLocation != null && redirectLocation.startsWith(URI_CALLBACK);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String extractAuthorizationCodeFromUri(String uri) {
|
||||
Field code = httpClient.newRequest(uri).getParams().get("code");
|
||||
return code != null ? code.getValue() : null;
|
||||
}
|
||||
|
||||
private String getCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(codeVerifier.getBytes());
|
||||
|
||||
StringBuilder hashStr = new StringBuilder(hash.length * 2);
|
||||
for (byte b : hash) {
|
||||
hashStr.append(String.format("%02x", b));
|
||||
}
|
||||
|
||||
return Base64.getUrlEncoder().encodeToString(hashStr.toString().getBytes());
|
||||
}
|
||||
|
||||
private String generateRandomString(int length) {
|
||||
Random random = new Random();
|
||||
|
||||
String generatedString = random.ints('a', 'z' + 1).limit(length)
|
||||
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
|
||||
|
||||
return generatedString;
|
||||
}
|
||||
|
||||
private void addQueryParameters(org.eclipse.jetty.client.api.Request request, String codeChallenge, String state) {
|
||||
request.param("client_id", CLIENT_ID);
|
||||
request.param("code_challenge", codeChallenge);
|
||||
request.param("code_challenge_method", "S256");
|
||||
request.param("redirect_uri", URI_CALLBACK);
|
||||
request.param("response_type", "code");
|
||||
request.param("scope", SSO_SCOPES);
|
||||
request.param("state", state);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ContentResponse executeHttpRequest(org.eclipse.jetty.client.api.Request request) {
|
||||
request.timeout(10, TimeUnit.SECONDS);
|
||||
|
||||
@@ -14,14 +14,9 @@ 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.math.RoundingMode;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -29,7 +24,6 @@ import java.util.Set;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.measure.quantity.Temperature;
|
||||
import javax.ws.rs.ProcessingException;
|
||||
@@ -41,10 +35,8 @@ import javax.ws.rs.core.Response;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
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.handler.TeslaAccountHandler.Authenticator;
|
||||
import org.openhab.binding.tesla.internal.handler.TeslaAccountHandler.Request;
|
||||
import org.openhab.binding.tesla.internal.protocol.ChargeState;
|
||||
import org.openhab.binding.tesla.internal.protocol.ClimateState;
|
||||
@@ -69,7 +61,6 @@ import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -88,13 +79,8 @@ import com.google.gson.JsonParser;
|
||||
*/
|
||||
public class TeslaVehicleHandler extends BaseThingHandler {
|
||||
|
||||
private static final int EVENT_STREAM_PAUSE = 5000;
|
||||
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 EVENT_MAXIMUM_ERRORS_IN_INTERVAL = 10;
|
||||
private static final int EVENT_ERROR_INTERVAL_SECONDS = 15;
|
||||
private static final int API_SLEEP_INTERVAL_MINUTES = 20;
|
||||
private static final int MOVE_THRESHOLD_INTERVAL_MINUTES = 5;
|
||||
|
||||
@@ -113,7 +99,6 @@ public class TeslaVehicleHandler extends BaseThingHandler {
|
||||
|
||||
protected boolean allowWakeUp;
|
||||
protected boolean allowWakeUpForCommands;
|
||||
protected boolean enableEvents = false;
|
||||
protected long lastTimeStamp;
|
||||
protected long apiIntervalTimestamp;
|
||||
protected int apiIntervalErrors;
|
||||
@@ -184,13 +169,6 @@ public class TeslaVehicleHandler extends BaseThingHandler {
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
if (enableEvents) {
|
||||
if (eventThread == null) {
|
||||
eventThread = new Thread(eventRunnable, "openHAB-Tesla-Events-" + getThing().getUID());
|
||||
eventThread.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1015,173 +993,4 @@ public class TeslaVehicleHandler extends BaseThingHandler {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected Runnable eventRunnable = new Runnable() {
|
||||
Response eventResponse;
|
||||
BufferedReader eventBufferedReader;
|
||||
InputStreamReader eventInputStreamReader;
|
||||
boolean isEstablished = false;
|
||||
|
||||
protected boolean establishEventStream() {
|
||||
try {
|
||||
if (!isEstablished) {
|
||||
eventBufferedReader = null;
|
||||
|
||||
eventClient = clientBuilder.build()
|
||||
.register(new Authenticator((String) getConfig().get(CONFIG_USERNAME), vehicle.tokens[0]));
|
||||
eventTarget = eventClient.target(URI_EVENT).path(vehicle.vehicle_id + "/").queryParam("values",
|
||||
Arrays.asList(EventKeys.values()).stream().skip(1).map(Enum::toString)
|
||||
.collect(Collectors.joining(",")));
|
||||
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) {
|
||||
logger.trace("Event stream: The event stream will be reset");
|
||||
isEstablished = false;
|
||||
}
|
||||
}
|
||||
line = eventBufferedReader.readLine();
|
||||
}
|
||||
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) {
|
||||
if (allowWakeUp) {
|
||||
// wake up the vehicle until streaming token <> 0
|
||||
logger.debug("Event stream: Waking up the vehicle");
|
||||
wakeUp();
|
||||
}
|
||||
} else {
|
||||
vehicle = queryVehicle();
|
||||
}
|
||||
Thread.sleep(EVENT_STREAM_PAUSE);
|
||||
}
|
||||
}
|
||||
} catch (IOException | NumberFormatException e) {
|
||||
logger.debug("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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ package org.openhab.binding.tesla.internal.protocol.sso;
|
||||
* The {@link TokenResponse} is a datastructure to capture
|
||||
* authentication response from Tesla Remote Service
|
||||
*
|
||||
* @author Nicolai Grødum
|
||||
* @author Nicolai Grødum - Initial contribution
|
||||
*/
|
||||
public class TokenResponse {
|
||||
|
||||
|
||||
@@ -18,12 +18,8 @@ thing-type.tesla.modely.description = A Tesla Model Y Vehicle
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.tesla.account.password.label = Password
|
||||
thing-type.config.tesla.account.password.description = Password for the Tesla Remote Service
|
||||
thing-type.config.tesla.account.refreshToken.label = Refresh Token
|
||||
thing-type.config.tesla.account.refreshToken.description = Refresh token for account authentication. Use "openhab:tesla" to retrieve it.
|
||||
thing-type.config.tesla.account.username.label = Username
|
||||
thing-type.config.tesla.account.username.description = Username for the Tesla Remote Service, e.g email address.
|
||||
thing-type.config.tesla.account.refreshToken.description = Refresh token for account authentication. Please see the documentation on how to get hold of it.
|
||||
thing-type.config.tesla.model3.allowWakeup.label = Allow Wake-Up
|
||||
thing-type.config.tesla.model3.allowWakeup.description = Allows waking up the vehicle. Caution: This can result in huge vampire drain!
|
||||
thing-type.config.tesla.model3.allowWakeupForCommands.label = Allow Wake-Up For Commands
|
||||
|
||||
@@ -11,18 +11,7 @@
|
||||
<config-description>
|
||||
<parameter name="refreshToken" type="text" required="false">
|
||||
<label>Refresh Token</label>
|
||||
<description>Refresh token for account authentication. Use "openhab:tesla" to retrieve it.</description>
|
||||
</parameter>
|
||||
<parameter name="username" type="text" required="false">
|
||||
<advanced>true</advanced>
|
||||
<label>Username</label>
|
||||
<description>Username for the Tesla Remote Service, e.g email address.</description>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="false">
|
||||
<advanced>true</advanced>
|
||||
<context>password</context>
|
||||
<label>Password</label>
|
||||
<description>Password for the Tesla Remote Service</description>
|
||||
<description>Refresh token for account authentication. Please see the documentation on how to get hold of it.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
Reference in New Issue
Block a user