]> git.basschouten.com Git - openhab-addons.git/blob
f2c3d4e4cd5e505f11f5ac0934d5a4605aa45bce
[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.deconz.internal.netutils;
14
15 import java.net.URI;
16 import java.util.Map;
17 import java.util.Objects;
18 import java.util.concurrent.ConcurrentHashMap;
19 import java.util.concurrent.atomic.AtomicInteger;
20
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.eclipse.jetty.websocket.api.Session;
24 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
25 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
26 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
27 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
28 import org.eclipse.jetty.websocket.api.annotations.WebSocket;
29 import org.eclipse.jetty.websocket.client.WebSocketClient;
30 import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
31 import org.openhab.binding.deconz.internal.types.ResourceType;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 import com.google.gson.Gson;
36
37 /**
38  * Establishes and keeps a websocket connection to the deCONZ software.
39  *
40  * The connection is closed by deCONZ now and then and needs to be re-established.
41  *
42  * @author David Graeff - Initial contribution
43  */
44 @WebSocket
45 @NonNullByDefault
46 public class WebSocketConnection {
47     private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger();
48     private final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
49
50     private final WebSocketClient client;
51     private final String socketName;
52     private final Gson gson;
53
54     private final WebSocketConnectionListener connectionListener;
55     private final Map<String, WebSocketMessageListener> listeners = new ConcurrentHashMap<>();
56
57     private ConnectionState connectionState = ConnectionState.DISCONNECTED;
58     private @Nullable Session session;
59
60     public WebSocketConnection(WebSocketConnectionListener listener, WebSocketClient client, Gson gson) {
61         this.connectionListener = listener;
62         this.client = client;
63         this.client.setMaxIdleTimeout(0);
64         this.gson = gson;
65         this.socketName = "Websocket$" + System.currentTimeMillis() + "-" + INSTANCE_COUNTER.incrementAndGet();
66     }
67
68     public void start(String ip) {
69         if (connectionState == ConnectionState.CONNECTED) {
70             return;
71         } else if (connectionState == ConnectionState.CONNECTING) {
72             logger.debug("{} already connecting", socketName);
73             return;
74         } else if (connectionState == ConnectionState.DISCONNECTING) {
75             logger.warn("{} trying to re-connect while still disconnecting", socketName);
76         }
77         try {
78             URI destUri = URI.create("ws://" + ip);
79             client.start();
80             logger.debug("Trying to connect {} to {}", socketName, destUri);
81             client.connect(this, destUri).get();
82         } catch (Exception e) {
83             connectionListener.connectionLost("Error while connecting: " + e.getMessage());
84         }
85     }
86
87     public void close() {
88         try {
89             connectionState = ConnectionState.DISCONNECTING;
90             client.stop();
91         } catch (Exception e) {
92             logger.debug("{} encountered an error while closing connection", socketName, e);
93         }
94         client.destroy();
95     }
96
97     public void registerListener(ResourceType resourceType, String sensorID, WebSocketMessageListener listener) {
98         listeners.put(getListenerId(resourceType, sensorID), listener);
99     }
100
101     public void unregisterListener(ResourceType resourceType, String sensorID) {
102         listeners.remove(getListenerId(resourceType, sensorID));
103     }
104
105     @SuppressWarnings("unused")
106     @OnWebSocketConnect
107     public void onConnect(Session session) {
108         connectionState = ConnectionState.CONNECTED;
109         logger.debug("{} successfully connected to {}: {}", socketName, session.getRemoteAddress().getAddress(),
110                 session.hashCode());
111         connectionListener.connectionEstablished();
112         this.session = session;
113     }
114
115     @SuppressWarnings({ "null", "unused" })
116     @OnWebSocketMessage
117     public void onMessage(Session session, String message) {
118         if (!session.equals(this.session)) {
119             handleWrongSession(session, message);
120             return;
121         }
122         logger.trace("{} received raw data: {}", socketName, message);
123
124         try {
125             DeconzBaseMessage changedMessage = Objects.requireNonNull(gson.fromJson(message, DeconzBaseMessage.class));
126             if (changedMessage.r == ResourceType.UNKNOWN) {
127                 logger.trace("Received message has unknown resource type. Skipping message.");
128                 return;
129             }
130
131             WebSocketMessageListener listener = listeners.get(getListenerId(changedMessage.r, changedMessage.id));
132             if (listener == null) {
133                 logger.trace(
134                         "Couldn't find listener for id {} with resource type {}. Either no thing for this id has been defined or this is a bug.",
135                         changedMessage.id, changedMessage.r);
136                 return;
137             }
138
139             Class<? extends DeconzBaseMessage> expectedMessageType = changedMessage.r.getExpectedMessageType();
140             if (expectedMessageType == null) {
141                 logger.warn(
142                         "BUG! Could not get expected message type for resource type {}. Please report this incident.",
143                         changedMessage.r);
144                 return;
145             }
146
147             DeconzBaseMessage deconzMessage = gson.fromJson(message, expectedMessageType);
148             if (deconzMessage != null) {
149                 listener.messageReceived(changedMessage.id, deconzMessage);
150
151             }
152         } catch (RuntimeException e) {
153             // we need to catch all processing exceptions, otherwise they could affect the connection
154             logger.warn("{} encountered an error while processing the message {}: {}", socketName, message,
155                     e.getMessage());
156         }
157     }
158
159     @SuppressWarnings("unused")
160     @OnWebSocketError
161     public void onError(@Nullable Session session, Throwable cause) {
162         if (session == null) {
163             logger.trace("Encountered an error while processing on error without session. Connection state is {}: {}",
164                     connectionState, cause.getMessage());
165             return;
166         }
167         if (!session.equals(this.session)) {
168             handleWrongSession(session, "Connection error: " + cause.getMessage());
169             return;
170         }
171         logger.warn("{} connection errored, closing: {}", socketName, cause.getMessage());
172
173         Session storedSession = this.session;
174         if (storedSession != null && storedSession.isOpen()) {
175             storedSession.close(-1, "Processing error");
176         }
177     }
178
179     @SuppressWarnings("unused")
180     @OnWebSocketClose
181     public void onClose(Session session, int statusCode, String reason) {
182         if (!session.equals(this.session)) {
183             handleWrongSession(session, "Connection closed: " + statusCode + " / " + reason);
184             return;
185         }
186         logger.trace("{} closed connection: {} / {}", socketName, statusCode, reason);
187         connectionState = ConnectionState.DISCONNECTED;
188         this.session = null;
189         connectionListener.connectionLost(reason);
190     }
191
192     private void handleWrongSession(Session session, String message) {
193         logger.warn("{}/{} received and discarded message for other session {}: {}.", socketName, session.hashCode(),
194                 session.hashCode(), message);
195         if (session.isOpen()) {
196             // Close the session if it is still open. It should already be closed anyway
197             session.close();
198         }
199     }
200
201     /**
202      * check connection state (successfully connected)
203      *
204      * @return true if connected, false if connecting, disconnecting or disconnected
205      */
206     public boolean isConnected() {
207         return connectionState == ConnectionState.CONNECTED;
208     }
209
210     /**
211      * create a unique identifier for a listener
212      *
213      * @param resourceType the listener resource-type (LIGHT, SENSOR, ...)
214      * @param id the listener id (same as deconz-id)
215      * @return a unique string for this listener
216      */
217     private String getListenerId(ResourceType resourceType, String id) {
218         return resourceType.name() + "$" + id;
219     }
220
221     /**
222      * used internally to represent the connection state
223      */
224     private enum ConnectionState {
225         CONNECTING,
226         CONNECTED,
227         DISCONNECTING,
228         DISCONNECTED
229     }
230 }