]> git.basschouten.com Git - openhab-addons.git/blob
45690511a503e40c2ae9c3e51e883e2efb5dff91
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.tesla.internal.handler;
14
15 import java.io.BufferedReader;
16 import java.io.ByteArrayInputStream;
17 import java.io.IOException;
18 import java.io.InputStreamReader;
19 import java.net.URI;
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;
25
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;
38
39 import com.google.gson.Gson;
40
41 /**
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
45  * websockets alive.
46  *
47  * @author Karel Goderis - Initial contribution
48  */
49 public class TeslaEventEndpoint implements WebSocketListener, WebSocketPingPongListener {
50
51     private static final int TIMEOUT_MILLISECONDS = 3000;
52     private static final int IDLE_TIMEOUT_MILLISECONDS = 30000;
53
54     private final Logger logger = LoggerFactory.getLogger(TeslaEventEndpoint.class);
55
56     private String endpointId;
57     protected WebSocketFactory webSocketFactory;
58
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();
64
65     public TeslaEventEndpoint(ThingUID uid, WebSocketFactory webSocketFactory) {
66         try {
67             this.endpointId = "TeslaEventEndpoint-" + uid.getAsString();
68
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);
75         }
76     }
77
78     public void close() {
79         try {
80             if (client.isRunning()) {
81                 client.stop();
82             }
83         } catch (Exception e) {
84             logger.warn("An exception occurred while stopping the WebSocket client : {}", e.getMessage());
85         }
86     }
87
88     public void connect(URI endpointURI) {
89         if (connectionState == ConnectionState.CONNECTED) {
90             return;
91         } else if (connectionState == ConnectionState.CONNECTING) {
92             logger.debug("{} : Already connecting to {}", endpointId, endpointURI);
93             return;
94         } else if (connectionState == ConnectionState.CLOSING) {
95             logger.warn("{} : Connecting to {} while already closing the connection", endpointId, endpointURI);
96             return;
97         }
98         Future<Session> futureConnect = null;
99         try {
100             if (!client.isRunning()) {
101                 logger.debug("{} : Starting the client to connect to {}", endpointId, endpointURI);
102                 client.start();
103             } else {
104                 logger.debug("{} : The client to connect to {} is already running", endpointId, endpointURI);
105             }
106
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);
115             }
116         }
117     }
118
119     @Override
120     public void onWebSocketConnect(Session session) {
121         logger.debug("{} : Connected to {} with hash {}", endpointId, session.getRemoteAddress().getAddress(),
122                 session.hashCode());
123         connectionState = ConnectionState.CONNECTED;
124         this.session = session;
125     }
126
127     public void closeConnection() {
128         try {
129             connectionState = ConnectionState.CLOSING;
130             if (session != null && session.isOpen()) {
131                 logger.debug("{} : Closing the session", endpointId);
132                 session.close(StatusCode.NORMAL, "bye");
133             }
134         } catch (Exception e) {
135             logger.error("{} : An exception occurred while closing the session : {}", endpointId, e.getMessage());
136             connectionState = ConnectionState.CLOSED;
137         }
138     }
139
140     @Override
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;
144         this.session = null;
145     }
146
147     @Override
148     public void onWebSocketText(String message) {
149         // NoOp
150     }
151
152     @Override
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)));
157         String str;
158         try {
159             while ((str = in.readLine()) != null) {
160                 logger.trace("{} : Received raw data '{}'", endpointId, str);
161                 if (this.eventHandler != null) {
162                     try {
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,
167                                 e.getMessage());
168                     }
169                 }
170             }
171         } catch (IOException e) {
172             logger.error("{} : An exception occurred while receiving raw data : {}", endpointId, e.getMessage());
173         }
174     }
175
176     @Override
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");
181         }
182     }
183
184     public void sendMessage(String message) throws IOException {
185         try {
186             if (session != null) {
187                 logger.debug("{} : Sending raw data '{}'", endpointId, message);
188                 session.getRemote().sendString(message);
189             } else {
190                 throw new IOException("Session is not initialized");
191             }
192         } catch (IOException e) {
193             if (session != null && session.isOpen()) {
194                 session.close(StatusCode.ABNORMAL, "Send Message Error");
195             }
196             throw e;
197         }
198     }
199
200     public void ping() {
201         try {
202             if (session != null) {
203                 ByteBuffer buffer = ByteBuffer.allocate(8).putLong(System.nanoTime()).flip();
204                 session.getRemote().sendPing(buffer);
205             }
206         } catch (IOException e) {
207             logger.error("{} : An exception occurred while pinging the remote end : {}", endpointId, e.getMessage());
208         }
209     }
210
211     @Override
212     public void onWebSocketPing(ByteBuffer payload) {
213         ByteBuffer buffer = ByteBuffer.allocate(8).putLong(System.nanoTime()).flip();
214         try {
215             if (session != null) {
216                 session.getRemote().sendPing(buffer);
217             }
218         } catch (IOException e) {
219             logger.error("{} : An exception occurred while processing a ping message : {}", endpointId, e.getMessage());
220         }
221     }
222
223     @Override
224     public void onWebSocketPong(ByteBuffer payload) {
225         long start = payload.getLong();
226         long roundTrip = System.nanoTime() - start;
227
228         logger.trace("{} : Received a Pong with a roundtrip of {} milliseconds", endpointId,
229                 TimeUnit.MILLISECONDS.convert(roundTrip, TimeUnit.NANOSECONDS));
230     }
231
232     public void addEventHandler(EventHandler eventHandler) {
233         this.eventHandler = eventHandler;
234     }
235
236     public boolean isConnected() {
237         return connectionState == ConnectionState.CONNECTED;
238     }
239
240     public static interface EventHandler {
241         public void handleEvent(Event event);
242     }
243
244     private enum ConnectionState {
245         CONNECTING,
246         CONNECTED,
247         CLOSING,
248         CLOSED
249     }
250 }