]> git.basschouten.com Git - openhab-addons.git/commitdiff
[Tesla] Add event stream & handling post new authentication process by Tesla (#13116)
authorKarel Goderis <karel.goderis@me.com>
Wed, 21 Sep 2022 19:55:27 +0000 (21:55 +0200)
committerGitHub <noreply@github.com>
Wed, 21 Sep 2022 19:55:27 +0000 (21:55 +0200)
Signed-Off-By: Karel Goderis <karel.goderis@me.com>
bundles/org.openhab.binding.tesla/README.md
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaBindingConstants.java
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaHandlerFactory.java
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaEventEndpoint.java [new file with mode: 0644]
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaVehicleHandler.java
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/Event.java [new file with mode: 0644]
bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/model3.xml
bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/models.xml
bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modelx.xml
bundles/org.openhab.binding.tesla/src/main/resources/OH-INF/thing/modely.xml

index e5c9aa53feab2ceaf1ebfe3dfaecef513068f04e..6c98808961a9ccd1afb595125d4f58d1d5c1ea32 100644 (file)
@@ -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
 
index 43c72f58511f1a70d9c1888f73dd2a151c0379f8..ac95a27c81852908120fbb0aac020d5f1d8c1224 100644 (file)
@@ -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";
index 7c686e39a5a40fb496763d8b35e737e4f3c09d40..d89fea388cd259ef958018c4d5d607682faf6c6e 100644 (file)
@@ -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);
         }
     }
 }
index a72507c8d73b73654e73f04fc9fd2b82f2a4bb37..e809c3c64d2dc8e11921674c05b3efa79c3eac6f 100644 (file)
@@ -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 (file)
index 0000000..1eb6cfe
--- /dev/null
@@ -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
+    }
+}
index e81a006a518318fa3b117a39d414db592478dec6..04fe260758438146d3fb5cf56340cc0189ded408 100644 (file)
@@ -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;
+                }
+            }
+        }
+    };
 }
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 (file)
index 0000000..9500529
--- /dev/null
@@ -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;
+}
index 55d7109df20b8af09ff64e96a835a3648cb1a61e..c1662d7c955afbf9f759a42596cacc18c60f8f79 100644 (file)
                                <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>
index 5fda08586b2f1ff539fc59a726ad6bebda5fe4f0..a7ba2e2c9dfe00195b0f4aa0012bd685ba7d60dc 100644 (file)
                                <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>
index b1753a842fc9469f9823041ecd43d09ec372cfb7..96b7d1e8017577a9ad0823a4334c08393094e926 100644 (file)
                                <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>
index 95e60569f5ef9ab9d162690b75c3e40c220ebb10..76b3fdea0ed089d51fb7b1e439be32ecb52bf0f3 100644 (file)
                                <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>