]> git.basschouten.com Git - openhab-addons.git/blob
beec69d7f9fd32fc770b312620b53079286cd7c4
[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 connect(URI endpointURI) {
79         if (connectionState == ConnectionState.CONNECTED) {
80             return;
81         } else if (connectionState == ConnectionState.CONNECTING) {
82             logger.debug("{} : Already connecting to {}", endpointId, endpointURI);
83             return;
84         } else if (connectionState == ConnectionState.CLOSING) {
85             logger.warn("{} : Connecting to {} while already closing the connection", endpointId, endpointURI);
86             return;
87         }
88         Future<Session> futureConnect = null;
89         try {
90             if (!client.isRunning()) {
91                 logger.debug("{} : Starting the client to connect to {}", endpointId, endpointURI);
92                 client.start();
93             } else {
94                 logger.debug("{} : The client to connect to {} is already running", endpointId, endpointURI);
95             }
96
97             logger.debug("{} : Connecting to {}", endpointId, endpointURI);
98             connectionState = ConnectionState.CONNECTING;
99             futureConnect = client.connect(this, endpointURI);
100             futureConnect.get(TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
101         } catch (Exception e) {
102             logger.error("An exception occurred while connecting the Event Endpoint : '{}'", e.getMessage());
103             if (futureConnect != null) {
104                 futureConnect.cancel(true);
105             }
106         }
107     }
108
109     @Override
110     public void onWebSocketConnect(Session session) {
111         logger.debug("{} : Connected to {} with hash {}", endpointId, session.getRemoteAddress().getAddress(),
112                 session.hashCode());
113         connectionState = ConnectionState.CONNECTED;
114         this.session = session;
115     }
116
117     public void close() {
118         try {
119             connectionState = ConnectionState.CLOSING;
120             if (session != null && session.isOpen()) {
121                 logger.debug("{} : Closing the session", endpointId);
122                 session.close(StatusCode.NORMAL, "bye");
123             }
124         } catch (Exception e) {
125             logger.error("{} : An exception occurred while closing the session : {}", endpointId, e.getMessage());
126             connectionState = ConnectionState.CLOSED;
127         }
128     }
129
130     @Override
131     public void onWebSocketClose(int statusCode, String reason) {
132         logger.debug("{} : Closed the session with status {} for reason {}", endpointId, statusCode, reason);
133         connectionState = ConnectionState.CLOSED;
134         this.session = null;
135     }
136
137     @Override
138     public void onWebSocketText(String message) {
139         // NoOp
140     }
141
142     @Override
143     public void onWebSocketBinary(byte[] payload, int offset, int length) {
144         BufferedReader in = new BufferedReader(
145                 new InputStreamReader(new ByteArrayInputStream(payload), StandardCharsets.UTF_8.newDecoder()
146                         .onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT)));
147         String str;
148         try {
149             while ((str = in.readLine()) != null) {
150                 logger.trace("{} : Received raw data '{}'", endpointId, str);
151                 if (this.eventHandler != null) {
152                     try {
153                         Event event = gson.fromJson(str, Event.class);
154                         this.eventHandler.handleEvent(event);
155                     } catch (RuntimeException e) {
156                         logger.error("{} : An exception occurred while processing raw data : {}", endpointId,
157                                 e.getMessage());
158                     }
159                 }
160             }
161         } catch (IOException e) {
162             logger.error("{} : An exception occurred while receiving raw data : {}", endpointId, e.getMessage());
163         }
164     }
165
166     @Override
167     public void onWebSocketError(Throwable cause) {
168         logger.error("{} : An error occurred in the session : {}", endpointId, cause.getMessage());
169         if (session != null && session.isOpen()) {
170             session.close(StatusCode.ABNORMAL, "Session Error");
171         }
172     }
173
174     public void sendMessage(String message) throws IOException {
175         try {
176             if (session != null) {
177                 logger.debug("{} : Sending raw data '{}'", endpointId, message);
178                 session.getRemote().sendString(message);
179             } else {
180                 throw new IOException("Session is not initialized");
181             }
182         } catch (IOException e) {
183             if (session != null && session.isOpen()) {
184                 session.close(StatusCode.ABNORMAL, "Send Message Error");
185             }
186             throw e;
187         }
188     }
189
190     public void ping() {
191         try {
192             if (session != null) {
193                 ByteBuffer buffer = ByteBuffer.allocate(8).putLong(System.nanoTime()).flip();
194                 session.getRemote().sendPing(buffer);
195             }
196         } catch (IOException e) {
197             logger.error("{} : An exception occurred while pinging the remote end : {}", endpointId, e.getMessage());
198         }
199     }
200
201     @Override
202     public void onWebSocketPing(ByteBuffer payload) {
203         ByteBuffer buffer = ByteBuffer.allocate(8).putLong(System.nanoTime()).flip();
204         try {
205             if (session != null) {
206                 session.getRemote().sendPing(buffer);
207             }
208         } catch (IOException e) {
209             logger.error("{} : An exception occurred while processing a ping message : {}", endpointId, e.getMessage());
210         }
211     }
212
213     @Override
214     public void onWebSocketPong(ByteBuffer payload) {
215         long start = payload.getLong();
216         long roundTrip = System.nanoTime() - start;
217
218         logger.trace("{} : Received a Pong with a roundtrip of {} milliseconds", endpointId,
219                 TimeUnit.MILLISECONDS.convert(roundTrip, TimeUnit.NANOSECONDS));
220     }
221
222     public void addEventHandler(EventHandler eventHandler) {
223         this.eventHandler = eventHandler;
224     }
225
226     public boolean isConnected() {
227         return connectionState == ConnectionState.CONNECTED;
228     }
229
230     public static interface EventHandler {
231         public void handleEvent(Event event);
232     }
233
234     private enum ConnectionState {
235         CONNECTING,
236         CONNECTED,
237         CLOSING,
238         CLOSED
239     }
240 }