From 062a7c1e765b6f5dcce454fe23da50b084f829d8 Mon Sep 17 00:00:00 2001 From: Karel Goderis Date: Wed, 21 Sep 2022 21:55:27 +0200 Subject: [PATCH] [Tesla] Add event stream & handling post new authentication process by Tesla (#13116) Signed-Off-By: Karel Goderis --- bundles/org.openhab.binding.tesla/README.md | 7 +- .../tesla/internal/TeslaBindingConstants.java | 2 +- .../tesla/internal/TeslaHandlerFactory.java | 8 +- .../internal/handler/TeslaAccountHandler.java | 4 + .../internal/handler/TeslaEventEndpoint.java | 239 ++++++++++++++++++ .../internal/handler/TeslaVehicleHandler.java | 230 +++++++++++++++-- .../tesla/internal/protocol/Event.java | 27 ++ .../main/resources/OH-INF/thing/model3.xml | 5 + .../main/resources/OH-INF/thing/models.xml | 5 + .../main/resources/OH-INF/thing/modelx.xml | 5 + .../main/resources/OH-INF/thing/modely.xml | 5 + 11 files changed, 515 insertions(+), 22 deletions(-) create mode 100644 bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaEventEndpoint.java create mode 100644 bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/Event.java diff --git a/bundles/org.openhab.binding.tesla/README.md b/bundles/org.openhab.binding.tesla/README.md index e5c9aa53fea..6c98808961a 100644 --- a/bundles/org.openhab.binding.tesla/README.md +++ b/bundles/org.openhab.binding.tesla/README.md @@ -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 diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaBindingConstants.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaBindingConstants.java index 43c72f58511..ac95a27c818 100644 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaBindingConstants.java +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaBindingConstants.java @@ -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"; diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaHandlerFactory.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaHandlerFactory.java index 7c686e39a5a..d89fea388cd 100644 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaHandlerFactory.java +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaHandlerFactory.java @@ -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); } } } diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java index a72507c8d73..e809c3c64d2 100644 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java @@ -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; diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaEventEndpoint.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaEventEndpoint.java new file mode 100644 index 00000000000..1eb6cfea2ac --- /dev/null +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaEventEndpoint.java @@ -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 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 + } +} diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaVehicleHandler.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaVehicleHandler.java index e81a006a518..04fe2607584 100644 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaVehicleHandler.java +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaVehicleHandler.java @@ -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 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; + } + } + } + }; } diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/Event.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/Event.java new file mode 100644 index 00000000000..95005296470 --- /dev/null +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/Event.java @@ -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; +} diff --git a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/model3.xml b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/model3.xml index 55d7109df20..c1662d7c955 100644 --- a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/model3.xml +++ b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/model3.xml @@ -140,6 +140,11 @@ 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. + + false + + Enable the event stream for the vehicle + diff --git a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/models.xml b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/models.xml index 5fda08586b2..a7ba2e2c9df 100644 --- a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/models.xml +++ b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/models.xml @@ -147,6 +147,11 @@ 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. + + false + + Enable the event stream for the vehicle + diff --git a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modelx.xml b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modelx.xml index b1753a842fc..96b7d1e8017 100644 --- a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modelx.xml +++ b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modelx.xml @@ -147,6 +147,11 @@ 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. + + false + + Enable the event stream for the vehicle + diff --git a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modely.xml b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modely.xml index 95e60569f5e..76b3fdea0ed 100644 --- a/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modely.xml +++ b/bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modely.xml @@ -142,6 +142,11 @@ 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. + + false + + Enable the event stream for the vehicle +