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.deconz.internal.netutils;
17 import java.util.Objects;
18 import java.util.concurrent.ConcurrentHashMap;
19 import java.util.concurrent.atomic.AtomicInteger;
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;
35 import com.google.gson.Gson;
38 * Establishes and keeps a websocket connection to the deCONZ software.
40 * The connection is closed by deCONZ now and then and needs to be re-established.
42 * @author David Graeff - Initial contribution
46 public class WebSocketConnection {
47 private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger();
48 private final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
50 private final WebSocketClient client;
51 private final String socketName;
52 private final Gson gson;
54 private final WebSocketConnectionListener connectionListener;
55 private final Map<String, WebSocketMessageListener> listeners = new ConcurrentHashMap<>();
57 private ConnectionState connectionState = ConnectionState.DISCONNECTED;
58 private @Nullable Session session;
60 public WebSocketConnection(WebSocketConnectionListener listener, WebSocketClient client, Gson gson) {
61 this.connectionListener = listener;
63 this.client.setMaxIdleTimeout(0);
65 this.socketName = "Websocket$" + System.currentTimeMillis() + "-" + INSTANCE_COUNTER.incrementAndGet();
68 public void start(String ip) {
69 if (connectionState == ConnectionState.CONNECTED) {
71 } else if (connectionState == ConnectionState.CONNECTING) {
72 logger.debug("{} already connecting", socketName);
74 } else if (connectionState == ConnectionState.DISCONNECTING) {
75 logger.warn("{} trying to re-connect while still disconnecting", socketName);
78 URI destUri = URI.create("ws://" + ip);
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());
89 connectionState = ConnectionState.DISCONNECTING;
91 } catch (Exception e) {
92 logger.debug("{} encountered an error while closing connection", socketName, e);
97 public void registerListener(ResourceType resourceType, String sensorID, WebSocketMessageListener listener) {
98 listeners.put(getListenerId(resourceType, sensorID), listener);
101 public void unregisterListener(ResourceType resourceType, String sensorID) {
102 listeners.remove(getListenerId(resourceType, sensorID));
105 @SuppressWarnings("unused")
107 public void onConnect(Session session) {
108 connectionState = ConnectionState.CONNECTED;
109 logger.debug("{} successfully connected to {}: {}", socketName, session.getRemoteAddress().getAddress(),
111 connectionListener.connectionEstablished();
112 this.session = session;
115 @SuppressWarnings({ "null", "unused" })
117 public void onMessage(Session session, String message) {
118 if (!session.equals(this.session)) {
119 handleWrongSession(session, message);
122 logger.trace("{} received raw data: {}", socketName, message);
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.");
131 WebSocketMessageListener listener = listeners.get(getListenerId(changedMessage.r, changedMessage.id));
132 if (listener == null) {
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);
139 Class<? extends DeconzBaseMessage> expectedMessageType = changedMessage.r.getExpectedMessageType();
140 if (expectedMessageType == null) {
142 "BUG! Could not get expected message type for resource type {}. Please report this incident.",
147 DeconzBaseMessage deconzMessage = gson.fromJson(message, expectedMessageType);
148 if (deconzMessage != null) {
149 listener.messageReceived(changedMessage.id, deconzMessage);
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,
159 @SuppressWarnings("unused")
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());
167 if (!session.equals(this.session)) {
168 handleWrongSession(session, "Connection error: " + cause.getMessage());
171 logger.warn("{} connection errored, closing: {}", socketName, cause.getMessage());
173 Session storedSession = this.session;
174 if (storedSession != null && storedSession.isOpen()) {
175 storedSession.close(-1, "Processing error");
179 @SuppressWarnings("unused")
181 public void onClose(Session session, int statusCode, String reason) {
182 if (!session.equals(this.session)) {
183 handleWrongSession(session, "Connection closed: " + statusCode + " / " + reason);
186 logger.trace("{} closed connection: {} / {}", socketName, statusCode, reason);
187 connectionState = ConnectionState.DISCONNECTED;
189 connectionListener.connectionLost(reason);
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
202 * check connection state (successfully connected)
204 * @return true if connected, false if connecting, disconnecting or disconnected
206 public boolean isConnected() {
207 return connectionState == ConnectionState.CONNECTED;
211 * create a unique identifier for a listener
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
217 private String getListenerId(ResourceType resourceType, String id) {
218 return resourceType.name() + "$" + id;
222 * used internally to represent the connection state
224 private enum ConnectionState {