Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tesla] Adapt binding to changed API from Tesla backend #14924

Merged
merged 2 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 70 additions & 91 deletions bundles/org.openhab.binding.tesla/README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class TeslaBindingConstants {
public static final String API_NAME = "Tesla Client API";
public static final String API_VERSION = "api/1/";
public static final String PATH_COMMAND = "command/{cmd}";
public static final String PATH_DATA_REQUEST = "data_request/{cmd}";
public static final String PATH_DATA_REQUEST = "vehicle_data";
public static final String PATH_VEHICLE_ID = "/{vid}/";
public static final String PATH_WAKE_UP = "wake_up";
public static final String PATH_ACCESS_TOKEN = "oauth/token";
Expand Down Expand Up @@ -71,19 +71,11 @@ public class TeslaBindingConstants {
public static final String COMMAND_WAKE_UP = "wake_up";
public static final String DATA_THROTTLE = "datathrottle";

// Tesla REST API vehicle states
public static final String CHARGE_STATE = "charge_state";
public static final String CLIMATE_STATE = "climate_state";
public static final String DRIVE_STATE = "drive_state";
public static final String GUI_STATE = "gui_settings";
public static final String MOBILE_ENABLED_STATE = "mobile_enabled";
public static final String VEHICLE_STATE = "vehicle_state";
public static final String VEHICLE_CONFIG = "vehicle_config";

public static final String BINDING_ID = "tesla";

// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_VEHICLE = new ThingTypeUID(BINDING_ID, "vehicle");
public static final ThingTypeUID THING_TYPE_MODELS = new ThingTypeUID(BINDING_ID, "models");
public static final ThingTypeUID THING_TYPE_MODEL3 = new ThingTypeUID(BINDING_ID, "model3");
public static final ThingTypeUID THING_TYPE_MODELX = new ThingTypeUID(BINDING_ID, "modelx");
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.openhab.core.io.net.http.WebSocketFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeMigrationService;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
Expand All @@ -50,21 +51,24 @@ public class TeslaHandlerFactory extends BaseThingHandlerFactory {
private static final int EVENT_STREAM_CONNECT_TIMEOUT = 3;
private static final int EVENT_STREAM_READ_TIMEOUT = 200;

public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_MODELS,
THING_TYPE_MODEL3, THING_TYPE_MODELX, THING_TYPE_MODELY);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_VEHICLE,
THING_TYPE_MODELS, THING_TYPE_MODEL3, THING_TYPE_MODELX, THING_TYPE_MODELY);

private final ClientBuilder clientBuilder;
private final HttpClientFactory httpClientFactory;
private final WebSocketFactory webSocketFactory;
private final ThingTypeMigrationService thingTypeMigrationService;

@Activate
public TeslaHandlerFactory(@Reference ClientBuilder clientBuilder, @Reference HttpClientFactory httpClientFactory,
final @Reference WebSocketFactory webSocketFactory) {
final @Reference WebSocketFactory webSocketFactory,
final @Reference ThingTypeMigrationService thingTypeMigrationService) {
this.clientBuilder = clientBuilder //
.connectTimeout(EVENT_STREAM_CONNECT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(EVENT_STREAM_READ_TIMEOUT, TimeUnit.SECONDS);
this.httpClientFactory = httpClientFactory;
this.webSocketFactory = webSocketFactory;
this.thingTypeMigrationService = thingTypeMigrationService;
}

@Override
Expand All @@ -77,7 +81,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();

if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) {
return new TeslaAccountHandler((Bridge) thing, clientBuilder.build(), httpClientFactory);
return new TeslaAccountHandler((Bridge) thing, clientBuilder.build(), httpClientFactory,
thingTypeMigrationService);
} else {
return new TeslaVehicleHandler(thing, webSocketFactory);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,31 +81,15 @@ public void deactivate() {

@Override
public void vehicleFound(Vehicle vehicle, VehicleConfig vehicleConfig) {
ThingTypeUID type = identifyModel(vehicleConfig);
ThingTypeUID type = vehicleConfig == null ? TeslaBindingConstants.THING_TYPE_VEHICLE
: vehicleConfig.identifyModel();
if (type != null) {
logger.debug("Found a {} vehicle", type.getId());
ThingUID thingUID = new ThingUID(type, handler.getThing().getUID(), vehicle.vin);
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withLabel(vehicle.display_name)
.withBridge(handler.getThing().getUID()).withProperty(TeslaBindingConstants.VIN, vehicle.vin)
.build();
thingDiscovered(discoveryResult);
}
}

private ThingTypeUID identifyModel(VehicleConfig vehicleConfig) {
logger.debug("Found a {} vehicle", vehicleConfig.car_type);
switch (vehicleConfig.car_type) {
case "models":
case "models2":
return TeslaBindingConstants.THING_TYPE_MODELS;
case "modelx":
return TeslaBindingConstants.THING_TYPE_MODELX;
case "model3":
return TeslaBindingConstants.THING_TYPE_MODEL3;
case "modely":
return TeslaBindingConstants.THING_TYPE_MODELY;
default:
logger.debug("Found unknown vehicle type '{}' - ignoring it.", vehicleConfig.car_type);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
import javax.ws.rs.core.MediaType;
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.VehicleData;
import org.openhab.binding.tesla.internal.protocol.sso.TokenResponse;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
Expand All @@ -43,6 +45,7 @@
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.ThingTypeMigrationService;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
Expand All @@ -66,7 +69,7 @@ public class TeslaAccountHandler extends BaseBridgeHandler {
public static final int API_MAXIMUM_ERRORS_IN_INTERVAL = 3;
public static final int API_ERROR_INTERVAL_SECONDS = 15;
private static final int CONNECT_RETRY_INTERVAL = 15000;
private static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault());

private final Logger logger = LoggerFactory.getLogger(TeslaAccountHandler.class);
Expand All @@ -80,6 +83,7 @@ public class TeslaAccountHandler extends BaseBridgeHandler {
final WebTarget wakeUpTarget;

private final TeslaSSOHandler ssoHandler;
private final ThingTypeMigrationService thingTypeMigrationService;

// Threading and Job related variables
protected ScheduledFuture<?> connectJob;
Expand All @@ -96,10 +100,12 @@ public class TeslaAccountHandler extends BaseBridgeHandler {
private TokenResponse logonToken;
private final Set<VehicleListener> vehicleListeners = new HashSet<>();

public TeslaAccountHandler(Bridge bridge, Client teslaClient, HttpClientFactory httpClientFactory) {
public TeslaAccountHandler(Bridge bridge, Client teslaClient, HttpClientFactory httpClientFactory,
ThingTypeMigrationService thingTypeMigrationService) {
super(bridge);
this.teslaTarget = teslaClient.target(URI_OWNERS);
this.ssoHandler = new TeslaSSOHandler(httpClientFactory.getCommonHttpClient());
this.thingTypeMigrationService = thingTypeMigrationService;

this.vehiclesTarget = teslaTarget.path(API_VERSION).path(VEHICLES);
this.vehicleTarget = vehiclesTarget.path(PATH_VEHICLE_ID);
Expand Down Expand Up @@ -143,7 +149,7 @@ public void dispose() {
}

public void scanForVehicles() {
scheduler.execute(() -> queryVehicles());
scheduler.execute(this::queryVehicles);
}

public void addVehicleListener(VehicleListener listener) {
Expand Down Expand Up @@ -179,7 +185,6 @@ protected boolean checkResponse(Response response, boolean immediatelyFail) {
ThingStatusInfo authenticationResult = authenticate();
updateStatus(authenticationResult.getStatus(), authenticationResult.getStatusDetail(),
authenticationResult.getDescription());
return false;
} else {
apiIntervalErrors++;
if (immediatelyFail || apiIntervalErrors >= API_MAXIMUM_ERRORS_IN_INTERVAL) {
Expand Down Expand Up @@ -221,18 +226,26 @@ protected Vehicle[] queryVehicles() {
Vehicle[] vehicleArray = gson.fromJson(jsonObject.getAsJsonArray("response"), Vehicle[].class);

for (Vehicle vehicle : vehicleArray) {
String responseString = invokeAndParse(vehicle.id, VEHICLE_CONFIG, null, dataRequestTarget, 0);
if (responseString == null || responseString.isBlank()) {
continue;
String responseString = invokeAndParse(vehicle.id, null, null, dataRequestTarget, 0);
VehicleConfig vehicleConfig = null;
if (responseString != null && !responseString.isBlank()) {
vehicleConfig = gson.fromJson(responseString, VehicleData.class).vehicle_config;
}
VehicleConfig vehicleConfig = gson.fromJson(responseString, VehicleConfig.class);
for (VehicleListener listener : vehicleListeners) {
listener.vehicleFound(vehicle, vehicleConfig);
}
for (Thing vehicleThing : getThing().getThings()) {
if (vehicle.vin.equals(vehicleThing.getConfiguration().get(VIN))) {
TeslaVehicleHandler vehicleHandler = (TeslaVehicleHandler) vehicleThing.getHandler();
if (vehicleHandler != null) {
if (TeslaBindingConstants.THING_TYPE_VEHICLE.equals(vehicleThing.getThingTypeUID())
&& vehicleConfig != null) {
// Seems the type of this vehicle has not been identified before, so let's switch the
// thing type of it
thingTypeMigrationService.migrateThingType(vehicleThing, vehicleConfig.identifyModel(),
vehicleThing.getConfiguration());
break;
}
logger.debug("Querying the vehicle: VIN {}", vehicle.vin);
String vehicleJSON = gson.toJson(vehicle);
vehicleHandler.parseAndUpdate("queryVehicle", null, vehicleJSON);
Expand All @@ -256,13 +269,13 @@ ThingStatusInfo authenticate() {
TokenResponse token = logonToken;

boolean hasExpired = true;
logger.debug("Current authentication time {}", dateFormatter.format(Instant.now()));
logger.debug("Current authentication time {}", DATE_FORMATTER.format(Instant.now()));

if (token != null) {
Instant tokenCreationInstant = Instant.ofEpochMilli(token.created_at * 1000);
Instant tokenExpiresInstant = Instant.ofEpochMilli((token.created_at + token.expires_in) * 1000);
logger.debug("Found a request token from {}", dateFormatter.format(tokenCreationInstant));
logger.debug("Access token expiration time {}", dateFormatter.format(tokenExpiresInstant));
logger.debug("Found a request token from {}", DATE_FORMATTER.format(tokenCreationInstant));
logger.debug("Access token expiration time {}", DATE_FORMATTER.format(tokenExpiresInstant));

if (tokenExpiresInstant.isBefore(Instant.now())) {
logger.debug("The access token has expired");
Expand Down Expand Up @@ -308,15 +321,13 @@ protected String invokeAndParse(String vehicleId, String command, String payLoad
.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", vehicleId)
.request(MediaType.APPLICATION_JSON_TYPE)
.header("Authorization", "Bearer " + logonToken.access_token).get();
} else {
if (command != null) {
response = target.resolveTemplate("cmd", command).resolveTemplate("vid", vehicleId)
.request(MediaType.APPLICATION_JSON_TYPE)
.header("Authorization", "Bearer " + logonToken.access_token).get();
} else {
response = target.resolveTemplate("vid", vehicleId).request(MediaType.APPLICATION_JSON_TYPE)
.header("Authorization", "Bearer " + logonToken.access_token).get();
}
response = target.resolveTemplate("vid", vehicleId).request(MediaType.APPLICATION_JSON_TYPE)
.header("Authorization", "Bearer " + logonToken.access_token).get();
}

if (!checkResponse(response, false)) {
Expand All @@ -330,7 +341,6 @@ protected String invokeAndParse(String vehicleId, String command, String payLoad
logger.debug("Retrying to send the command {}.", command);
return invokeAndParse(vehicleId, command, payLoad, target, noOfretries - 1);
} catch (InterruptedException e) {
return null;
}
}
return null;
Expand All @@ -353,8 +363,9 @@ protected String invokeAndParse(String vehicleId, String command, String payLoad
lock.lock();

ThingStatusInfo status = getThing().getStatusInfo();
if (status.getStatus() != ThingStatus.ONLINE
&& status.getStatusDetail() != ThingStatusDetail.CONFIGURATION_ERROR) {
if ((status.getStatus() != ThingStatus.ONLINE
&& status.getStatusDetail() != ThingStatusDetail.CONFIGURATION_ERROR)
|| hasUnidentifiedVehicles()) {
logger.debug("Setting up an authenticated connection to the Tesla back-end");

ThingStatusInfo authenticationResult = authenticate();
Expand Down Expand Up @@ -394,19 +405,16 @@ protected String invokeAndParse(String vehicleId, String command, String payLoad
}
}
}
} else {
if (response != null) {
logger.error("Error fetching the list of vehicles : {}:{}", response.getStatus(),
response.getStatusInfo());
updateStatus(ThingStatus.OFFLINE);
}
} else if (response != null) {
logger.error("Error fetching the list of vehicles : {}:{}", response.getStatus(),
response.getStatusInfo());
updateStatus(ThingStatus.OFFLINE);
}
} else if (authenticationResult.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR) {
// make sure to set thing to CONFIGURATION_ERROR in case of failed authentication in order not to
// hit request limit on retries on the Tesla SSO endpoints.
updateStatus(ThingStatus.OFFLINE, authenticationResult.getStatusDetail());
}

}
} catch (Exception e) {
logger.error("An exception occurred while connecting to the Tesla back-end: '{}'", e.getMessage(), e);
Expand All @@ -415,6 +423,11 @@ protected String invokeAndParse(String vehicleId, String command, String payLoad
}
};

private boolean hasUnidentifiedVehicles() {
return getThing().getThings().stream()
.anyMatch(vehicle -> TeslaBindingConstants.THING_TYPE_VEHICLE.equals(vehicle.getThingTypeUID()));
}

protected class Request implements Runnable {

private static final int NO_OF_RETRIES = 3;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.websocket.api.Session;
Expand All @@ -32,6 +31,7 @@
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.openhab.binding.tesla.internal.protocol.Event;
import org.openhab.core.io.net.http.WebSocketFactory;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -54,26 +54,36 @@ public class TeslaEventEndpoint implements WebSocketListener, WebSocketPingPongL

private String endpointId;
protected WebSocketFactory webSocketFactory;
private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger();

private WebSocketClient client;
private ConnectionState connectionState = ConnectionState.CLOSED;
private @Nullable Session session;
private EventHandler eventHandler;
private final Gson gson = new Gson();

public TeslaEventEndpoint(WebSocketFactory webSocketFactory) {
public TeslaEventEndpoint(ThingUID uid, WebSocketFactory webSocketFactory) {
try {
this.endpointId = "TeslaEventEndpoint-" + INSTANCE_COUNTER.incrementAndGet();
this.endpointId = "TeslaEventEndpoint-" + uid.getAsString();

client = webSocketFactory.createWebSocketClient(endpointId);
String name = ThingWebClientUtil.buildWebClientConsumerName(uid, null);
client = webSocketFactory.createWebSocketClient(name);
this.client.setConnectTimeout(TIMEOUT_MILLISECONDS);
this.client.setMaxIdleTimeout(IDLE_TIMEOUT_MILLISECONDS);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public void close() {
try {
if (client.isRunning()) {
client.stop();
}
} catch (Exception e) {
logger.warn("An exception occurred while stopping the WebSocket client : {}", e.getMessage());
}
}

public void connect(URI endpointURI) {
if (connectionState == ConnectionState.CONNECTED) {
return;
Expand Down Expand Up @@ -113,7 +123,7 @@ public void onWebSocketConnect(Session session) {
this.session = session;
}

public void close() {
public void closeConnection() {
try {
connectionState = ConnectionState.CLOSING;
if (session != null && session.isOpen()) {
Expand Down
Loading