2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.tesla.internal.handler;
15 import java.io.BufferedReader;
16 import java.io.ByteArrayInputStream;
17 import java.io.IOException;
18 import java.io.InputStreamReader;
20 import java.nio.ByteBuffer;
21 import java.nio.charset.CodingErrorAction;
22 import java.nio.charset.StandardCharsets;
23 import java.util.concurrent.Future;
24 import java.util.concurrent.TimeUnit;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.jetty.websocket.api.Session;
28 import org.eclipse.jetty.websocket.api.StatusCode;
29 import org.eclipse.jetty.websocket.api.WebSocketListener;
30 import org.eclipse.jetty.websocket.api.WebSocketPingPongListener;
31 import org.eclipse.jetty.websocket.client.WebSocketClient;
32 import org.openhab.binding.tesla.internal.protocol.Event;
33 import org.openhab.core.io.net.http.WebSocketFactory;
34 import org.openhab.core.thing.ThingUID;
35 import org.openhab.core.thing.util.ThingWebClientUtil;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
39 import com.google.gson.Gson;
42 * The {@link TeslaEventEndpoint} is responsible managing a websocket connection to a specific URI, most notably the
43 * Tesla event stream infrastructure. Consumers can register an {@link EventHandler} in order to receive data that was
44 * received by the websocket endpoint. The {@link TeslaEventEndpoint} can also implements a ping/pong mechanism to keep
47 * @author Karel Goderis - Initial contribution
49 public class TeslaEventEndpoint implements WebSocketListener, WebSocketPingPongListener {
51 private static final int TIMEOUT_MILLISECONDS = 3000;
52 private static final int IDLE_TIMEOUT_MILLISECONDS = 30000;
54 private final Logger logger = LoggerFactory.getLogger(TeslaEventEndpoint.class);
56 private String endpointId;
57 protected WebSocketFactory webSocketFactory;
59 private WebSocketClient client;
60 private ConnectionState connectionState = ConnectionState.CLOSED;
61 private @Nullable Session session;
62 private EventHandler eventHandler;
63 private final Gson gson = new Gson();
65 public TeslaEventEndpoint(ThingUID uid, WebSocketFactory webSocketFactory) {
67 this.endpointId = "TeslaEventEndpoint-" + uid.getAsString();
69 String name = ThingWebClientUtil.buildWebClientConsumerName(uid, null);
70 client = webSocketFactory.createWebSocketClient(name);
71 this.client.setConnectTimeout(TIMEOUT_MILLISECONDS);
72 this.client.setMaxIdleTimeout(IDLE_TIMEOUT_MILLISECONDS);
73 } catch (Exception e) {
74 throw new RuntimeException(e);
80 if (client.isRunning()) {
83 } catch (Exception e) {
84 logger.warn("An exception occurred while stopping the WebSocket client : {}", e.getMessage());
88 public void connect(URI endpointURI) {
89 if (connectionState == ConnectionState.CONNECTED) {
91 } else if (connectionState == ConnectionState.CONNECTING) {
92 logger.debug("{} : Already connecting to {}", endpointId, endpointURI);
94 } else if (connectionState == ConnectionState.CLOSING) {
95 logger.warn("{} : Connecting to {} while already closing the connection", endpointId, endpointURI);
98 Future<Session> futureConnect = null;
100 if (!client.isRunning()) {
101 logger.debug("{} : Starting the client to connect to {}", endpointId, endpointURI);
104 logger.debug("{} : The client to connect to {} is already running", endpointId, endpointURI);
107 logger.debug("{} : Connecting to {}", endpointId, endpointURI);
108 connectionState = ConnectionState.CONNECTING;
109 futureConnect = client.connect(this, endpointURI);
110 futureConnect.get(TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
111 } catch (Exception e) {
112 logger.error("An exception occurred while connecting the Event Endpoint : '{}'", e.getMessage());
113 if (futureConnect != null) {
114 futureConnect.cancel(true);
120 public void onWebSocketConnect(Session session) {
121 logger.debug("{} : Connected to {} with hash {}", endpointId, session.getRemoteAddress().getAddress(),
123 connectionState = ConnectionState.CONNECTED;
124 this.session = session;
127 public void closeConnection() {
129 connectionState = ConnectionState.CLOSING;
130 if (session != null && session.isOpen()) {
131 logger.debug("{} : Closing the session", endpointId);
132 session.close(StatusCode.NORMAL, "bye");
134 } catch (Exception e) {
135 logger.error("{} : An exception occurred while closing the session : {}", endpointId, e.getMessage());
136 connectionState = ConnectionState.CLOSED;
141 public void onWebSocketClose(int statusCode, String reason) {
142 logger.debug("{} : Closed the session with status {} for reason {}", endpointId, statusCode, reason);
143 connectionState = ConnectionState.CLOSED;
148 public void onWebSocketText(String message) {
153 public void onWebSocketBinary(byte[] payload, int offset, int length) {
154 BufferedReader in = new BufferedReader(
155 new InputStreamReader(new ByteArrayInputStream(payload), StandardCharsets.UTF_8.newDecoder()
156 .onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT)));
159 while ((str = in.readLine()) != null) {
160 logger.trace("{} : Received raw data '{}'", endpointId, str);
161 if (this.eventHandler != null) {
163 Event event = gson.fromJson(str, Event.class);
164 this.eventHandler.handleEvent(event);
165 } catch (RuntimeException e) {
166 logger.error("{} : An exception occurred while processing raw data : {}", endpointId,
171 } catch (IOException e) {
172 logger.error("{} : An exception occurred while receiving raw data : {}", endpointId, e.getMessage());
177 public void onWebSocketError(Throwable cause) {
178 logger.error("{} : An error occurred in the session : {}", endpointId, cause.getMessage());
179 if (session != null && session.isOpen()) {
180 session.close(StatusCode.ABNORMAL, "Session Error");
184 public void sendMessage(String message) throws IOException {
186 if (session != null) {
187 logger.debug("{} : Sending raw data '{}'", endpointId, message);
188 session.getRemote().sendString(message);
190 throw new IOException("Session is not initialized");
192 } catch (IOException e) {
193 if (session != null && session.isOpen()) {
194 session.close(StatusCode.ABNORMAL, "Send Message Error");
202 if (session != null) {
203 ByteBuffer buffer = ByteBuffer.allocate(8).putLong(System.nanoTime()).flip();
204 session.getRemote().sendPing(buffer);
206 } catch (IOException e) {
207 logger.error("{} : An exception occurred while pinging the remote end : {}", endpointId, e.getMessage());
212 public void onWebSocketPing(ByteBuffer payload) {
213 ByteBuffer buffer = ByteBuffer.allocate(8).putLong(System.nanoTime()).flip();
215 if (session != null) {
216 session.getRemote().sendPing(buffer);
218 } catch (IOException e) {
219 logger.error("{} : An exception occurred while processing a ping message : {}", endpointId, e.getMessage());
224 public void onWebSocketPong(ByteBuffer payload) {
225 long start = payload.getLong();
226 long roundTrip = System.nanoTime() - start;
228 logger.trace("{} : Received a Pong with a roundtrip of {} milliseconds", endpointId,
229 TimeUnit.MILLISECONDS.convert(roundTrip, TimeUnit.NANOSECONDS));
232 public void addEventHandler(EventHandler eventHandler) {
233 this.eventHandler = eventHandler;
236 public boolean isConnected() {
237 return connectionState == ConnectionState.CONNECTED;
240 public static interface EventHandler {
241 public void handleEvent(Event event);
244 private enum ConnectionState {