mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[Tesla] Add event stream & handling post new authentication process by Tesla (#13116)
Signed-Off-By: Karel Goderis <karel.goderis@me.com>
This commit is contained in:
parent
3e5dde0474
commit
062a7c1e76
@ -45,7 +45,12 @@ When using one of such apps, simply copy and paste the received refresh token in
|
||||
|
||||
The vehicle Thing requires the vehicle's VIN as a configuration parameter `vin`.
|
||||
|
||||
Additionally, the optional boolean parameter `allowWakeup` can be set. This determines whether openHAB is allowed to wake up the vehicle in order to retrieve data from it. This setting is not recommended as it will result in a significant vampire drain (i.e. energy consumption although the vehicle is parking).
|
||||
Additionally, the optional boolean parameter `allowWakeup` can be set.
|
||||
This determines whether openHAB is allowed to wake up the vehicle in order to retrieve data from it.
|
||||
This setting is not recommended as it will result in a significant vampire drain (i.e. energy consumption although the vehicle is parking).
|
||||
|
||||
In addition, the optional boolean parameter `enableEvents` can be set.
|
||||
By doing so, events streamed by the Tesla back-end system will be captured and processed, providing near real-time updates of some key variables generated by the vehicle.
|
||||
|
||||
## Channels
|
||||
|
||||
|
@ -32,7 +32,7 @@ public class TeslaBindingConstants {
|
||||
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";
|
||||
public static final String URI_EVENT = "https://streaming.vn.teslamotors.com/stream/";
|
||||
public static final String URI_EVENT = "wss://streaming.vn.teslamotors.com/streaming/";
|
||||
public static final String URI_OWNERS = "https://owner-api.teslamotors.com";
|
||||
public static final String VALETPIN = "valetpin";
|
||||
public static final String VEHICLES = "vehicles";
|
||||
|
@ -24,6 +24,7 @@ import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.tesla.internal.handler.TeslaAccountHandler;
|
||||
import org.openhab.binding.tesla.internal.handler.TeslaVehicleHandler;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
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.ThingTypeUID;
|
||||
@ -54,13 +55,16 @@ public class TeslaHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private final ClientBuilder clientBuilder;
|
||||
private final HttpClientFactory httpClientFactory;
|
||||
private final WebSocketFactory webSocketFactory;
|
||||
|
||||
@Activate
|
||||
public TeslaHandlerFactory(@Reference ClientBuilder clientBuilder, @Reference HttpClientFactory httpClientFactory) {
|
||||
public TeslaHandlerFactory(@Reference ClientBuilder clientBuilder, @Reference HttpClientFactory httpClientFactory,
|
||||
final @Reference WebSocketFactory webSocketFactory) {
|
||||
this.clientBuilder = clientBuilder //
|
||||
.connectTimeout(EVENT_STREAM_CONNECT_TIMEOUT, TimeUnit.SECONDS)
|
||||
.readTimeout(EVENT_STREAM_READ_TIMEOUT, TimeUnit.SECONDS);
|
||||
this.httpClientFactory = httpClientFactory;
|
||||
this.webSocketFactory = webSocketFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -75,7 +79,7 @@ public class TeslaHandlerFactory extends BaseThingHandlerFactory {
|
||||
if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) {
|
||||
return new TeslaAccountHandler((Bridge) thing, clientBuilder.build(), httpClientFactory);
|
||||
} else {
|
||||
return new TeslaVehicleHandler(thing, clientBuilder);
|
||||
return new TeslaVehicleHandler(thing, webSocketFactory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -167,6 +167,10 @@ public class TeslaAccountHandler extends BaseBridgeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return logonToken.access_token;
|
||||
}
|
||||
|
||||
protected boolean checkResponse(Response response, boolean immediatelyFail) {
|
||||
if (response != null && response.getStatus() == 200) {
|
||||
return true;
|
||||
|
@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 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.handler;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
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;
|
||||
import org.eclipse.jetty.websocket.api.StatusCode;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketListener;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketPingPongListener;
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* The {@link TeslaEventEndpoint} is responsible managing a websocket connection to a specific URI, most notably the
|
||||
* Tesla event stream infrastructure. Consumers can register an {@link EventHandler} in order to receive data that was
|
||||
* received by the websocket endpoint. The {@link TeslaEventEndpoint} can also implements a ping/pong mechanism to keep
|
||||
* websockets alive.
|
||||
*
|
||||
* @author Karel Goderis - Initial contribution
|
||||
*/
|
||||
public class TeslaEventEndpoint implements WebSocketListener, WebSocketPingPongListener {
|
||||
|
||||
private static final int TIMEOUT_MILLISECONDS = 3000;
|
||||
private static final int IDLE_TIMEOUT_MILLISECONDS = 30000;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(TeslaEventEndpoint.class);
|
||||
|
||||
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) {
|
||||
try {
|
||||
this.endpointId = "TeslaEventEndpoint-" + INSTANCE_COUNTER.incrementAndGet();
|
||||
|
||||
client = webSocketFactory.createWebSocketClient(endpointId);
|
||||
this.client.setConnectTimeout(TIMEOUT_MILLISECONDS);
|
||||
this.client.setMaxIdleTimeout(IDLE_TIMEOUT_MILLISECONDS);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void connect(URI endpointURI) {
|
||||
if (connectionState == ConnectionState.CONNECTED) {
|
||||
return;
|
||||
} else if (connectionState == ConnectionState.CONNECTING) {
|
||||
logger.debug("{} : Already connecting to {}", endpointId, endpointURI);
|
||||
return;
|
||||
} else if (connectionState == ConnectionState.CLOSING) {
|
||||
logger.warn("{} : Connecting to {} while already closing the connection", endpointId, endpointURI);
|
||||
return;
|
||||
}
|
||||
Future<Session> futureConnect = null;
|
||||
try {
|
||||
if (!client.isRunning()) {
|
||||
logger.debug("{} : Starting the client to connect to {}", endpointId, endpointURI);
|
||||
client.start();
|
||||
} else {
|
||||
logger.debug("{} : The client to connect to {} is already running", endpointId, endpointURI);
|
||||
}
|
||||
|
||||
logger.debug("{} : Connecting to {}", endpointId, endpointURI);
|
||||
connectionState = ConnectionState.CONNECTING;
|
||||
futureConnect = client.connect(this, endpointURI);
|
||||
futureConnect.get(TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
|
||||
} catch (Exception e) {
|
||||
logger.error("An exception occurred while connecting the Event Endpoint : '{}'", e.getMessage());
|
||||
if (futureConnect != null) {
|
||||
futureConnect.cancel(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
logger.debug("{} : Connected to {} with hash {}", endpointId, session.getRemoteAddress().getAddress(),
|
||||
session.hashCode());
|
||||
connectionState = ConnectionState.CONNECTED;
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
connectionState = ConnectionState.CLOSING;
|
||||
if (session != null && session.isOpen()) {
|
||||
logger.debug("{} : Closing the session", endpointId);
|
||||
session.close(StatusCode.NORMAL, "bye");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("{} : An exception occurred while closing the session : {}", endpointId, e.getMessage());
|
||||
connectionState = ConnectionState.CLOSED;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketClose(int statusCode, String reason) {
|
||||
logger.debug("{} : Closed the session with status {} for reason {}", endpointId, statusCode, reason);
|
||||
connectionState = ConnectionState.CLOSED;
|
||||
this.session = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketText(String message) {
|
||||
// NoOp
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketBinary(byte[] payload, int offset, int length) {
|
||||
BufferedReader in = new BufferedReader(
|
||||
new InputStreamReader(new ByteArrayInputStream(payload), StandardCharsets.UTF_8.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT)));
|
||||
String str;
|
||||
try {
|
||||
while ((str = in.readLine()) != null) {
|
||||
logger.trace("{} : Received raw data '{}'", endpointId, str);
|
||||
if (this.eventHandler != null) {
|
||||
try {
|
||||
Event event = gson.fromJson(str, Event.class);
|
||||
this.eventHandler.handleEvent(event);
|
||||
} catch (RuntimeException e) {
|
||||
logger.error("{} : An exception occurred while processing raw data : {}", endpointId,
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("{} : An exception occurred while receiving raw data : {}", endpointId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketError(Throwable cause) {
|
||||
logger.error("{} : An error occurred in the session : {}", endpointId, cause.getMessage());
|
||||
if (session != null && session.isOpen()) {
|
||||
session.close(StatusCode.ABNORMAL, "Session Error");
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMessage(String message) throws IOException {
|
||||
try {
|
||||
if (session != null) {
|
||||
logger.debug("{} : Sending raw data '{}'", endpointId, message);
|
||||
session.getRemote().sendString(message);
|
||||
} else {
|
||||
throw new IOException("Session is not initialized");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (session != null && session.isOpen()) {
|
||||
session.close(StatusCode.ABNORMAL, "Send Message Error");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public void ping() {
|
||||
try {
|
||||
if (session != null) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(8).putLong(System.nanoTime()).flip();
|
||||
session.getRemote().sendPing(buffer);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("{} : An exception occurred while pinging the remote end : {}", endpointId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketPing(ByteBuffer payload) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(8).putLong(System.nanoTime()).flip();
|
||||
try {
|
||||
if (session != null) {
|
||||
session.getRemote().sendPing(buffer);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("{} : An exception occurred while processing a ping message : {}", endpointId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWebSocketPong(ByteBuffer payload) {
|
||||
long start = payload.getLong();
|
||||
long roundTrip = System.nanoTime() - start;
|
||||
|
||||
logger.trace("{} : Received a Pong with a roundtrip of {} milliseconds", endpointId,
|
||||
TimeUnit.MILLISECONDS.convert(roundTrip, TimeUnit.NANOSECONDS));
|
||||
}
|
||||
|
||||
public void addEventHandler(EventHandler eventHandler) {
|
||||
this.eventHandler = eventHandler;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return connectionState == ConnectionState.CONNECTED;
|
||||
}
|
||||
|
||||
public static interface EventHandler {
|
||||
public void handleEvent(Event event);
|
||||
}
|
||||
|
||||
private enum ConnectionState {
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
CLOSING,
|
||||
CLOSED
|
||||
}
|
||||
}
|
@ -14,9 +14,13 @@ package org.openhab.binding.tesla.internal.handler;
|
||||
|
||||
import static org.openhab.binding.tesla.internal.TeslaBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@ -24,28 +28,30 @@ 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;
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.ClientBuilder;
|
||||
import javax.ws.rs.client.WebTarget;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
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.Request;
|
||||
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.Event;
|
||||
import org.openhab.binding.tesla.internal.protocol.GUIState;
|
||||
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.openhab.core.io.net.http.WebSocketFactory;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
@ -61,6 +67,7 @@ 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;
|
||||
@ -83,11 +90,15 @@ public class TeslaVehicleHandler extends BaseThingHandler {
|
||||
private static final int SLOW_STATUS_REFRESH_INTERVAL = 60000;
|
||||
private static final int API_SLEEP_INTERVAL_MINUTES = 20;
|
||||
private static final int MOVE_THRESHOLD_INTERVAL_MINUTES = 5;
|
||||
private static final int EVENT_MAXIMUM_ERRORS_IN_INTERVAL = 10;
|
||||
private static final int EVENT_ERROR_INTERVAL_SECONDS = 15;
|
||||
private static final int EVENT_STREAM_PAUSE = 3000;
|
||||
private static final int EVENT_TIMESTAMP_AGE_LIMIT = 3000;
|
||||
private static final int EVENT_TIMESTAMP_MAX_DELTA = 10000;
|
||||
private static final int EVENT_PING_INTERVAL = 10000;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(TeslaVehicleHandler.class);
|
||||
|
||||
protected WebTarget eventTarget;
|
||||
|
||||
// Vehicle state variables
|
||||
protected Vehicle vehicle;
|
||||
protected String vehicleJSON;
|
||||
@ -99,6 +110,7 @@ public class TeslaVehicleHandler extends BaseThingHandler {
|
||||
|
||||
protected boolean allowWakeUp;
|
||||
protected boolean allowWakeUpForCommands;
|
||||
protected boolean enableEvents = false;
|
||||
protected long lastTimeStamp;
|
||||
protected long apiIntervalTimestamp;
|
||||
protected int apiIntervalErrors;
|
||||
@ -117,18 +129,17 @@ public class TeslaVehicleHandler extends BaseThingHandler {
|
||||
protected TeslaAccountHandler account;
|
||||
|
||||
protected QueueChannelThrottler stateThrottler;
|
||||
protected ClientBuilder clientBuilder;
|
||||
protected Client eventClient;
|
||||
protected TeslaChannelSelectorProxy teslaChannelSelectorProxy = new TeslaChannelSelectorProxy();
|
||||
protected Thread eventThread;
|
||||
protected ScheduledFuture<?> fastStateJob;
|
||||
protected ScheduledFuture<?> slowStateJob;
|
||||
protected WebSocketFactory webSocketFactory;
|
||||
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
public TeslaVehicleHandler(Thing thing, ClientBuilder clientBuilder) {
|
||||
public TeslaVehicleHandler(Thing thing, WebSocketFactory webSocketFactory) {
|
||||
super(thing);
|
||||
this.clientBuilder = clientBuilder;
|
||||
this.webSocketFactory = webSocketFactory;
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@ -138,9 +149,7 @@ public class TeslaVehicleHandler extends BaseThingHandler {
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
allowWakeUp = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUP);
|
||||
allowWakeUpForCommands = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUPFORCOMMANDS);
|
||||
|
||||
// the streaming API seems to be broken - let's keep the code, if it comes back one day
|
||||
// enableEvents = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ENABLEEVENTS);
|
||||
enableEvents = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ENABLEEVENTS);
|
||||
|
||||
account = (TeslaAccountHandler) getBridge().getHandler();
|
||||
lock = new ReentrantLock();
|
||||
@ -166,6 +175,14 @@ public class TeslaVehicleHandler extends BaseThingHandler {
|
||||
slowStateJob = scheduler.scheduleWithFixedDelay(slowStateRunnable, 0, SLOW_STATUS_REFRESH_INTERVAL,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
if (enableEvents) {
|
||||
if (eventThread == null) {
|
||||
eventThread = new Thread(eventRunnable, "openHAB-Tesla-Events-" + getThing().getUID());
|
||||
eventThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
@ -193,10 +210,6 @@ public class TeslaVehicleHandler extends BaseThingHandler {
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
if (eventClient != null) {
|
||||
eventClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -565,9 +578,6 @@ public class TeslaVehicleHandler extends BaseThingHandler {
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
if (eventClient != null) {
|
||||
eventClient.close();
|
||||
}
|
||||
} else if ((System.currentTimeMillis() - apiIntervalTimestamp) > 1000
|
||||
* TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS) {
|
||||
logger.trace("Resetting the error counter. ({} errors in the last interval)", apiIntervalErrors);
|
||||
@ -997,4 +1007,188 @@ public class TeslaVehicleHandler extends BaseThingHandler {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected Runnable eventRunnable = new Runnable() {
|
||||
TeslaEventEndpoint eventEndpoint;
|
||||
boolean isAuthenticated = false;
|
||||
long lastPingTimestamp = 0;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
eventEndpoint = new TeslaEventEndpoint(webSocketFactory);
|
||||
eventEndpoint.addEventHandler(new TeslaEventEndpoint.EventHandler() {
|
||||
@Override
|
||||
public void handleEvent(Event event) {
|
||||
if (event != null) {
|
||||
switch (event.msg_type) {
|
||||
case "control:hello":
|
||||
logger.debug("Event : Received hello");
|
||||
break;
|
||||
case "data:update":
|
||||
logger.debug("Event : Received an update: '{}'", event.value);
|
||||
|
||||
String vals[] = event.value.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 : 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);
|
||||
}
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(
|
||||
"The variable/value pair '{}':'{}' is successfully processed",
|
||||
EventKeys.values()[i], vals[i]);
|
||||
}
|
||||
} else {
|
||||
Map<String, String> properties = editProperties();
|
||||
properties.put(selector.getChannelID(),
|
||||
(selector.getState(vals[i])).toString());
|
||||
updateProperties(properties);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(
|
||||
"The variable/value pair '{}':'{}' is successfully used to set property '{}'",
|
||||
EventKeys.values()[i], vals[i], selector.getChannelID());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (logger.isDebugEnabled()) {
|
||||
SimpleDateFormat dateFormatter = new SimpleDateFormat(
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSS");
|
||||
logger.debug(
|
||||
"Event : 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 : 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 : The event endpoint will be reset");
|
||||
eventEndpoint.close();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "data:error":
|
||||
logger.debug("Event : Received an error: '{}'/'{}'", event.value, event.error_type);
|
||||
eventEndpoint.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
if (getThing().getStatus() == ThingStatus.ONLINE) {
|
||||
if (isAwake()) {
|
||||
eventEndpoint.connect(new URI(URI_EVENT));
|
||||
|
||||
if (eventEndpoint.isConnected()) {
|
||||
if (!isAuthenticated) {
|
||||
logger.debug("Event : Authenticating vehicle {}", vehicle.vehicle_id);
|
||||
JsonObject payloadObject = new JsonObject();
|
||||
payloadObject.addProperty("msg_type", "data:subscribe_oauth");
|
||||
payloadObject.addProperty("token", account.getAccessToken());
|
||||
payloadObject.addProperty("value", Arrays.asList(EventKeys.values()).stream()
|
||||
.skip(1).map(Enum::toString).collect(Collectors.joining(",")));
|
||||
payloadObject.addProperty("tag", vehicle.vehicle_id);
|
||||
|
||||
eventEndpoint.sendMessage(gson.toJson(payloadObject));
|
||||
isAuthenticated = true;
|
||||
|
||||
lastPingTimestamp = System.nanoTime();
|
||||
}
|
||||
|
||||
if (TimeUnit.MILLISECONDS.convert(System.nanoTime() - lastPingTimestamp,
|
||||
TimeUnit.NANOSECONDS) > EVENT_PING_INTERVAL) {
|
||||
logger.trace("Event : Pinging the Tesla event stream infrastructure");
|
||||
eventEndpoint.ping();
|
||||
lastPingTimestamp = System.nanoTime();
|
||||
}
|
||||
}
|
||||
|
||||
if (!eventEndpoint.isConnected()) {
|
||||
isAuthenticated = false;
|
||||
eventIntervalErrors++;
|
||||
if (eventIntervalErrors >= EVENT_MAXIMUM_ERRORS_IN_INTERVAL) {
|
||||
logger.warn(
|
||||
"Event : 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);
|
||||
eventEndpoint.close();
|
||||
}
|
||||
|
||||
if ((System.currentTimeMillis() - eventIntervalTimestamp) > 1000
|
||||
* EVENT_ERROR_INTERVAL_SECONDS) {
|
||||
logger.trace(
|
||||
"Event : Resetting the error counter. ({} errors in the last interval)",
|
||||
eventIntervalErrors);
|
||||
eventIntervalTimestamp = System.currentTimeMillis();
|
||||
eventIntervalErrors = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.debug("Event : The vehicle is not awake");
|
||||
if (vehicle != null) {
|
||||
if (allowWakeUp) {
|
||||
// wake up the vehicle until streaming token <> 0
|
||||
logger.debug("Event : Waking up the vehicle");
|
||||
wakeUp();
|
||||
}
|
||||
} else {
|
||||
vehicle = queryVehicle();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (URISyntaxException | NumberFormatException | IOException e) {
|
||||
logger.debug("Event : An exception occurred while processing events: '{}'", e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(EVENT_STREAM_PAUSE);
|
||||
} catch (InterruptedException e) {
|
||||
logger.debug("Event : An exception occurred while putting the event thread to sleep: '{}'",
|
||||
e.getMessage());
|
||||
}
|
||||
|
||||
if (Thread.interrupted()) {
|
||||
logger.debug("Event : The event thread was interrupted");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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.protocol;
|
||||
|
||||
/**
|
||||
* The {@link Event} is a datastructure to capture
|
||||
* events sent by the Tesla vehicle.
|
||||
*
|
||||
* @author Karel Goderis - Initial contribution
|
||||
*/
|
||||
public class Event {
|
||||
public String msg_type;
|
||||
public String value;
|
||||
public String tag;
|
||||
public String error_type;
|
||||
public int connectionTimeout;
|
||||
}
|
@ -140,6 +140,11 @@
|
||||
<description>Allows waking up the vehicle, when commands are sent to it. Execution of commands will be delayed in
|
||||
this case and you could cause the vehicle to stay awake very long.</description>
|
||||
</parameter>
|
||||
<parameter name="enableEvents" type="boolean" required="false">
|
||||
<default>false</default>
|
||||
<label>Enable Events</label>
|
||||
<description>Enable the event stream for the vehicle</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
@ -147,6 +147,11 @@
|
||||
<description>Allows waking up the vehicle, when commands are sent to it. Execution of commands will be delayed in
|
||||
this case and you could cause the vehicle to stay awake very long.</description>
|
||||
</parameter>
|
||||
<parameter name="enableEvents" type="boolean" required="false">
|
||||
<default>false</default>
|
||||
<label>Enable Events</label>
|
||||
<description>Enable the event stream for the vehicle</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
@ -147,6 +147,11 @@
|
||||
<description>Allows waking up the vehicle, when commands are sent to it. Execution of commands will be delayed in
|
||||
this case and you could cause the vehicle to stay awake very long.</description>
|
||||
</parameter>
|
||||
<parameter name="enableEvents" type="boolean" required="false">
|
||||
<default>false</default>
|
||||
<label>Enable Events</label>
|
||||
<description>Enable the event stream for the vehicle</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
@ -142,6 +142,11 @@
|
||||
<description>Allows waking up the vehicle, when commands are sent to it. Execution of commands will be delayed in
|
||||
this case and you could cause the vehicle to stay awake very long.</description>
|
||||
</parameter>
|
||||
<parameter name="enableEvents" type="boolean" required="false">
|
||||
<default>false</default>
|
||||
<label>Enable Events</label>
|
||||
<description>Enable the event stream for the vehicle</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
Loading…
Reference in New Issue
Block a user